Compare commits
570
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a409db9ea3 | ||
|
|
8016b034ac | ||
|
|
5f4fcc28ae | ||
|
|
a99a76a3e0 | ||
|
|
b7028c112d | ||
|
|
2229a383c5 | ||
|
|
ea1f1a5b3d | ||
|
|
db2657a5b3 | ||
|
|
ff2c233381 | ||
|
|
ab9176b10d | ||
|
|
7c77603711 | ||
|
|
5cc41b8e8c | ||
|
|
d470ca3a8f | ||
|
|
f2d55f435d | ||
|
|
ea63c5ea88 | ||
|
|
b01616f744 | ||
|
|
811ec5c7ee | ||
|
|
1950bcdc16 | ||
|
|
972ecef2be | ||
|
|
540efc8819 | ||
|
|
7d02608a14 | ||
|
|
7861e498c5 | ||
|
|
dd28de3cac | ||
|
|
cf872c071b | ||
|
|
6c6b5cac25 | ||
|
|
50ce8fbfa3 | ||
|
|
d0ef8e723f | ||
|
|
c434154d84 | ||
|
|
bc038c8d74 | ||
|
|
7528552280 | ||
|
|
b71669630a | ||
|
|
52eb92d527 | ||
|
|
46bce400af | ||
|
|
b681d7a923 | ||
|
|
ec086448ea | ||
|
|
476b0e89ef | ||
|
|
c1b327ca05 | ||
|
|
fa39949b1a | ||
|
|
f1290af45b | ||
|
|
10954ef117 | ||
|
|
0540f7a9eb | ||
|
|
a4aab09360 | ||
|
|
af7518c1a2 | ||
|
|
5be029fb57 | ||
|
|
d82999b141 | ||
|
|
1ffb7fa9a7 | ||
|
|
2566bee0ce | ||
|
|
3d113a06d4 | ||
|
|
2e3b8c86da | ||
|
|
058e3aefa1 | ||
|
|
7a4ba3a5af | ||
|
|
2f5f25ea75 | ||
|
|
fd476ce512 | ||
|
|
9588cdc8a0 | ||
|
|
17769022bc | ||
|
|
a46dc0388c | ||
|
|
553f79425f | ||
|
|
72cd6e9fd1 | ||
|
|
f63984e25a | ||
|
|
48a91dca3d | ||
|
|
5eb637a3a8 | ||
|
|
f722eb1a83 | ||
|
|
7759bbc648 | ||
|
|
65de7baeda | ||
|
|
e243404480 | ||
|
|
f735e6d846 | ||
|
|
9fbaf7bcf6 | ||
|
|
1959e11e5c | ||
|
|
c25d354fc0 | ||
|
|
e5d90a71d9 | ||
|
|
785a4eff46 | ||
|
|
627112b114 | ||
|
|
9b3bd9d02c | ||
|
|
2c56c614f3 | ||
|
|
3ccaa7004e | ||
|
|
289113661f | ||
|
|
74ce22dd26 | ||
|
|
170ce1cbfb | ||
|
|
e1b761e848 | ||
|
|
68592a7df8 | ||
|
|
b28938f66a | ||
|
|
adab97f505 | ||
|
|
edf51aac2d | ||
|
|
dbc6474fc3 | ||
|
|
ed68c4174f | ||
|
|
3abbff13d5 | ||
|
|
304b089113 | ||
|
|
29a163ec46 | ||
|
|
76fbba739f | ||
|
|
7204e882f1 | ||
|
|
a8d775ca09 | ||
|
|
983b9b6f34 | ||
|
|
84c910b19c | ||
|
|
1fcb25193d | ||
|
|
f52b2fe49f | ||
|
|
24a9fa998a | ||
|
|
4785271ebb | ||
|
|
f9d765ad92 | ||
|
|
50868988c0 | ||
|
|
81ae867a43 | ||
|
|
2649ceb6bb | ||
|
|
6e02bc877f | ||
|
|
f61ad83e11 | ||
|
|
feeb47d3da | ||
|
|
0ff1b07096 | ||
|
|
8674eed0c5 | ||
|
|
e2ddd5412a | ||
|
|
f37844c953 | ||
|
|
d703c76704 | ||
|
|
6e25d4e75a | ||
|
|
0702198f0a | ||
|
|
43df7aeda7 | ||
|
|
c8becb8203 | ||
|
|
44d813504a | ||
|
|
3fc8910c9a | ||
|
|
563b6ab888 | ||
|
|
d7e44d3a22 | ||
|
|
a99b488ae1 | ||
|
|
85d0077107 | ||
|
|
9510c9eddc | ||
|
|
9550d1a68b | ||
|
|
b04819aa36 | ||
|
|
f43dccb12d | ||
|
|
9cf05f44d0 | ||
|
|
f6ae34c9e6 | ||
|
|
528af14e00 | ||
|
|
dbe652dcce | ||
|
|
1658638938 | ||
|
|
63db4c1dd2 | ||
|
|
43f51f7f07 | ||
|
|
f74b08b02e | ||
|
|
9dd07bef93 | ||
|
|
231c700946 | ||
|
|
37daa0e657 | ||
|
|
6dfd3acad0 | ||
|
|
ce3ed3c001 | ||
|
|
0ad425b982 | ||
|
|
cf5629a6dd | ||
|
|
37fb9c1abb | ||
|
|
157b8d4f3f | ||
|
|
1c3fb231b7 | ||
|
|
b72dd35e04 | ||
|
|
0b2a1cd293 | ||
|
|
f1f0c0c6de | ||
|
|
36eae17751 | ||
|
|
7a57b75ca3 | ||
|
|
0ea9ef5b41 | ||
|
|
e13790ba08 | ||
|
|
171a1b0840 | ||
|
|
8604ba2547 | ||
|
|
bd365d575f | ||
|
|
0e262d5972 | ||
|
|
774e6ffedf | ||
|
|
de2542ece6 | ||
|
|
c29194a30a | ||
|
|
8870d93a44 | ||
|
|
6a26440c4d | ||
|
|
2735d6877f | ||
|
|
117f5655d7 | ||
|
|
c808bb2610 | ||
|
|
5e7b3790eb | ||
|
|
07ea150ad3 | ||
|
|
9280bbfbca | ||
|
|
abf076fceb | ||
|
|
0c0997624f | ||
|
|
67090e0d6f | ||
|
|
385129c434 | ||
|
|
327126307d | ||
|
|
36dad683d8 | ||
|
|
9be9279239 | ||
|
|
ffc5c20fec | ||
|
|
65453eb7d6 | ||
|
|
2ad1c808c2 | ||
|
|
3898e5e494 | ||
|
|
b16132d61b | ||
|
|
d3aaf642bd | ||
|
|
1902c486fd | ||
|
|
7e90e7c819 | ||
|
|
279d3f4885 | ||
|
|
2c0f7f91c1 | ||
|
|
d699f8b3e7 | ||
|
|
56ced3f8a2 | ||
|
|
ed4f8681c8 | ||
|
|
d52008c536 | ||
|
|
8d0d0acc3b | ||
|
|
9a1c3db7f9 | ||
|
|
96d78213ff | ||
|
|
cdb54658ee | ||
|
|
4ce37ee3ea | ||
|
|
c62b87c7f7 | ||
|
|
ba04858328 | ||
|
|
a12fca6220 | ||
|
|
136702654e | ||
|
|
23adad403a | ||
|
|
1d7952ed5a | ||
|
|
ca54569033 | ||
|
|
3a72d9f0e7 | ||
|
|
da0acb2827 | ||
|
|
0baffe96f3 | ||
|
|
5e6f5b2653 | ||
|
|
990b7042c4 | ||
|
|
aca05da68b | ||
|
|
0677d92c37 | ||
|
|
e7cfa26b4d | ||
|
|
8960470a09 | ||
|
|
6006dceafa | ||
|
|
1c9e92812e | ||
|
|
d7ea690685 | ||
|
|
e85c6fe628 | ||
|
|
48967675e2 | ||
|
|
f5e54778d6 | ||
|
|
19f83ec2a5 | ||
|
|
7919a63e04 | ||
|
|
00212ea09d | ||
|
|
96c902fcc3 | ||
|
|
c177ae67d6 | ||
|
|
b73cddab53 | ||
|
|
8f88c76e2e | ||
|
|
c977a4e958 | ||
|
|
e2e8d8de30 | ||
|
|
7f007ffa1f | ||
|
|
913f915bf0 | ||
|
|
02e7a6df3a | ||
|
|
7be869b2b6 | ||
|
|
15c4776918 | ||
|
|
d3102b57cc | ||
|
|
9bef20de5b | ||
|
|
9231c04c6a | ||
|
|
36ba4d35e0 | ||
|
|
b80959866a | ||
|
|
de1eba025a | ||
|
|
28e575f364 | ||
|
|
d8368b4b92 | ||
|
|
cc10bb518c | ||
|
|
90131f6395 | ||
|
|
1814867c09 | ||
|
|
390bf8a489 | ||
|
|
74aa78c748 | ||
|
|
379f280c11 | ||
|
|
7d1e69c007 | ||
|
|
b905ce7216 | ||
|
|
4bb7db3f71 | ||
|
|
b3f48d4603 | ||
|
|
90763c73a7 | ||
|
|
635419dc7c | ||
|
|
2356d5edbf | ||
|
|
9eed0a1f4e | ||
|
|
a9a6ab4520 | ||
|
|
e08a764591 | ||
|
|
8b43f766b3 | ||
|
|
cb9cad9783 | ||
|
|
216becff6e | ||
|
|
836673198a | ||
|
|
e34739a0b1 | ||
|
|
7c592e6acc | ||
|
|
0f8637be23 | ||
|
|
3b597aeac7 | ||
|
|
c43b2297a4 | ||
|
|
1b337a291d | ||
|
|
16b662875f | ||
|
|
307f61934e | ||
|
|
00265f2f58 | ||
|
|
93d558edbc | ||
|
|
0f2b1aa2e6 | ||
|
|
3f6bac99ab | ||
|
|
46e2955642 | ||
|
|
ac9bf6b35f | ||
|
|
869b2ccb82 | ||
|
|
3ddfdfe9b0 | ||
|
|
3860e74579 | ||
|
|
3b49a6ec2a | ||
|
|
2f89588358 | ||
|
|
0513df944f | ||
|
|
ddf319b9d5 | ||
|
|
13f5bc0aab | ||
|
|
e6f3fcb604 | ||
|
|
eaa342d479 | ||
|
|
ac11f3e462 | ||
|
|
106b394c44 | ||
|
|
6c1505685b | ||
|
|
2e681be690 | ||
|
|
5448f2a426 | ||
|
|
7647c355af | ||
|
|
a95fca40f9 | ||
|
|
148ba253cf | ||
|
|
640d481018 | ||
|
|
4c18513661 | ||
|
|
37b4d2d62e | ||
|
|
9c762fb346 | ||
|
|
2c9e9e246a | ||
|
|
ab721c1625 | ||
|
|
2864f342cc | ||
|
|
92511db446 | ||
|
|
44c2720f80 | ||
|
|
35cebdebce | ||
|
|
e4437cc654 | ||
|
|
fe2c400a2a | ||
|
|
39875585ef | ||
|
|
7796ead644 | ||
|
|
531d2f3a0e | ||
|
|
8595a12f7f | ||
|
|
f76171ac84 | ||
|
|
1605c8ad50 | ||
|
|
63e77958fb | ||
|
|
0bae0ea5ff | ||
|
|
c67e9ae617 | ||
|
|
7e23ac14ff | ||
|
|
a0a7e37921 | ||
|
|
c01c888503 | ||
|
|
58dbe547bb | ||
|
|
3cf4fc6b33 | ||
|
|
411f8ab275 | ||
|
|
8952c4a352 | ||
|
|
93248797a0 | ||
|
|
dace72f5cc | ||
|
|
f468a028d0 | ||
|
|
a9b889234a | ||
|
|
88cec73644 | ||
|
|
91e57c6555 | ||
|
|
6c5f40f788 | ||
|
|
a9920db381 | ||
|
|
d04b8c5e32 | ||
|
|
3627419e12 | ||
|
|
3446ce9ca2 | ||
|
|
582b88e248 | ||
|
|
2db744d170 | ||
|
|
fa33be1b4a | ||
|
|
296e14f0f6 | ||
|
|
4c8444d077 | ||
|
|
2e2030a533 | ||
|
|
f4ec272567 | ||
|
|
b7dd44a028 | ||
|
|
081e406d51 | ||
|
|
e3a937649f | ||
|
|
d09c9b2b03 | ||
|
|
8624285d09 | ||
|
|
f1cbe81699 | ||
|
|
aea2e0beb3 | ||
|
|
fdd08bc1a2 | ||
|
|
98dbd30327 | ||
|
|
2565b7689f | ||
|
|
52305c0107 | ||
|
|
bed07d440b | ||
|
|
4e832a084f | ||
|
|
cc20868bde | ||
|
|
14602282e9 | ||
|
|
2e3f298036 | ||
|
|
44cb9025b0 | ||
|
|
02145ba244 | ||
|
|
58e616ea47 | ||
|
|
2ec7296c0f | ||
|
|
d0f328dd2d | ||
|
|
2154d713cb | ||
|
|
ea0ebcc2e9 | ||
|
|
b9cc3b46bb | ||
|
|
439ad539a8 | ||
|
|
10e8e5a446 | ||
|
|
34e55b962d | ||
|
|
1fdb9ff1b6 | ||
|
|
4f255a32e2 | ||
|
|
814baa27d6 | ||
|
|
9531dca892 | ||
|
|
18df783d86 | ||
|
|
03d7485037 | ||
|
|
1a55ae0ac5 | ||
|
|
76d6ba7969 | ||
|
|
f95d3130ff | ||
|
|
09696071ef | ||
|
|
e5d11d200c | ||
|
|
6a87622b58 | ||
|
|
9bc88a7e03 | ||
|
|
4c0e6a6209 | ||
|
|
017cb7c9df | ||
|
|
17867671f1 | ||
|
|
8896e41726 | ||
|
|
12a55dd626 | ||
|
|
77a32b3adc | ||
|
|
2f10a1c7e6 | ||
|
|
a3bed52479 | ||
|
|
6b7dd6042d | ||
|
|
c4d5477f8b | ||
|
|
f5342f1541 | ||
|
|
1e336e290d | ||
|
|
9ebbca2557 | ||
|
|
8a2108fc14 | ||
|
|
03c7a9303a | ||
|
|
a0c5527bf6 | ||
|
|
8155fb0c8f | ||
|
|
06b46ef27c | ||
|
|
1858f0c37d | ||
|
|
792a225987 | ||
|
|
2e324dc9c5 | ||
|
|
09f7a4323c | ||
|
|
dd73e9b472 | ||
|
|
b4f5ddc822 | ||
|
|
d2f33959ed | ||
|
|
652533d071 | ||
|
|
73e350c845 | ||
|
|
9dec15dbfa | ||
|
|
41ba4dc23c | ||
|
|
790cff3406 | ||
|
|
7fb87ae12f | ||
|
|
2407e61913 | ||
|
|
6147263647 | ||
|
|
cb71cbe1d5 | ||
|
|
080356fe9c | ||
|
|
d4e19284f0 | ||
|
|
ae9153b78e | ||
|
|
ca58e9ac26 | ||
|
|
432888e468 | ||
|
|
c918b9492b | ||
|
|
caef6a3652 | ||
|
|
b2acafd6e7 | ||
|
|
349fdfe18d | ||
|
|
efed6e001f | ||
|
|
6b0e61cdf7 | ||
|
|
0acfa77492 | ||
|
|
ffefd08761 | ||
|
|
a61662429a | ||
|
|
57244bcbfe | ||
|
|
3fd9ff46a3 | ||
|
|
d4ad727f2a | ||
|
|
516ff7feeb | ||
|
|
0eb332aefd | ||
|
|
ff07787a9a | ||
|
|
b5959ccc33 | ||
|
|
b80f160914 | ||
|
|
3b866be3ab | ||
|
|
e55b22c821 | ||
|
|
f06e6f1e1e | ||
|
|
5f3e7361b4 | ||
|
|
835611a11e | ||
|
|
ee9a23ca79 | ||
|
|
448bed17ec | ||
|
|
b4d0f8a15e | ||
|
|
72a97f6cb7 | ||
|
|
8aadf07968 | ||
|
|
a6f98d11ab | ||
|
|
4ee0593619 | ||
|
|
d4db9b51f0 | ||
|
|
f67eb75c7b | ||
|
|
6f4322bf6a | ||
|
|
66ace947c6 | ||
|
|
4d9c70bf09 | ||
|
|
4c944c8323 | ||
|
|
b65254c2cc | ||
|
|
5b3c9ef247 | ||
|
|
027da38574 | ||
|
|
80e4602621 | ||
|
|
07310c804a | ||
|
|
4641ce4048 | ||
|
|
142842a36b | ||
|
|
6371fc4e8c | ||
|
|
3fac5f7a06 | ||
|
|
80f3966d39 | ||
|
|
69a439d13a | ||
|
|
251ac2cf47 | ||
|
|
2dc0350213 | ||
|
|
7e40937cdd | ||
|
|
761a7df29f | ||
|
|
b3a84fc3a8 | ||
|
|
262bcd07b9 | ||
|
|
8ca77d93b3 | ||
|
|
1b4f0362b5 | ||
|
|
ce60288892 | ||
|
|
2515b90497 | ||
|
|
3ce9f8090f | ||
|
|
68da97e138 | ||
|
|
ef28b97a1b | ||
|
|
e43a0a8977 | ||
|
|
2ee2d3405e | ||
|
|
af204a3156 | ||
|
|
109b22be62 | ||
|
|
aa4b06c125 | ||
|
|
96fce1d661 | ||
|
|
3d471198f5 | ||
|
|
05d0653a5b | ||
|
|
6758f4e90d | ||
|
|
6e6b57c49c | ||
|
|
b4f72b9528 | ||
|
|
cd81ee3140 | ||
|
|
6ddba14908 | ||
|
|
0fc0fc33da | ||
|
|
529761b45f | ||
|
|
bac64110c1 | ||
|
|
d04de253e7 | ||
|
|
8c7a801800 | ||
|
|
01df80d358 | ||
|
|
06248cfe6c | ||
|
|
a3af93d4bc | ||
|
|
646ee0274b | ||
|
|
fe0a74cfc4 | ||
|
|
5bed587da9 | ||
|
|
f7a40f83a9 | ||
|
|
a66bc4982f | ||
|
|
04051eeacc | ||
|
|
f33dc8ce37 | ||
|
|
306f983c8e | ||
|
|
9e9d616814 | ||
|
|
5a09beea31 | ||
|
|
8c2b3fa487 | ||
|
|
62e89960f7 | ||
|
|
e2b0c0ea89 | ||
|
|
3eed30567d | ||
|
|
565babd1d3 | ||
|
|
455b51a998 | ||
|
|
f136181216 | ||
|
|
463ef48052 | ||
|
|
64004329da | ||
|
|
f497928054 | ||
|
|
0911ebfa45 | ||
|
|
beedf946c9 | ||
|
|
5b31314f02 | ||
|
|
4b15fef30c | ||
|
|
e11a86af7b | ||
|
|
f4f8f426d0 | ||
|
|
b8eb84165e | ||
|
|
273ff757af | ||
|
|
f84e2eef29 | ||
|
|
b4eb5ab6d1 | ||
|
|
1ae6a4a223 | ||
|
|
e1a429c42a | ||
|
|
790e8ca50f | ||
|
|
7ba43ea4f4 | ||
|
|
39c858af64 | ||
|
|
093861b2e6 | ||
|
|
4ef487a8c4 | ||
|
|
849ebb966a | ||
|
|
f1b0e34194 | ||
|
|
7cdc7d2cef | ||
|
|
db4672dcda | ||
|
|
fc2b601687 | ||
|
|
493d44fa19 | ||
|
|
5d3a1568a2 | ||
|
|
be29194a57 | ||
|
|
d067da2300 | ||
|
|
6d0c10e1cc | ||
|
|
609bfa1277 | ||
|
|
158e53e799 | ||
|
|
3cc67a4e9e | ||
|
|
df80eb2952 | ||
|
|
b78eb7b9bf | ||
|
|
65ce143cfc | ||
|
|
8dbb2a6895 | ||
|
|
a24fe52182 | ||
|
|
d0131b1044 | ||
|
|
307ef86493 | ||
|
|
a1a9fae9fa | ||
|
|
56984b3df0 | ||
|
|
7a261d56fc | ||
|
|
4ee2280565 | ||
|
|
a24900a957 | ||
|
|
ef17d3dc3e | ||
|
|
d711763c80 | ||
|
|
8318bf741a | ||
|
|
17c1915810 | ||
|
|
674ea94588 | ||
|
|
090184c1e9 | ||
|
|
31fa0b50a3 | ||
|
|
374632bbc6 | ||
|
|
8a36c9dac9 | ||
|
|
1900e21f72 | ||
|
|
2c85483db5 | ||
|
|
63db9fb7d4 | ||
|
|
5155744ec5 | ||
|
|
fedc8be778 | ||
|
|
125939e5b3 | ||
|
|
3b410b5f90 | ||
|
|
d7bac2048f | ||
|
|
dd4bee73c6 |
@@ -0,0 +1,5 @@
|
||||
.*
|
||||
Dockerfile
|
||||
LICENSE
|
||||
README.md
|
||||
renovate.json
|
||||
@@ -0,0 +1,32 @@
|
||||
name: build_develop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*"
|
||||
- "!main"
|
||||
paths-ignore:
|
||||
- "README.md"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Docker
|
||||
uses: https://github.com/papodaca/install-docker-action@v1
|
||||
- name: Log into container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.cheezers.org
|
||||
username: imcompiling
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
- name: set env
|
||||
run: echo "GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
- name: Build Docker Image
|
||||
run: docker build -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_BRANCH}} .
|
||||
- name: Push Docker Image
|
||||
run: docker push gitea.cheezers.org/imcompiling/agon:${{env.GIT_BRANCH}}
|
||||
@@ -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
|
||||
@@ -0,0 +1,28 @@
|
||||
name: build_release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v[0-9].[0-9].[0-9]"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Docker
|
||||
uses: https://github.com/papodaca/install-docker-action@v1
|
||||
- name: Log into container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.cheezers.org
|
||||
username: imcompiling
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
- name: Set env
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- name: Build Docker Images
|
||||
run: docker build -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}} .
|
||||
- name: Push Tagged Image
|
||||
run: docker push gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}}
|
||||
+7
-5
@@ -105,6 +105,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
poetry.lock
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@@ -124,13 +125,14 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
|
||||
# logging
|
||||
nextcord.log
|
||||
|
||||
# data file for bot
|
||||
highscores_data.json
|
||||
highscores_message_map.json
|
||||
highscores_config.json
|
||||
highscores_config.json
|
||||
deleted_backups/
|
||||
highscores_data/
|
||||
TODO.txt
|
||||
sql_tester.py
|
||||
/database
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"kevinrose.vsc-python-indent",
|
||||
"charliermarsh.ruff",
|
||||
"donjayamanne.python-extension-pack"
|
||||
]
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Module",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "agon",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"dotenv",
|
||||
"gmtime",
|
||||
"highscore",
|
||||
"highscores",
|
||||
"imcompiling",
|
||||
"iterrows",
|
||||
"nextcord",
|
||||
"ondelete"
|
||||
],
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportOptionalMemberAccess": "warning",
|
||||
"reportArgumentType": "information",
|
||||
"reportAttributeAccessIssue": "warning"
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
FROM python:3.12.3-slim
|
||||
|
||||
# install uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.12.13 /uv /uvx /bin/
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get -y install sqlite3
|
||||
|
||||
# Copy project to image
|
||||
ADD . /home
|
||||
|
||||
# Sync the project into a new environment, using the frozen lockfile
|
||||
WORKDIR /home
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
CMD ["uv", "run", "--frozen", "--no-dev", "-m", "agon"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Empty init file for off_cooldown_bot module."""
|
||||
@@ -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
@@ -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)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Cog loader. Adds all command cogs to the bot."""
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from agon.cogs.commands.create import CreateCog
|
||||
from agon.cogs.commands.delete import DeleteCog
|
||||
from agon.cogs.commands.get import GetCog
|
||||
from agon.cogs.commands.help import HelpCog
|
||||
from agon.cogs.commands.refresh import RefreshCog
|
||||
from agon.cogs.commands.set import SetCog
|
||||
from agon.cogs.commands.submission import SubmissionCog
|
||||
from agon.cogs.commands.update import UpdateCog
|
||||
from agon.cogs.commands.view import ViewCog
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot) -> None:
|
||||
"""Adds all command classes to bot."""
|
||||
bot.add_cog(CreateCog(bot))
|
||||
bot.add_cog(DeleteCog())
|
||||
bot.add_cog(GetCog())
|
||||
bot.add_cog(HelpCog())
|
||||
bot.add_cog(RefreshCog(bot))
|
||||
bot.add_cog(SetCog())
|
||||
bot.add_cog(SubmissionCog())
|
||||
bot.add_cog(UpdateCog(bot))
|
||||
bot.add_cog(ViewCog())
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Commands for creating items in bot."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import exc, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup
|
||||
from agon.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
|
||||
|
||||
|
||||
class CreateCog(commands.Cog, name="Create Cog"):
|
||||
"""Class holds commands for creating things."""
|
||||
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
"""Default constructor for CreateCog class."""
|
||||
self._bot = bot
|
||||
|
||||
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
|
||||
async def create(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Parent slash command for creating things. Never called."""
|
||||
|
||||
@create.subcommand(description="Create a new Category. Must not exist already in Group.")
|
||||
async def category( # noqa: PLR0913 need all these arguments because discord command.
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True),
|
||||
category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True),
|
||||
is_ascending: bool = nextcord.SlashOption(description="If lower value is a better score.", required=True), # noqa: FBT001
|
||||
is_time_record: bool = nextcord.SlashOption( # noqa: FBT001
|
||||
description="If the score should be shown in time format. Cannot be changed.", required=True
|
||||
),
|
||||
is_validated: bool = nextcord.SlashOption( # noqa: FBT001
|
||||
description="If scores need to be validated before posted.", required=True
|
||||
),
|
||||
size: int = nextcord.SlashOption(
|
||||
description="How many scores to post in Category. Default is 3.", required=False, default=3, min_value=1
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Creates a new category to be placed into group_id group.
|
||||
|
||||
Fails if the category name already exists within the group. Then calls send_highscore_message().
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction for the command.
|
||||
group_id (str): Id of group to add category to. Is SlashOption.
|
||||
category_name (str): Name of new category. Is SlashOption.
|
||||
is_ascending (bool): If the scores are sorted ascending or not. Is SlashOption.
|
||||
is_time_record (bool): If the scores are a time record or not. Is SlashOption.
|
||||
is_validated (bool): If the scores should be validated or not. Is SlashOption.
|
||||
size (int): How many sores to show in the category message. Is SlashOption.
|
||||
"""
|
||||
new_category = Category(
|
||||
name=category_name,
|
||||
group_id=int(group_id),
|
||||
size=size,
|
||||
is_ascending=is_ascending,
|
||||
is_time_record=is_time_record,
|
||||
is_validated=is_validated,
|
||||
)
|
||||
|
||||
# Open connection to engine and add new category.
|
||||
with Session(engine) as session:
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
|
||||
group = session.scalar(stmt)
|
||||
try:
|
||||
session.add(new_category)
|
||||
session.commit()
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
await interaction.response.send_message(
|
||||
f'"{category_name}" has been added to "{group.name}"!', ephemeral=True
|
||||
)
|
||||
except exc.IntegrityError:
|
||||
# Category name already exists in group
|
||||
session.rollback()
|
||||
await interaction.response.send_message(
|
||||
f'"{category_name}" already exists in "{group.name}". Please rename.', ephemeral=True
|
||||
)
|
||||
|
||||
@create.subcommand(description="Create a new Category Group. Must not exist already for Guild.")
|
||||
async def group(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_name: str = nextcord.SlashOption(description="Name of new Group.", required=True),
|
||||
pad_length: int = nextcord.SlashOption(
|
||||
description="Number of characters wide a category is printed. Default is 26.",
|
||||
required=False,
|
||||
default=26,
|
||||
min_value=7,
|
||||
max_value=MAX_LINE_LENGTH,
|
||||
),
|
||||
username_length: int = nextcord.SlashOption(
|
||||
description="Number of characters of a username to display. Default is 12.",
|
||||
required=False,
|
||||
default=12,
|
||||
min_value=1,
|
||||
max_value=30,
|
||||
),
|
||||
channel: nextcord.TextChannel | nextcord.Thread | None = nextcord.SlashOption(
|
||||
description="Channel to post Group in. If left empty, defaults to current channel.",
|
||||
required=False,
|
||||
default=None,
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Creates a new category group to be placed into the database with the guild's id.
|
||||
|
||||
Fails if the category group name already exists within the guild. Then calls send_highscore_message().
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction for the command.
|
||||
group_name (str): Name of new Group. Is SlashOption.
|
||||
pad_length (int): Number of characters wide a category is printed. Is SlashOption.
|
||||
username_length (int): Number of characters of a username to display. Is SlashOption.
|
||||
channel (Optional[nextcord.abc.GuildChannel]): Channel to post group in. If None, defaults to current
|
||||
channel. Is SlashOption.
|
||||
"""
|
||||
# if no channel provided, use current channel.
|
||||
if channel is None:
|
||||
channel_id: int = interaction.channel.id
|
||||
else:
|
||||
channel_id: int = channel.id
|
||||
|
||||
new_group = CategoryGroup(
|
||||
name=group_name,
|
||||
channel_id=channel_id,
|
||||
guild_id=interaction.guild_id,
|
||||
pad_length=pad_length,
|
||||
username_length=username_length,
|
||||
)
|
||||
|
||||
with Session(engine) as session:
|
||||
try:
|
||||
session.add(new_group)
|
||||
session.commit()
|
||||
await send_highscore_message(session, interaction.guild, new_group)
|
||||
await interaction.response.send_message(f'"{group_name}" has been created!', ephemeral=True)
|
||||
except exc.IntegrityError:
|
||||
# Category Group already exists in guild
|
||||
session.rollback()
|
||||
await interaction.response.send_message(
|
||||
f'"{group_name}" already exists in "{interaction.guild.name}". Please rename.', ephemeral=True
|
||||
)
|
||||
|
||||
@category.on_autocomplete("group_id")
|
||||
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
|
||||
"""
|
||||
Collects list of group names and compares to input group name for autocomplete assistance.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction with command.
|
||||
group_id (str): String to run autocomplete compared to.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
|
||||
@@ -0,0 +1,288 @@
|
||||
"""Commands for deleting groups/categories/scores in database and messages in discord."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import distinct, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup, Score
|
||||
from agon.utils import (
|
||||
autofill_categories,
|
||||
autofill_groups,
|
||||
autofill_users_in_group_category,
|
||||
autofill_users_in_guild,
|
||||
send_highscore_message,
|
||||
)
|
||||
|
||||
from .views.delete_confirm import DeleteConfirmView, create_delete_id
|
||||
|
||||
|
||||
class DeleteCog(commands.Cog, name="Delete Cog"):
|
||||
"""Class holds commands for deleting."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Default constructor for DeleteCog class."""
|
||||
|
||||
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
|
||||
async def delete(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Parent slash command for deleting things. Never called."""
|
||||
|
||||
@delete.subcommand()
|
||||
async def all(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Subcommand parent for deleting all of a thing. Never called."""
|
||||
|
||||
@all.subcommand(description="Clears highscores channel of all highscore messages. Does not change data.")
|
||||
async def channel_messages(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
channel: nextcord.TextChannel | nextcord.Thread | None = nextcord.SlashOption(
|
||||
description="Channel to clear of messages.",
|
||||
required=False,
|
||||
default=None,
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Deletes all group messages from channel. Does not change data.
|
||||
|
||||
Does not delete highscores channel id. Uses current channel if not supplied a channel.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command
|
||||
channel (Optional[nextcord.abc.GuildChannel]): Channel to clear of messages.
|
||||
Is a SlashOption. Limited to text channels. Defaults to None.
|
||||
"""
|
||||
if not channel:
|
||||
if isinstance(interaction.channel, nextcord.TextChannel | nextcord.Thread):
|
||||
channel = interaction.channel
|
||||
else:
|
||||
# This should never happen.
|
||||
await interaction.response.send_message("Channel is not a text channel somehow.", ephemeral=True)
|
||||
return
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(CategoryGroup)
|
||||
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
|
||||
.where(col(CategoryGroup.channel_id) == channel.id)
|
||||
)
|
||||
# TODO test when nothing in channel
|
||||
groups = session.scalars(stmt)
|
||||
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
for group in groups:
|
||||
if group.message_id is not None:
|
||||
try:
|
||||
message: nextcord.Message = await channel.fetch_message(group.message_id)
|
||||
await message.delete()
|
||||
except nextcord.NotFound:
|
||||
# silently ignore that messages are not found. They got deleted already.
|
||||
pass
|
||||
|
||||
await interaction.send("Highscore messages deleted.", ephemeral=True)
|
||||
|
||||
@all.subcommand(
|
||||
description="Removes all of a user's scores. Warning: data is destroyed, use with care.",
|
||||
)
|
||||
async def users(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
user_name: str = nextcord.SlashOption(
|
||||
description="User to remove scores for. Note: This is not always a user's discord name.", required=True
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Searches all of guild's scores data for any score from the user and removes.
|
||||
|
||||
Updates highscore message if user was removed from that Category Group.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
user_name (str): User to remove from highscores
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(distinct(col(Score.category_id))).where(col(Score.username) == user_name)
|
||||
category_ids: list[int] = list(session.scalars(stmt))
|
||||
|
||||
stmt = select(Score).where(col(Score.username) == user_name)
|
||||
for score in session.scalars(stmt):
|
||||
session.delete(score)
|
||||
session.commit()
|
||||
|
||||
group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
|
||||
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id).in_(group_ids))
|
||||
|
||||
for group in session.scalars(stmt):
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
|
||||
await interaction.send(f'User "{user_name}" was removed from all Category Groups.', ephemeral=True)
|
||||
|
||||
@delete.subcommand(description="Delete a category from a group. Deletion is permanent.")
|
||||
async def category(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", description="Group to delete from.", required=True),
|
||||
category_id: str = nextcord.SlashOption(name="category", description="Category to delete.", required=True),
|
||||
) -> None:
|
||||
"""
|
||||
Calls the delete confirmation view for the given group and category.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction of command.
|
||||
group_id (str): Group to delete from. Is SlashOption.
|
||||
category_id (str): Category to delete. Is SlashOption.
|
||||
"""
|
||||
await interaction.response.send_modal(
|
||||
DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id)))
|
||||
)
|
||||
|
||||
@delete.subcommand(description="Delete a group. Deletion is permanent.")
|
||||
async def group(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", description="Group to delete.", required=True),
|
||||
) -> None:
|
||||
"""
|
||||
Calls the delete confirmation view for the given group.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction of command.
|
||||
group_id (str): Group to delete from. Is SlashOption.
|
||||
"""
|
||||
await interaction.response.send_modal(DeleteConfirmView(custom_id=create_delete_id(int(group_id))))
|
||||
|
||||
@delete.subcommand()
|
||||
async def user(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Subcommand parent for deleting user information. Never called."""
|
||||
|
||||
@user.subcommand(description="Deletes a user's scores from a category.")
|
||||
async def from_category(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
|
||||
category_id: str = nextcord.SlashOption(
|
||||
name="category", description="Category to delete user from.", required=True
|
||||
),
|
||||
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
|
||||
) -> None:
|
||||
"""
|
||||
Removes a user from a category. Uses autofill functions to fill out group_id, category_id, and user_name.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): String representation of Group id to remove user from. Is SlashOption.
|
||||
category_id (str): String representation of Category id to remove user from. Is SlashOption.
|
||||
user_name (str): The user to remove. Is SlashOption.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
|
||||
group = session.scalar(stmt)
|
||||
|
||||
stmt = select(col(Category.name)).where(col(Category.id) == int(category_id))
|
||||
category_name = session.scalar(stmt)
|
||||
|
||||
stmt = (
|
||||
select(Score).where(col(Score.category_id) == int(category_id)).where(col(Score.username) == user_name)
|
||||
)
|
||||
|
||||
# there should only be one because category id and username are a composite primary key in score
|
||||
session.delete(session.scalar(stmt))
|
||||
session.commit()
|
||||
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
await interaction.response.send_message(
|
||||
f'User "{user_name}" removed from "{category_name}".', ephemeral=True
|
||||
)
|
||||
|
||||
@user.subcommand(description="Deletes a user's scores from entire group.")
|
||||
async def from_group(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
|
||||
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
|
||||
) -> None:
|
||||
"""
|
||||
Removes user from a group. Uses autofill functions to fill out group_id and user_name.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): String representation of Group id to remove user from. Is SlashOption.
|
||||
user_name (str): The user to remove. Is SlashOption.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
|
||||
group = session.scalar(stmt)
|
||||
category_ids = select(col(Category.id)).where(col(Category.group_id) == int(group_id))
|
||||
stmt = select(Score).where(col(Score.category_id).in_(category_ids)).where(col(Score.username) == user_name)
|
||||
|
||||
for score in session.scalars(stmt):
|
||||
session.delete(score)
|
||||
session.commit()
|
||||
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
await interaction.response.send_message(
|
||||
f'User "{user_name}" removed from all categories in {group.name}.', ephemeral=True
|
||||
)
|
||||
|
||||
@from_category.on_autocomplete("group_id")
|
||||
@from_group.on_autocomplete("group_id")
|
||||
@category.on_autocomplete("group_id")
|
||||
@group.on_autocomplete("group_id")
|
||||
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
|
||||
"""
|
||||
Autocomplete function for group names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): String to be autocompleted with Group name.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
|
||||
|
||||
@from_category.on_autocomplete("category_id")
|
||||
@category.on_autocomplete("category_id")
|
||||
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
|
||||
"""
|
||||
Autocomplete function for category names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
category_id (str): String to be autocompleted with Category name.
|
||||
group_id (str): Group to find Category from.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(
|
||||
autofill_categories(interaction.guild_id, int(group_id), category_id)
|
||||
)
|
||||
|
||||
@from_category.on_autocomplete("user_name")
|
||||
@from_group.on_autocomplete("user_name")
|
||||
async def user_autocomplete(
|
||||
self, interaction: nextcord.Interaction, user_name: str, group_id: str, category_id: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Autocomplete functions for user names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
user_name (str): String to be autocompleted with User name.
|
||||
group_id (str): String representation of Group id to find category from.
|
||||
category_id (Optional[str]): String representation of Category id to find User from, can be None.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(
|
||||
autofill_users_in_group_category(
|
||||
interaction.guild_id, int(group_id), int(category_id) if category_id else 0, user_name
|
||||
)
|
||||
)
|
||||
|
||||
@users.on_autocomplete("user_name")
|
||||
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str) -> None:
|
||||
"""
|
||||
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction with command.
|
||||
username (str): String to run autocompletion against.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
|
||||
@@ -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))
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Commands for helping a user."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
|
||||
class HelpCog(commands.Cog, name="Help commands."):
|
||||
"""Class holds commands for getting help."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Default constructor for HelpCog class. Does nothing."""
|
||||
|
||||
@nextcord.slash_command()
|
||||
async def help(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Parent slash command for help. Never called."""
|
||||
|
||||
@help.subcommand(description="Describes how to get started with Agon.")
|
||||
async def setup(self, interaction: nextcord.Interaction) -> None:
|
||||
"""
|
||||
Sends a message describing how to get started with Agon.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
"""
|
||||
# I hate this but don't want indents in the string.
|
||||
|
||||
help_str: str = "Welcome to Agon! Three simple steps are needed to get started."
|
||||
help_str += '1.) Set a channel to receive submission verifications with "/set verify_channel."\n'
|
||||
help_str += "This channel is used to allow moderators to check if a submission is real/valid.\n\n"
|
||||
help_str += '2.) Set up your highscores! Use "/create group" to first create a group message in the'
|
||||
help_str += 'channel you would like and then use "/create category" to create a category in that group message!'
|
||||
help_str += "\n\n"
|
||||
help_str += '3.) Users can then submit scores with the "/submit" command.'
|
||||
|
||||
await interaction.response.send_message(content=help_str)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Commands for refreshing highscore messages."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import CategoryGroup
|
||||
from agon.utils import autofill_groups, send_highscore_message
|
||||
|
||||
|
||||
class RefreshCog(commands.Cog, name="Refresh Cog"):
|
||||
"""Class holds commands for refreshing."""
|
||||
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
"""
|
||||
Default constructor for RefreshCog class.
|
||||
|
||||
Args:
|
||||
bot (commands.Bot): Bot object. Required to register submission button view.
|
||||
"""
|
||||
self._bot = bot
|
||||
|
||||
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
|
||||
async def refresh(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Parent slash command for refreshing things. Never called."""
|
||||
|
||||
@refresh.subcommand(description="Manually refresh a group. Use when changing settings or data manually changed.")
|
||||
async def group(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", description="Name of Group.", required=True),
|
||||
) -> None:
|
||||
"""
|
||||
Refreshes a group given by user.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): Str representation of Group id to refresh. Is SlashOption.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(CategoryGroup)
|
||||
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
|
||||
.where(col(CategoryGroup.id) == int(group_id))
|
||||
)
|
||||
group = session.scalar(stmt)
|
||||
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
await interaction.response.send_message(f"Updated group {group.name}!", ephemeral=True)
|
||||
|
||||
@refresh.subcommand()
|
||||
async def all(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Subcommand parent for refreshing all things. Never called."""
|
||||
|
||||
@all.subcommand(description="Triggers post/update of highscores to registered highscore channels.")
|
||||
async def groups(self, interaction: nextcord.Interaction) -> None:
|
||||
"""
|
||||
A command which posts the highscore in the registered channel.
|
||||
|
||||
Sends warning if no channel has been registered. Either creates new messages for each group if one doesn't
|
||||
exist or edits the existing message.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.guild_id) == interaction.guild_id)
|
||||
|
||||
# awaiting response because posting scores takes a while
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
for group in session.scalars(stmt):
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
|
||||
await interaction.send(f"Posted all groups in {interaction.guild.name}'s highscores.", ephemeral=True)
|
||||
|
||||
@group.on_autocomplete("group_id")
|
||||
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
|
||||
"""
|
||||
Collects list of group names and compares to input group name for autocomplete assistance.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction with command.
|
||||
group_id (str): String to run autocomplete compared to.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
|
||||
@@ -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))
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Contains command for submitting a score."""
|
||||
|
||||
import re
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.cogs.submission.verification_view import VerificationView
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup, Guild, Submission
|
||||
from agon.logger import agon_logger
|
||||
from agon.utils import autofill_categories, autofill_groups, submit_score
|
||||
|
||||
|
||||
class SubmissionCog(commands.Cog):
|
||||
"""Class holds command for submitting a score."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
return
|
||||
|
||||
# TODO(imcompiling): fix too many return statements when reworking command.
|
||||
# https://gitea.cheezers.org/imcompiling/agon/issues/76
|
||||
@nextcord.slash_command()
|
||||
async def submit( # noqa: PLR0911, PLR0913
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(name="group", required=True, description="Group to submit score to."),
|
||||
category_id: str = nextcord.SlashOption(
|
||||
name="category", required=True, description="Category to submit score to."
|
||||
),
|
||||
score: str = nextcord.SlashOption(required=True, description="Score to submit"),
|
||||
proof: nextcord.Attachment = nextcord.SlashOption(
|
||||
required=False, description="Proof for submission. Only needed if score requires verification."
|
||||
),
|
||||
username: str = nextcord.SlashOption(
|
||||
required=False, description="Modified username if submitting under not your discord name."
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Submit a highscore.
|
||||
|
||||
Sets the username if one isn't passed in. Checks if a submission needs verified. Ensures that the score is
|
||||
following the correct format. Then either creates the verification request or submits score if verification
|
||||
isn't required.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction of the command.
|
||||
group_id (str): Is SlashOption. Group that the score category belongs in.
|
||||
category_id (str): Is SlashOption. Category to submit the score in.
|
||||
score (str): Is SlashOption. The score.
|
||||
proof (nextcord.Attachment): Is SlashOption. Picture to provide proof of score.
|
||||
username (str, optional): Is SlashOption. Username to associate with the score.
|
||||
"""
|
||||
# fill out username if not passed in
|
||||
if username is None:
|
||||
username = interaction.user.display_name
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(Category)
|
||||
.where(col(Category.group_id) == int(group_id))
|
||||
.where(col(Category.id) == int(category_id))
|
||||
)
|
||||
if (category := session.scalar(stmt)) is None:
|
||||
await interaction.response.send_message("Category not found.", ephemeral=True)
|
||||
return
|
||||
|
||||
# check if verification is required. if no picture, deny
|
||||
if category.is_validated and proof is None:
|
||||
await interaction.response.send_message(
|
||||
"This category required verification. Please attach a picture and re-submit.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# check with regex if score is in correct format
|
||||
# check if verify_score was successful. It replies with corrections required itself.
|
||||
if (
|
||||
verified_score := await self.verify_score(interaction, score, is_time_record=category.is_time_record)
|
||||
) is None:
|
||||
# Verify score handles responding. Might change when submit is split up.
|
||||
return
|
||||
|
||||
# Create submission verification request or submit score if verification not needed.
|
||||
if not category.is_validated:
|
||||
await submit_score(interaction, int(group_id), int(category_id), username, verified_score)
|
||||
await interaction.response.send_message("Score has been submitted!", ephemeral=True)
|
||||
return
|
||||
|
||||
stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
|
||||
|
||||
if (verification_channel_id := session.scalar(stmt)) is None:
|
||||
error_response = "No verification channel was registered. Contact server admins."
|
||||
await interaction.response.send_message(error_response, ephemeral=True)
|
||||
return
|
||||
|
||||
if (channel := interaction.guild.get_channel(verification_channel_id)) is None:
|
||||
error_response = "Registered Verification channel does not exist. Contact server admins."
|
||||
await interaction.response.send_message(error_response, ephemeral=True)
|
||||
return
|
||||
|
||||
if not isinstance(channel, nextcord.TextChannel | nextcord.Thread):
|
||||
await interaction.response.send_message("An error happened with submission.")
|
||||
agon_logger.error(
|
||||
"Channel id %s in guild %s is does not correspond to a TextChannel.", channel.id, channel.guild.id
|
||||
)
|
||||
return
|
||||
|
||||
# Send the verification message to admins
|
||||
verification_message = await channel.send(
|
||||
file=await proof.to_file(),
|
||||
view=VerificationView(),
|
||||
embeds=[self.create_embed(interaction, group_id, category_id, verified_score, username, session)],
|
||||
content=f"Submission from <@{interaction.user.id}>.",
|
||||
)
|
||||
await interaction.response.send_message(content="Score submitted!", ephemeral=True)
|
||||
|
||||
# Save the submission data for later use.
|
||||
submission: Submission = Submission(
|
||||
category_id=int(category_id),
|
||||
group_id=int(group_id),
|
||||
guild_id=interaction.guild_id,
|
||||
message_id=verification_message.id,
|
||||
score=verified_score,
|
||||
user_id=interaction.user.id,
|
||||
username=username,
|
||||
)
|
||||
|
||||
session.add(submission)
|
||||
session.commit()
|
||||
|
||||
def create_embed( # noqa: PLR0913
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str,
|
||||
category_id: str,
|
||||
score: str,
|
||||
username: str,
|
||||
session: Session,
|
||||
) -> nextcord.Embed:
|
||||
"""
|
||||
Creates an embed containing submission data for human verification.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Discord interaction of the command.
|
||||
group_id (str): Used to get group name.
|
||||
category_id (str): Used to get category name.
|
||||
score (str): Score to set.
|
||||
username (str): Username to set.
|
||||
session (Session): Open connection to database.
|
||||
|
||||
Returns:
|
||||
nextcord.Embed: Embed containing human readable submission data.
|
||||
"""
|
||||
embed = nextcord.Embed(title="Highscores Submission", description="Review fields and accept.")
|
||||
embed.add_field(name="User Name", value=username, inline=False)
|
||||
|
||||
# Get group name from database
|
||||
stmt = (
|
||||
select(col(CategoryGroup.name))
|
||||
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
|
||||
.where(col(CategoryGroup.id) == group_id)
|
||||
)
|
||||
embed.add_field(name="Group", value=session.scalar(stmt), inline=False)
|
||||
|
||||
# Get category name from database
|
||||
stmt = (
|
||||
select(col(Category.name)).where(col(Category.group_id) == group_id).where(col(Category.id) == category_id)
|
||||
)
|
||||
embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
|
||||
embed.add_field(name="Score", value=score, inline=False)
|
||||
return embed
|
||||
|
||||
async def verify_score(self, interaction: nextcord.Interaction, score: str, *, is_time_record: bool) -> str | None:
|
||||
"""
|
||||
Uses regex to verify that scores are in the correct format.
|
||||
|
||||
Handles responding to the interaction if the format is incorrect. Also standardizes the time record.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Discord interaction of the command.
|
||||
score (str): String representation of the score being verified.
|
||||
is_time_record (bool): Whether the score needs to be verified/formatted as a time record.
|
||||
|
||||
Returns:
|
||||
str | None: String of corrected score. None if formatted incorrectly.
|
||||
"""
|
||||
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", score)
|
||||
int_regex = re.search("^[0-9]+$", score)
|
||||
if time_regex is None and is_time_record:
|
||||
await interaction.response.send_message(
|
||||
"Time record must follow format MM:SS.ms, SS.ms, or SS.", ephemeral=True
|
||||
)
|
||||
return None
|
||||
if int_regex is None and not is_time_record:
|
||||
await interaction.response.send_message("Number record must only be number.", ephemeral=True)
|
||||
return None
|
||||
if is_time_record:
|
||||
# standardize time score format.
|
||||
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
|
||||
# score is already fine.
|
||||
pass
|
||||
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
|
||||
# need to add leading colon and 0
|
||||
score = "0:" + score
|
||||
elif re.search("^[0-9](\\.[0-9])?$", score):
|
||||
# need to add 0 in seconds and leading colon and 0
|
||||
score = "0:0" + score
|
||||
elif re.search("^[0][0-9]:[0-9]{2}(\\.[0-9])?$", score):
|
||||
# if there is a minute 10ths place 0, remove
|
||||
score = score[1:]
|
||||
|
||||
return score
|
||||
|
||||
@submit.on_autocomplete("group_id")
|
||||
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
|
||||
"""
|
||||
Autocomplete function for group names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): String to be autocompleted with Group name.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
|
||||
|
||||
@submit.on_autocomplete("category_id")
|
||||
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
|
||||
"""
|
||||
Autocomplete function for category names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
category_id (str): String to be autocompleted with Category name.
|
||||
group_id (str): Group to find Category from.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(
|
||||
autofill_categories(interaction.guild_id, int(group_id), category_id)
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Commands for updating information/variables held in database."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Guild
|
||||
from agon.utils import autofill_users_in_guild
|
||||
|
||||
from .views.update_username_verify import UpdateUsernameVerifyView
|
||||
|
||||
|
||||
class UpdateCog(commands.Cog, name="Update commands."):
|
||||
"""Class holds commands and views needed for updating things in bot."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
"""
|
||||
Default constructor for Update class. Sets internal bot variable.
|
||||
|
||||
Args:
|
||||
bot (commands.Bot): Bot object. Required to register update username button view.
|
||||
"""
|
||||
self._bot: commands.Bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self) -> None:
|
||||
"""Called when cog is loaded. Adds views required for DataManagement commands."""
|
||||
self._bot.add_view(UpdateUsernameVerifyView())
|
||||
|
||||
@nextcord.slash_command()
|
||||
async def update(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Parent slash command for updating things. Never called."""
|
||||
|
||||
@update.subcommand(description="Request a username update. Admins must approve.")
|
||||
async def username(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
old_username: str = nextcord.SlashOption(description="Username currently stored in highscores.", required=True),
|
||||
new_username: str = nextcord.SlashOption(description="Username to update to.", required=True),
|
||||
) -> None:
|
||||
"""
|
||||
Creates a request to send to admins for changing a username.
|
||||
|
||||
Uses UpdateUsernameVerifyView to do the work.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command
|
||||
old_username (str): Username to replace.
|
||||
new_username (str): Username to replace old with.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
|
||||
verify_channel = interaction.guild.get_channel(session.scalar(stmt))
|
||||
|
||||
if not isinstance(verify_channel, nextcord.TextChannel):
|
||||
await interaction.send("Verification channel not set. Cannot process request.", ephemeral=True)
|
||||
return
|
||||
|
||||
embed: nextcord.Embed = nextcord.Embed(title="Username change request.")
|
||||
embed.add_field(name="Old Username", value=old_username)
|
||||
embed.add_field(name="New Username", value=new_username)
|
||||
await verify_channel.send(
|
||||
content=f"Username change request from <@{interaction.user.id}>.",
|
||||
embed=embed,
|
||||
view=UpdateUsernameVerifyView(),
|
||||
)
|
||||
await interaction.send("Request sent to admins for verification.", ephemeral=True)
|
||||
|
||||
@username.on_autocomplete("old_username")
|
||||
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str) -> None:
|
||||
"""
|
||||
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction with command.
|
||||
username (str): String to run autocompletion against.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
|
||||
@@ -0,0 +1,112 @@
|
||||
"""View command hierarchy."""
|
||||
|
||||
import nextcord
|
||||
import pandas as pd
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup, Score
|
||||
from agon.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string
|
||||
|
||||
|
||||
class ViewCog(commands.Cog, name="View Command"):
|
||||
"""Class holds commands for viewing data."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Default constructor for ViewCog class. Does nothing."""
|
||||
|
||||
@nextcord.slash_command()
|
||||
async def view(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Parent slash command for view. Never called."""
|
||||
|
||||
@view.subcommand(description="Lists all scores for a given category.")
|
||||
async def leaderboard(
|
||||
self,
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: str = nextcord.SlashOption(
|
||||
name="group", description="Group to show leaderboard from.", required=True
|
||||
),
|
||||
category_id: str = nextcord.SlashOption(
|
||||
name="category", description="Category to show leaderboard.", required=True
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Create message with leaderboard of a group and category.
|
||||
|
||||
Gathers all users that have a submission in the group/category combo and returns a
|
||||
message with them in ranked order.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): String representation of Group id to search from. Is SlashOption.
|
||||
category_id (str): String representation of Category id to show leaderboard. Is SlashOption.
|
||||
"""
|
||||
str_list: list[str] = []
|
||||
|
||||
# get group, category, and all scores sorted correctly.
|
||||
with Session(engine) as session:
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
|
||||
group = session.scalar(stmt)
|
||||
|
||||
stmt = select(Category).where(col(Category.id) == int(category_id))
|
||||
category = session.scalar(stmt)
|
||||
|
||||
d_f = pd.read_sql_query(
|
||||
sql=session.query(Score).where(col(Score.category_id) == category_id).statement, con=engine
|
||||
)
|
||||
|
||||
scores = d_f.sort_values("Score", ascending=category.is_ascending).reset_index()
|
||||
|
||||
# get the longest string
|
||||
longest_str: int = scores.UserName.map(len).max()
|
||||
|
||||
for i, row in scores.iterrows():
|
||||
str_list.append(
|
||||
format_score_string(
|
||||
row["UserName"],
|
||||
row["Score"],
|
||||
longest_str,
|
||||
category.is_time_record,
|
||||
i + 1, # type: ignore[reportOperatorIssue]
|
||||
MAX_LINE_LENGTH,
|
||||
)
|
||||
)
|
||||
|
||||
message_str: str = f"**{group.name} {category.name} Leaderboard!**\n```"
|
||||
if len(str_list) == 0:
|
||||
message_str += "No Submissions!"
|
||||
else:
|
||||
for line in str_list:
|
||||
message_str += f"{line}\n"
|
||||
|
||||
message_str += "```"
|
||||
|
||||
await interaction.response.send_message(content=message_str)
|
||||
|
||||
@leaderboard.on_autocomplete("group_id")
|
||||
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
|
||||
"""
|
||||
Autocomplete function for group names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
group_id (str): String to be autocompleted with Group name.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
|
||||
|
||||
@leaderboard.on_autocomplete("category_id")
|
||||
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
|
||||
"""
|
||||
Autocomplete function for category names.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that invoked the command.
|
||||
category_id (str): String to be autocompleted with Category name.
|
||||
group_id (str): Group to find Category from.
|
||||
"""
|
||||
await interaction.response.send_autocomplete(
|
||||
autofill_categories(interaction.guild_id, int(group_id), category_id)
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Package for views used in commands."""
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Contains view and helper functions for confirming and processing deletes."""
|
||||
|
||||
import nextcord
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup
|
||||
from agon.utils import send_highscore_message
|
||||
|
||||
|
||||
class DeleteConfirmView(nextcord.ui.Modal):
|
||||
"""View modal popup for confirming deletions."""
|
||||
|
||||
def __init__(self, custom_id: str):
|
||||
super().__init__(title="Warning, deletion is permanent.", custom_id=custom_id)
|
||||
|
||||
self.checker = nextcord.ui.TextInput(
|
||||
label='Type "delete" to confirm deletion.',
|
||||
required=True,
|
||||
placeholder="delete",
|
||||
style=nextcord.TextInputStyle.short,
|
||||
)
|
||||
self.add_item(self.checker)
|
||||
|
||||
async def callback(self, interaction: nextcord.Interaction) -> None:
|
||||
"""Action for confirming deletion.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): User interaction object with the modal.
|
||||
"""
|
||||
if self.checker.value.lower() != "delete":
|
||||
await interaction.response.send_message('Response was not "delete". Canceling delete.')
|
||||
return
|
||||
|
||||
del_vals = parse_delete_id(self.custom_id)
|
||||
|
||||
with Session(engine) as session:
|
||||
stmt = select(CategoryGroup).where(CategoryGroup.id == del_vals["group_id"])
|
||||
group = session.scalar(stmt)
|
||||
if del_vals["category_id"] is None:
|
||||
# delete entire group
|
||||
session.delete(group)
|
||||
# delete the message related to group
|
||||
channel = interaction.guild.get_channel(group.channel_id)
|
||||
if not isinstance(channel, nextcord.TextChannel | nextcord.Thread):
|
||||
return
|
||||
message: nextcord.Message = await channel.fetch_message(group.message_id)
|
||||
await message.delete()
|
||||
|
||||
else:
|
||||
stmt = (
|
||||
delete(Category)
|
||||
.where(Category.group_id == del_vals["group_id"])
|
||||
.where(Category.id == del_vals["category_id"])
|
||||
)
|
||||
session.execute(stmt)
|
||||
# update the message
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
session.commit()
|
||||
|
||||
await interaction.response.send_message("Deletion complete.", ephemeral=True)
|
||||
|
||||
|
||||
def create_delete_id(group_id: int, category_id: int | None = None) -> str:
|
||||
"""
|
||||
Creates a delete id string to tell the view what to delete.
|
||||
|
||||
Args:
|
||||
group_id (int): group id to delete
|
||||
category_id (Optional[int]): category id to delete. Can be None.
|
||||
|
||||
Returns:
|
||||
str: string representation of the delete id.
|
||||
"""
|
||||
return f"delete-{group_id}-{category_id}"
|
||||
|
||||
|
||||
def parse_delete_id(delete_id: str) -> dict:
|
||||
"""
|
||||
Returns a dict containing the values of a delete id.
|
||||
|
||||
Args:
|
||||
delete_id (str): the delete id
|
||||
|
||||
Returns:
|
||||
dict: dict containing values from a delete id.
|
||||
"""
|
||||
vals = {}
|
||||
group_id = delete_id.split("-")[1]
|
||||
category_id = delete_id.split("-")[2]
|
||||
vals["group_id"] = group_id
|
||||
if category_id == "None":
|
||||
vals["category_id"] = None
|
||||
else:
|
||||
vals["category_id"] = category_id
|
||||
|
||||
return vals
|
||||
@@ -0,0 +1,71 @@
|
||||
"""View for allowing a username update."""
|
||||
|
||||
import nextcord
|
||||
from sqlalchemy import distinct, exc, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup, Score
|
||||
from agon.utils import send_highscore_message
|
||||
|
||||
|
||||
class UpdateUsernameVerifyView(nextcord.ui.View):
|
||||
"""Defines view for allowing a username update."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Default constructor for UpdateUsernameVerifyView. Calls super constructor with no timeout."""
|
||||
super().__init__(timeout=None)
|
||||
|
||||
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-username-update")
|
||||
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
|
||||
"""
|
||||
Button for denying username update. Deletes message.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): unused
|
||||
interaction (nextcord.Interaction): Interaction with the button
|
||||
"""
|
||||
await interaction.message.delete()
|
||||
|
||||
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-username-update")
|
||||
async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
|
||||
"""
|
||||
Button for approving username update.
|
||||
|
||||
Tells guild data to update username, then updates message to show it is complete.
|
||||
|
||||
Args:
|
||||
button (nextcord.ui.Button): unused
|
||||
interaction (nextcord.Interaction): Interaction with the button
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
try:
|
||||
embed_content = interaction.message.embeds[0].to_dict()
|
||||
old_username: str = embed_content["fields"][0]["value"] # type: ignore reportTypedDictNotRequiredAccess
|
||||
new_username: str = embed_content["fields"][1]["value"] # type: ignore reportTypedDictNotRequiredAccess
|
||||
|
||||
stmt = select(distinct(col(Score.category_id))).where(col(Score.username) == old_username)
|
||||
category_ids: list[int] = list(session.scalars(stmt))
|
||||
|
||||
stmt = select(Score).where(col(Score.username) == old_username)
|
||||
for score in session.scalars(stmt):
|
||||
score.UserName = new_username
|
||||
session.commit()
|
||||
|
||||
group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
|
||||
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id).in_(group_ids))
|
||||
|
||||
for group in session.scalars(stmt):
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
|
||||
content = interaction.message.content
|
||||
content += f"\nApproved by <@{interaction.user.id}>."
|
||||
await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None)
|
||||
except exc.DataError:
|
||||
session.rollback()
|
||||
content = interaction.message.content
|
||||
content += "\nError, UserName is too long."
|
||||
await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None)
|
||||
await interaction.response.send_message(content="User must resubmit with shorter name.", ephemeral=True)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Defines cog for guild listeners. Contains functions for various guild events."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Guild
|
||||
|
||||
|
||||
class GuildListener(commands.Cog, name="Guild Listener"):
|
||||
"""Sets up message listener."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_join(self, guild: nextcord.Guild) -> None:
|
||||
"""
|
||||
Listener for Guild Join. Adds guild to database.
|
||||
|
||||
Args:
|
||||
guild (nextcord.Guild): Guild that was joined.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(Guild).where(col(Guild.id == guild.id))
|
||||
|
||||
# just in case check if the guild already exists
|
||||
if (guild_data := session.scalar(stmt)) is None:
|
||||
new_guild = Guild(id=guild.id, name=guild.name)
|
||||
session.add(new_guild)
|
||||
else:
|
||||
guild_data.GuildName = guild.name
|
||||
guild_data.Active = True
|
||||
|
||||
session.commit()
|
||||
|
||||
# let imcompiling#2272 know about a new server joining!
|
||||
# staying with fetch due to intents not being set up correctly. Maybe change to get eventually.
|
||||
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
|
||||
await imcompiling.send(f'Agon has joined server "{guild.name}"!')
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_remove(self, guild: nextcord.Guild) -> None:
|
||||
"""
|
||||
Listens to when the bot has been removed from a guild. Update's the database to record the active flag as false.
|
||||
|
||||
Args:
|
||||
guild (nextcord.Guild): Guild that was left.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(Guild).where(col(Guild.id == guild.id))
|
||||
guild_data = session.scalar(stmt)
|
||||
guild_data.Active = False
|
||||
session.commit()
|
||||
|
||||
# let imcompiling#2272 know about leaving a server.
|
||||
# staying with fetch due to intents not being set up correctly. Maybe change to get eventually.
|
||||
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
|
||||
await imcompiling.send(f'Agon has been removed from "{guild.name}".')
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot) -> None:
|
||||
"""Adds function to bot."""
|
||||
bot.add_cog(GuildListener(bot))
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Initializing code for all loops."""
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from .bot_presence import BotPresence
|
||||
from .guild_data import GuildData
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot) -> None:
|
||||
"""Adds function to bot."""
|
||||
bot.add_cog(BotPresence(bot))
|
||||
bot.add_cog(GuildData(bot))
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Functions for controlling the bot's presence. Also contains the loop."""
|
||||
|
||||
import random
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands, tasks
|
||||
|
||||
|
||||
class BotPresence(commands.Cog, name="Bot Presence"):
|
||||
"""Class for bot presence functions."""
|
||||
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_join(self, guild: nextcord.Guild) -> None: # noqa: ARG002
|
||||
"""Listener for Guild Join. Updated presence on guild join."""
|
||||
await self.change_presence()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self) -> None:
|
||||
"""Starts the loop when bot is ready."""
|
||||
await self.bot.wait_until_ready()
|
||||
self.periodic_change_presence.start()
|
||||
|
||||
@tasks.loop(minutes=2)
|
||||
async def periodic_change_presence(self) -> None:
|
||||
"""Loops every 2 minutes and changes presence."""
|
||||
await self.change_presence()
|
||||
|
||||
async def change_presence(self) -> None:
|
||||
"""Randomly chooses a presence and updates bot's presence."""
|
||||
statuses = [
|
||||
"Now with slash commands!",
|
||||
"Vuln Pls.",
|
||||
"All your base are belong to us.",
|
||||
"Double plus good.",
|
||||
"That's a lot of damage!",
|
||||
]
|
||||
|
||||
status = random.choice(statuses) # noqa: S311
|
||||
|
||||
status += f" | {len(self.bot.guilds)} servers"
|
||||
|
||||
await self.bot.change_presence(activity=nextcord.Game(status))
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Contains a loop that updates guild data in the database periodically."""
|
||||
|
||||
from nextcord.ext import commands, tasks
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Guild
|
||||
|
||||
|
||||
class GuildData(commands.Cog, name="Guild Data"):
|
||||
"""Class to contain loop."""
|
||||
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self) -> None:
|
||||
"""Starts the loop when bot is ready."""
|
||||
await self.bot.wait_until_ready()
|
||||
await self.periodic_update_guild_data.start()
|
||||
|
||||
@tasks.loop(hours=24)
|
||||
async def periodic_update_guild_data(self) -> None:
|
||||
"""Loops every day, calls update_guild_data."""
|
||||
self.update_guild_data()
|
||||
|
||||
def update_guild_data(self) -> None:
|
||||
"""Updates all known guild's data in the database."""
|
||||
with Session(engine) as session:
|
||||
for guild in self.bot.guilds:
|
||||
# update given guild name for guild id
|
||||
stmt = select(Guild).where(col(Guild.id) == guild.id)
|
||||
guild_data = session.scalar(stmt)
|
||||
|
||||
# just in case checking if the guild doesn't exist.
|
||||
if guild_data is None:
|
||||
new_guild = Guild(id=guild.id, name=guild.name)
|
||||
session.add(new_guild)
|
||||
else:
|
||||
guild_data.name = guild.name
|
||||
|
||||
session.commit()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Cog for creating submission process."""
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from .submission_views import SubmissionCreateButton
|
||||
from .verification_view import VerificationView
|
||||
|
||||
|
||||
class SubmissionCog(commands.Cog, name="Submission Cog"):
|
||||
"""Constructs submission method."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
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) -> None:
|
||||
"""Adds function to bot."""
|
||||
bot.add_cog(SubmissionCog(bot))
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Views used for submission process."""
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
|
||||
class SubmissionCreateButton(nextcord.ui.View):
|
||||
"""Defines view for score submission button."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
super().__init__(timeout=None)
|
||||
self._bot = bot
|
||||
|
||||
@nextcord.ui.button(label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate")
|
||||
async def submitcreate_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
|
||||
"""
|
||||
Button for score submission.
|
||||
|
||||
Is deprecated, sends response telling user to use the new command instead.
|
||||
"""
|
||||
await interaction.response.send_message(
|
||||
"This feature has been deprecated. Please use the '/submit' slash command instead.", ephemeral=True
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""View for verification buttons."""
|
||||
|
||||
import nextcord
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Submission
|
||||
from agon.utils import submit_score
|
||||
|
||||
|
||||
class VerificationView(nextcord.ui.View):
|
||||
"""Defines views for verifying score submissions."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(timeout=None)
|
||||
|
||||
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission")
|
||||
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
|
||||
"""Button for denying score submission."""
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
delete(Submission)
|
||||
.where(col(Submission.guild_id) == interaction.guild_id)
|
||||
.where(col(Submission.message_id) == interaction.message.id)
|
||||
)
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
await interaction.message.delete()
|
||||
|
||||
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission")
|
||||
async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
|
||||
"""Button for approving score submission."""
|
||||
with Session(engine) as session:
|
||||
stmt = (
|
||||
select(Submission)
|
||||
.where(col(Submission.guild_id) == interaction.guild_id)
|
||||
.where(col(Submission.message_id) == interaction.message.id)
|
||||
)
|
||||
|
||||
if (submission := session.scalar(stmt)) is None:
|
||||
await interaction.response.send_message("Could not find submission data in database.", ephemeral=True)
|
||||
await interaction.message.delete()
|
||||
return
|
||||
|
||||
await submit_score(
|
||||
interaction, submission.group_id, submission.category_id, submission.username, submission.score
|
||||
)
|
||||
session.delete(submission)
|
||||
session.commit()
|
||||
content = interaction.message.content
|
||||
content += f"\nApproved by <@{interaction.user.id}>."
|
||||
await interaction.message.edit(content=content, view=None)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Loads environment variables for use in bot."""
|
||||
|
||||
import os
|
||||
|
||||
from dotenv.main import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Discord config
|
||||
BOT_PREFIX = "?"
|
||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
||||
SQL_DB = os.getenv("SQL_DB", "")
|
||||
SQL_HOST = os.getenv("SQL_HOST", "")
|
||||
SQL_PASS = os.getenv("SQL_PASS", "")
|
||||
SQL_USER = os.getenv("SQL_USER", "")
|
||||
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "prod")
|
||||
@@ -0,0 +1 @@
|
||||
"""Package for database interaction."""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Creates logger for Agon bot."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
agon_logger = logging.getLogger("default")
|
||||
agon_logger.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
|
||||
agon_logger.addHandler(handler)
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
"""Contains various utility functions for highscores bot."""
|
||||
|
||||
import time
|
||||
|
||||
import nextcord
|
||||
import pandas as pd
|
||||
from sqlalchemy import distinct, select
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
from sqlmodel import col
|
||||
|
||||
from agon.database.db import engine
|
||||
from agon.database.models import Category, CategoryGroup, Guild, Score
|
||||
from agon.logger import agon_logger
|
||||
|
||||
MAX_LINE_LENGTH = 142
|
||||
|
||||
|
||||
def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "dict[str, str]":
|
||||
"""
|
||||
Returns all category names that start with category_begin.
|
||||
|
||||
If category_begin is empty, returns list of all category names.
|
||||
|
||||
Args:
|
||||
guild_id (int): Guild id that holds the group and categories.
|
||||
group_id (int): Group id to search in.
|
||||
category_begin (str): The beginning of a category name to search with. Can be empty.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: List of all category names that start with category_begin.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
# search database for all category names
|
||||
subq = (
|
||||
select(CategoryGroup)
|
||||
.where(col(CategoryGroup.guild_id) == guild_id)
|
||||
.where(col(CategoryGroup.id) == group_id)
|
||||
.subquery()
|
||||
)
|
||||
cat_group_alias = aliased(CategoryGroup, subq)
|
||||
stmt = select(Category).where(col(Category.group_id) == cat_group_alias.id)
|
||||
categories_dict: dict[str, str] = {cat.name: str(cat.id) for cat in session.scalars(stmt)}
|
||||
|
||||
# if string empty, return all category names
|
||||
if not category_begin:
|
||||
return dict(list(categories_dict.items())[:25])
|
||||
|
||||
# else find all categories that start with category_begin
|
||||
categories: dict[str, str] = {
|
||||
cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower()
|
||||
}
|
||||
return dict(list(categories.items())[:25])
|
||||
|
||||
|
||||
def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
|
||||
"""
|
||||
Returns all group names that start with group_begin. If group_begin is empty, returns list of all group names.
|
||||
|
||||
Args:
|
||||
guild_id (int): Guild id that holds the groups.
|
||||
group_begin (str): The beginning of a group name to search with. Can be empty.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Dict of group names and ids where group name start with group_begin. Key is name, value is id.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
# Select all groups in guild.
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.guild_id) == guild_id)
|
||||
|
||||
# dict has to have key values of same type for discord.
|
||||
group_dict: dict[str, str] = {group.name: str(group.id) for group in session.scalars(stmt)}
|
||||
|
||||
# if string empty, return all group names
|
||||
if not group_begin:
|
||||
return dict(list(group_dict.items())[:25])
|
||||
|
||||
# else find all groups that start with group_begin
|
||||
groups: dict[str, str] = {
|
||||
group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower()
|
||||
}
|
||||
|
||||
return dict(list(groups.items())[:25])
|
||||
|
||||
|
||||
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]":
|
||||
"""
|
||||
Returns list of users that match user_begin.
|
||||
|
||||
Either all users in group or just users in category if category is not None. Might be expensive due to union-ing all
|
||||
users in group.
|
||||
|
||||
Args:
|
||||
guild_id (int): Guild id corresponding to group and category.
|
||||
group_id (str): group to look in for category.
|
||||
category_id (str): Category to look in for users. Can be None if not looking for category.
|
||||
user_begin (str): Beginning of user name to search with. Can be empty.
|
||||
|
||||
Returns:
|
||||
list[str]: List of users that start with user_begin.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
# get all categories in the group
|
||||
subq = (
|
||||
select(CategoryGroup)
|
||||
.where(col(CategoryGroup.guild_id) == guild_id)
|
||||
.where(col(CategoryGroup.id) == group_id)
|
||||
.subquery()
|
||||
)
|
||||
category_group = aliased(CategoryGroup, subq)
|
||||
|
||||
# If no category id, get all users in group.
|
||||
if category_id == 0:
|
||||
subq = select(col(Category.id)).where(col(Category.group_id) == category_group.id)
|
||||
stmt = select(distinct(col(Score.username))).where(col(Score.category_id).in_(subq))
|
||||
|
||||
else:
|
||||
# get all users in category_id
|
||||
subq = (
|
||||
select(Category)
|
||||
.where(col(Category.group_id) == category_group.id)
|
||||
.where(col(Category.id) == category_id)
|
||||
.subquery()
|
||||
)
|
||||
category = aliased(Category, subq)
|
||||
stmt = select(distinct(col(Score.username))).where(col(Score.category_id) == category.id)
|
||||
|
||||
users_list: list[str] = list(session.scalars(stmt))
|
||||
|
||||
# if string empty, return all users
|
||||
if not user_begin:
|
||||
return users_list[:25]
|
||||
|
||||
# else find all users that start with user_begin
|
||||
users: list[str] = [user for user in users_list if user_begin.lower() in user.lower()]
|
||||
|
||||
return users[:25]
|
||||
|
||||
|
||||
def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
|
||||
"""
|
||||
Obtains and returns a list of all the users who have submitted to the guild's highscores.
|
||||
|
||||
Args:
|
||||
guild_id (int): Id of the guild.
|
||||
user_begin (str): Beginning of user name to search with. Can be empty.
|
||||
|
||||
Returns:
|
||||
list[str]: All users with submissions in the guild.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
group_ids = select(col(CategoryGroup.id)).where(col(CategoryGroup.guild_id) == guild_id)
|
||||
category_ids = select(col(Category.id)).where(col(Category.group_id).in_(group_ids))
|
||||
stmt = select(distinct(col(Score.username))).where(col(Score.category_id).in_(category_ids))
|
||||
users_list: list[str] = list(session.scalars(stmt))
|
||||
|
||||
# if string empty, return all users
|
||||
if not user_begin:
|
||||
return users_list[:25]
|
||||
|
||||
# else find all users that start with user_begin
|
||||
users: list[str] = [user for user in users_list if user_begin.lower() in user.lower()]
|
||||
|
||||
return users[:25]
|
||||
|
||||
|
||||
# TODO(imcompiling): #73 Address complexity warning.
|
||||
# 073
|
||||
def format_highscore_message(session: Session, group: CategoryGroup) -> str:
|
||||
"""
|
||||
Creates a formatted string for a category's highscore discord message.
|
||||
|
||||
This formatted string is in discord's code block to ensure monospace text.
|
||||
Loops through each category to build the message one place at a time.
|
||||
|
||||
Args:
|
||||
session (Session): the sqlalchemy session used for this function call.
|
||||
group (CategoryGroup): group object that contains the contents of the message.
|
||||
|
||||
Returns:
|
||||
str: Formatted string for discord message
|
||||
"""
|
||||
ret_string = f"**{group.name}**\n```"
|
||||
|
||||
stmt = select(Guild).where(col(Guild.id) == group.guild_id)
|
||||
|
||||
# Get all categories in the group
|
||||
subq = (
|
||||
select(CategoryGroup)
|
||||
.where(col(CategoryGroup.guild_id) == group.guild_id)
|
||||
.where(col(CategoryGroup.name) == group.name)
|
||||
.subquery()
|
||||
)
|
||||
cat_group_alias = aliased(CategoryGroup, subq)
|
||||
stmt = select(Category).where(col(Category.group_id) == cat_group_alias.id)
|
||||
|
||||
# set up a map for the categories
|
||||
# also grab the max category size
|
||||
categories: list[Category] = []
|
||||
category_ids = []
|
||||
max_cat_size: int = 0
|
||||
pad_length = group.pad_length
|
||||
for cat in session.scalars(stmt):
|
||||
max_cat_size = max(cat.size, max_cat_size)
|
||||
categories.append(cat)
|
||||
category_ids.append(cat.id)
|
||||
|
||||
# get all scores in category group, load into data frame.
|
||||
d_f = pd.read_sql_query(
|
||||
sql=session.query(Score).where(col(Score.category_id).in_(category_ids)).statement,
|
||||
con=engine,
|
||||
)
|
||||
|
||||
# this list is what will be printed in the discord message
|
||||
# first string will be category list, others will be score printouts
|
||||
str_list: list[str] = ["" for i in range(max_cat_size + 1)]
|
||||
groups_df = d_f.groupby("CategoryId")
|
||||
|
||||
for category in categories:
|
||||
# category name string
|
||||
str_list[0] += f"{category.name}".ljust(pad_length, " ")
|
||||
|
||||
# ensure there is data to print
|
||||
if category.id in groups_df.indices:
|
||||
group_df = groups_df.get_group(category.id)
|
||||
|
||||
# process data. Want each group sorted correctly and only top X results
|
||||
processed_group = (
|
||||
group_df.sort_values("Score", ascending=category.is_ascending).head(category.size).reset_index()
|
||||
)
|
||||
|
||||
# add each score to list
|
||||
for ind in range(max_cat_size): # need to subtract to deal with category name string
|
||||
if ind in processed_group.index:
|
||||
username = processed_group["UserName"][ind]
|
||||
score = processed_group["Score"][ind]
|
||||
|
||||
str_list[ind + 1] += format_score_string(
|
||||
username,
|
||||
score,
|
||||
group.username_length,
|
||||
category.is_time_record,
|
||||
ind + 1,
|
||||
pad_length,
|
||||
)
|
||||
# if not enough entries, pad out
|
||||
elif ind + 1 <= category.size:
|
||||
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
|
||||
else:
|
||||
str_list[ind + 1] += "".ljust(pad_length, " ")
|
||||
else:
|
||||
for ind in range(max_cat_size):
|
||||
if ind + 1 <= category.size:
|
||||
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
|
||||
else:
|
||||
str_list[ind + 1] += "".ljust(pad_length, " ")
|
||||
|
||||
# next step is to cut down on the length of each line if required
|
||||
# need to use index to allow for modifying the list
|
||||
for ind, line in enumerate(str_list):
|
||||
if len(line) > MAX_LINE_LENGTH:
|
||||
split_loc = int(MAX_LINE_LENGTH / pad_length) * pad_length
|
||||
new_line = line[split_loc:]
|
||||
str_list[ind] = line[:split_loc]
|
||||
str_list.append(new_line)
|
||||
|
||||
# this list can now be placed into the format message function in the bot
|
||||
for ind, string in enumerate(str_list):
|
||||
# rstrip to clear unneeded spaces
|
||||
# add newline here for each string
|
||||
ret_string += string.rstrip() + "\n"
|
||||
|
||||
# add extra newline at category split unless there is already a space.
|
||||
if ((ind + 1) % (max_cat_size + 1)) == 0 and not ret_string.endswith("\n\n"):
|
||||
ret_string += "\n"
|
||||
|
||||
return ret_string + "```"
|
||||
|
||||
|
||||
# TODO(imcompiling): rework into two functions to remove the boolean and reduce function arguments.
|
||||
# 084
|
||||
def format_score_string(
|
||||
username: str,
|
||||
score: float,
|
||||
username_length: int,
|
||||
is_time_record: bool,
|
||||
rank: int,
|
||||
pad_length: int,
|
||||
) -> str:
|
||||
"""
|
||||
Builds the string to show a user's score.
|
||||
|
||||
Args:
|
||||
username (str): username.
|
||||
score (int): score in int form.
|
||||
username_length (float): limit to how long username is allowed to be.
|
||||
is_time_record (bool): if the score represents an number or a time record.
|
||||
rank (int): the user's rank.
|
||||
pad_length (int): limit to how long the string can be.
|
||||
|
||||
Returns:
|
||||
str: string to show user's score.
|
||||
"""
|
||||
username = username[:username_length].ljust(username_length, " ")
|
||||
|
||||
# get score, pad to correct length
|
||||
if is_time_record:
|
||||
# MM:SS.ms is 7 characters long
|
||||
# if there are single digit minutes, add a space for formatting.
|
||||
formatted_score = get_time_str(score)
|
||||
if formatted_score.find(":") and len(formatted_score.split(":", maxsplit=1)[0]) < 2: # noqa: PLR2004
|
||||
formatted_score = " " + formatted_score
|
||||
else:
|
||||
formatted_score = str(int(score)).rjust(7, " ")
|
||||
|
||||
return f"{rank}: {username} - {formatted_score}".ljust(pad_length, " ")
|
||||
|
||||
|
||||
def get_time_str(time_in: float) -> str:
|
||||
"""
|
||||
Formats a float value into the desired string format for a timestamp.
|
||||
|
||||
Args:
|
||||
time_in (float): A time record in seconds
|
||||
|
||||
Returns:
|
||||
str: Describes the time record in string format.
|
||||
"""
|
||||
# TODO see if theres a better way to do this
|
||||
milliseconds = round((time_in - int(time_in)) * 10)
|
||||
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
|
||||
|
||||
|
||||
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> None:
|
||||
"""
|
||||
Function takes care of checking if a message exists for a group.
|
||||
|
||||
Creates and sends/edits the message.
|
||||
|
||||
Args:
|
||||
session (Session): The sqlalchemy session being used for this function call.
|
||||
guild (nextcord.Guild): the nextcord.Guild object requesting this message.
|
||||
group (CategoryGroup): group to post to the message.
|
||||
"""
|
||||
highscore_string = format_highscore_message(session, group)
|
||||
if (channel := guild.get_channel(group.channel_id)) is None:
|
||||
agon_logger.error("Channel id %s in guild %s does not exist.", group.channel_id, guild.id)
|
||||
return
|
||||
|
||||
if not isinstance(channel, nextcord.TextChannel | nextcord.Thread):
|
||||
agon_logger.error(
|
||||
"Channel id %s in guild %s is does not correspond to a TextChannel or Thread.", channel.id, channel.guild.id
|
||||
)
|
||||
return
|
||||
|
||||
if group.message_id is None:
|
||||
# message doesn't exist.
|
||||
message: nextcord.Message = await channel.send(highscore_string)
|
||||
group.message_id = message.id
|
||||
session.commit()
|
||||
else:
|
||||
try:
|
||||
message: nextcord.Message = await channel.fetch_message(group.message_id)
|
||||
await message.edit(content=highscore_string)
|
||||
except nextcord.NotFound:
|
||||
# Message was deleted on discord's side.
|
||||
message: nextcord.Message = await channel.send(highscore_string)
|
||||
group.message_id = message.id
|
||||
session.commit()
|
||||
|
||||
return
|
||||
|
||||
|
||||
async def submit_score(
|
||||
interaction: nextcord.Interaction,
|
||||
group_id: int,
|
||||
category_id: int,
|
||||
username: str,
|
||||
score: str,
|
||||
) -> None:
|
||||
"""
|
||||
Adds passed score to group. Then updates group message by calling send_highscore_message.
|
||||
|
||||
Ensures that a highscores channel exists.
|
||||
|
||||
Args:
|
||||
interaction (nextcord.Interaction): Interaction that caused the score submission.
|
||||
group_id (int): group id to submit score to.
|
||||
category_id (int): Category id to submit score to.
|
||||
username (str): The user who submitted the score.
|
||||
score (str): Score to submit. Need to convert to correct type.
|
||||
"""
|
||||
with Session(engine) as session:
|
||||
stmt = select(col(Category.is_time_record)).where(col(Category.id) == category_id)
|
||||
is_time_record = session.scalar(stmt)
|
||||
|
||||
if is_time_record:
|
||||
minutes = float(score.split(":", maxsplit=1)[0])
|
||||
seconds = float(score.split(":", maxsplit=1)[1])
|
||||
score_f = (minutes * 60) + seconds
|
||||
else:
|
||||
score_f = float(score)
|
||||
|
||||
stmt = select(Score).where(col(Score.username) == username).where(col(Score.category_id) == category_id)
|
||||
existing_score = session.scalar(stmt)
|
||||
|
||||
if existing_score:
|
||||
existing_score.score = score_f
|
||||
else:
|
||||
score_obj = Score(username=username, score=score_f, category_id=category_id)
|
||||
session.add(score_obj)
|
||||
session.commit()
|
||||
|
||||
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
|
||||
group = session.scalar(stmt)
|
||||
|
||||
await send_highscore_message(session, interaction.guild, group)
|
||||
@@ -1,26 +0,0 @@
|
||||
"""
|
||||
Help command
|
||||
"""
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from .help_command import NewHelpCommand
|
||||
|
||||
|
||||
class HelpCog(commands.Cog):
|
||||
"""Help Command"""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self._original_help_command = bot.help_command
|
||||
bot.help_command = NewHelpCommand()
|
||||
bot.help_command.cog = self
|
||||
|
||||
def cog_unload(self):
|
||||
self.bot.help_command = self._original_help_command
|
||||
|
||||
|
||||
# setup functions for bot
|
||||
def setup(bot: commands.Bot):
|
||||
"""Adds function to bot"""
|
||||
bot.add_cog(HelpCog(bot))
|
||||
@@ -1,84 +0,0 @@
|
||||
"""
|
||||
Help command. Copied from discord-bot-template.
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from nextcord.embeds import EmptyEmbed
|
||||
from nextcord.ext import commands
|
||||
import config # pylint: disable=import-error
|
||||
|
||||
|
||||
class NewHelpCommand(commands.MinimalHelpCommand):
|
||||
"""Custom help command override using embeds"""
|
||||
|
||||
COLOUR = nextcord.Colour.blurple()
|
||||
|
||||
def get_ending_note(self):
|
||||
"""Returns note to display at the bottom"""
|
||||
invoked_with = self.invoked_with
|
||||
return f"Use {config.BOT_PREFIX}{invoked_with} [command] for more info on a command."
|
||||
|
||||
def get_command_signature(self, command: commands.core.Command):
|
||||
"""Retrieves the signature portion of the help page."""
|
||||
return f"{command.qualified_name} {command.signature}"
|
||||
|
||||
async def send_bot_help(self, mapping: dict):
|
||||
"""implements bot command help page"""
|
||||
embed = nextcord.Embed(title="Bot Commands", colour=self.COLOUR)
|
||||
avatar = self.context.bot.user.avatar
|
||||
avatar_url = avatar.url if avatar else EmptyEmbed
|
||||
embed.set_author(name=self.context.bot.user.name, icon_url=avatar_url)
|
||||
description = self.context.bot.description
|
||||
if description:
|
||||
embed.description = description
|
||||
|
||||
for cog, commands in mapping.items(): # pylint: disable=redefined-outer-name
|
||||
name = "No Category" if cog is None else cog.qualified_name
|
||||
filtered = await self.filter_commands(commands, sort=True)
|
||||
if filtered:
|
||||
# \u2002 = middle dot
|
||||
value = "\u2002".join(f"{config.BOT_PREFIX}{c.name}" for c in filtered)
|
||||
if cog and cog.description:
|
||||
value = f"{cog.description}\n{value}"
|
||||
embed.add_field(name=name, value=value)
|
||||
|
||||
embed.set_footer(text=self.get_ending_note())
|
||||
await self.get_destination().send(embed=embed)
|
||||
|
||||
async def send_cog_help(self, cog: commands.Cog):
|
||||
"""implements cog help page"""
|
||||
embed = nextcord.Embed(title=f"{cog.qualified_name} Commands", colour=self.COLOUR)
|
||||
if cog.description:
|
||||
embed.description = cog.description
|
||||
|
||||
filtered = await self.filter_commands(cog.get_commands(), sort=True)
|
||||
for command in filtered:
|
||||
embed.add_field(
|
||||
name=self.get_command_signature(command),
|
||||
value=command.short_doc or "...",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
embed.set_footer(text=self.get_ending_note())
|
||||
await self.get_destination().send(embed=embed)
|
||||
|
||||
async def send_group_help(self, group: commands.Group):
|
||||
"""implements group help page and command help page"""
|
||||
embed = nextcord.Embed(title=group.qualified_name, colour=self.COLOUR)
|
||||
if group.help:
|
||||
embed.description = group.help
|
||||
|
||||
if isinstance(group, commands.Group):
|
||||
filtered = await self.filter_commands(group.commands, sort=True)
|
||||
for command in filtered:
|
||||
embed.add_field(
|
||||
name=self.get_command_signature(command),
|
||||
value=command.short_doc or "...",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
embed.set_footer(text=self.get_ending_note())
|
||||
await self.get_destination().send(embed=embed)
|
||||
|
||||
# Use the same function as group help for command help
|
||||
send_command_help = send_group_help # type: ignore
|
||||
@@ -1,43 +0,0 @@
|
||||
"""
|
||||
Defines cog for on message listener.
|
||||
"""
|
||||
|
||||
from nextcord.ext import commands
|
||||
import nextcord
|
||||
|
||||
from highscores import submission_messages # pylint: disable=import-error
|
||||
from highscores import highscores_data # pylint: disable=import-error
|
||||
|
||||
|
||||
class SubmissionListener(commands.Cog, name="Message Listener"):
|
||||
"""Sets up message listener."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, proof_message: nextcord.Message):
|
||||
"""Listener for message creation. Adds score and attachments to submission."""
|
||||
if proof_message.author.id in submission_messages:
|
||||
message_id = submission_messages[proof_message.author.id]
|
||||
submission_message: nextcord.Message = await self.bot.get_channel(
|
||||
highscores_data["submission_channel_id"]
|
||||
).fetch_message(message_id)
|
||||
|
||||
# need check if correct user and correct channel
|
||||
if proof_message.channel.id == highscores_data["submission_channel_id"]:
|
||||
|
||||
embed = submission_message.embeds[0]
|
||||
embed.add_field(name="Score", value=proof_message.content, inline=False)
|
||||
await submission_message.edit(
|
||||
files=[await attch.to_file() for attch in proof_message.attachments],
|
||||
embed=embed,
|
||||
)
|
||||
await proof_message.delete()
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot):
|
||||
"""Adds function to bot"""
|
||||
bot.add_cog(SubmissionListener(bot))
|
||||
@@ -1,51 +0,0 @@
|
||||
"""
|
||||
Bot command to post the highscores
|
||||
"""
|
||||
|
||||
from nextcord.ext import commands
|
||||
import nextcord
|
||||
|
||||
from cogs.submission.submission_views import SubmissionButton # pylint: disable=import-error
|
||||
from highscores import highscores_config # pylint: disable=import-error
|
||||
from highscores import highscores_data # pylint: disable=import-error
|
||||
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
|
||||
from utilities.utils import send_highscore_message # pylint: disable=import-error
|
||||
|
||||
|
||||
class PostHighscores(commands.Cog, name="Post Highscores"):
|
||||
"""Posts Highscores."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="post-scores")
|
||||
@commands.has_permissions(administrator=True)
|
||||
async def post_highscores(self, ctx: commands.Context):
|
||||
"""
|
||||
A command which posts the highscore in the designated channel.
|
||||
Sends warning if no channel has been registered.
|
||||
Usage:
|
||||
```
|
||||
?post-scores
|
||||
```
|
||||
"""
|
||||
highscore_channel_id = highscores_data["highscore_channel_id"]
|
||||
print(f"Printing out highscores information in channel {highscore_channel_id}")
|
||||
channel = self.bot.get_channel(highscore_channel_id)
|
||||
|
||||
error_response = 'Registered Highscores channel does not exist or was never registered. \n\
|
||||
Register with "?register" command.'
|
||||
assert channel is not None, await ctx.send(error_response)
|
||||
|
||||
for boss in highscores_config["bosses"]:
|
||||
message: nextcord.Message = await send_highscore_message(channel, boss["boss"])
|
||||
await message.edit(view=SubmissionButton(self.bot))
|
||||
|
||||
save_highscores_data(highscores_data)
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot):
|
||||
"""Adds function to bot"""
|
||||
bot.add_cog(PostHighscores(bot))
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
Bot command for registering various channels to the bot.
|
||||
"""
|
||||
|
||||
from nextcord import Embed
|
||||
from nextcord.ext import commands
|
||||
|
||||
from highscores import highscores_data # pylint: disable=import-error
|
||||
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
|
||||
|
||||
|
||||
class RegisterVerificationChannel(commands.Cog, name="Register"):
|
||||
"""Bot command for registering various channels to the bot."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="register")
|
||||
@commands.has_permissions(administrator=True)
|
||||
async def register_highscores(self, ctx: commands.Context, registration_type: str):
|
||||
"""A command which registers current channel as for various uses.
|
||||
Usage:
|
||||
```
|
||||
?register verify (Use in channel you want to verify submissions in.)
|
||||
?register highscore (Use in channel you want to show highscore messages in.)
|
||||
?register submission (Use in channel you want submission messages to be send it.)
|
||||
?register help (Shows help message.)
|
||||
```
|
||||
"""
|
||||
# respond to the message
|
||||
channel_id = ctx.channel.id
|
||||
|
||||
if registration_type == "verify":
|
||||
await ctx.send(f"Registered <#{channel_id}> as channel to post verifications in.")
|
||||
highscores_data["verification_channel_id"] = channel_id
|
||||
elif registration_type == "highscore":
|
||||
await ctx.send(f"Registered <#{channel_id}> as channel to post highscores in.")
|
||||
highscores_data["highscore_channel_id"] = channel_id
|
||||
elif registration_type == "submission":
|
||||
await ctx.send(f"Registered <#{channel_id}> as channel to post submissions in.")
|
||||
highscores_data["submission_channel_id"] = channel_id
|
||||
else:
|
||||
embed = Embed(
|
||||
title="Register command help", description="How to use the register command."
|
||||
)
|
||||
embed.add_field(
|
||||
name="?register verify", value="Use in channel you want to verify submissions in."
|
||||
)
|
||||
embed.add_field(
|
||||
name="?register highscore",
|
||||
value="Use in channel you want to show highscore messages in.",
|
||||
)
|
||||
embed.add_field(
|
||||
name="?register submission",
|
||||
value="Use in channel you want submission messages to be send it.",
|
||||
)
|
||||
embed.add_field(name="?register help", value="Use to show this message.")
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
save_highscores_data(highscores_data)
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot):
|
||||
"""Adds function to bot"""
|
||||
bot.add_cog(RegisterVerificationChannel(bot))
|
||||
@@ -1,53 +0,0 @@
|
||||
"""
|
||||
Bot command to submit a score
|
||||
"""
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from highscores import highscores_data # pylint: disable=import-error
|
||||
from utilities.utils import submit_score # pylint: disable=import-error
|
||||
from .verification_view import VerificationView
|
||||
|
||||
|
||||
class Score(commands.Cog, name="Score"):
|
||||
"""Submits a score."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
"""Called when the cog is loaded"""
|
||||
self.bot.add_view(VerificationView())
|
||||
|
||||
@commands.command(name="score")
|
||||
async def score(self, ctx: commands.Context, boss_name: str, category: str, score: str):
|
||||
"""A command which calls a function to submit the score to highscores.
|
||||
Gathers required information from command and passes on.
|
||||
Usage:
|
||||
```
|
||||
?score "Boss Name" "Category" "score"
|
||||
This command still works if you prefer to use it over the ui.
|
||||
```
|
||||
"""
|
||||
verification_channel_id = highscores_data["verification_channel_id"]
|
||||
channel = self.bot.get_channel(verification_channel_id)
|
||||
|
||||
error_response = "Registered Verification channel does not exist or was never registered. \
|
||||
Contact server admins."
|
||||
assert channel is not None, await ctx.send(error_response)
|
||||
|
||||
verification_string = f"{ctx.author.display_name}:{boss_name}:{category}:{score}"
|
||||
|
||||
await channel.send(
|
||||
content=verification_string,
|
||||
files=[await attch.to_file() for attch in ctx.message.attachments],
|
||||
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"""
|
||||
bot.add_cog(Score(bot))
|
||||
@@ -1,40 +0,0 @@
|
||||
"""View for verification buttons"""
|
||||
|
||||
import nextcord
|
||||
from utilities.utils import submit_score # pylint: disable=import-error
|
||||
|
||||
|
||||
class VerificationView(nextcord.ui.View):
|
||||
"""Defines views for verifying score submissions"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-score-command"
|
||||
)
|
||||
async def approve_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Button for approving score submission."""
|
||||
content = interaction.message.content.split(":")
|
||||
user = content[0]
|
||||
boss_name = content[1]
|
||||
category = content[2]
|
||||
score = content[3]
|
||||
|
||||
await submit_score(interaction, user, boss_name, category, score)
|
||||
await interaction.message.delete()
|
||||
await interaction.channel.send(
|
||||
f"Approved: {boss_name} {category} {user} {score}",
|
||||
files=[await attch.to_file() for attch in interaction.message.attachments],
|
||||
)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-score-command"
|
||||
)
|
||||
async def deny_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Button for denying score submission."""
|
||||
await interaction.message.delete()
|
||||
@@ -1,26 +0,0 @@
|
||||
"""
|
||||
Cog for creating submission process.
|
||||
"""
|
||||
|
||||
from nextcord.ext import commands
|
||||
|
||||
from .submission_views import SubmissionButton
|
||||
|
||||
|
||||
class SubmissionCog(commands.Cog, name="Submission Cog"):
|
||||
"""Constructs submission method."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
"""Called when cog is loaded"""
|
||||
self.bot.add_view(SubmissionButton(self.bot))
|
||||
|
||||
|
||||
# This function will be called when this extension is loaded.
|
||||
# It is necessary to add these functions to the bot.
|
||||
def setup(bot: commands.Bot):
|
||||
"""Adds function to bot"""
|
||||
bot.add_cog(SubmissionCog(bot))
|
||||
@@ -1,122 +0,0 @@
|
||||
"""Views used for submission process."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
from utilities.data_storage import open_message_map # pylint: disable=import-error
|
||||
from highscores import highscores_data # pylint: disable=import-error
|
||||
from highscores import submission_messages # pylint: disable=import-error
|
||||
from .verification_view import VerificationView
|
||||
|
||||
|
||||
class SubmissionButton(nextcord.ui.View):
|
||||
"""Defines view for score submission button."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
super().__init__(timeout=None)
|
||||
self._bot = bot
|
||||
|
||||
@nextcord.ui.button(label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submit")
|
||||
async def submit_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Button for score submission."""
|
||||
highscores_message_map = open_message_map()
|
||||
boss_name = highscores_message_map[str(interaction.message.id)]
|
||||
embed = nextcord.Embed(title="Highscores Submission")
|
||||
embed.add_field(name="Boss", value=boss_name, inline=False)
|
||||
embed.add_field(name="User", value=interaction.user.display_name, inline=False)
|
||||
channel = self._bot.get_channel(highscores_data["submission_channel_id"])
|
||||
submission_message = await channel.send(
|
||||
f"<@{interaction.user.id}>",
|
||||
view=SubmissionDropdownView(
|
||||
self._bot, interaction, await self._category_select_options(boss_name)
|
||||
),
|
||||
embed=embed,
|
||||
)
|
||||
|
||||
# a user should only have 1 submission active at once.
|
||||
submission_messages[interaction.user.id] = submission_message.id
|
||||
|
||||
async def _category_select_options(self, boss_name: str) -> "list[nextcord.SelectOption]":
|
||||
options: list[nextcord.SelectOption] = [] # pylint: disable=unsubscriptable-object
|
||||
boss_dict = highscores_data[boss_name]
|
||||
for category in boss_dict["categories"].items():
|
||||
options.append(nextcord.SelectOption(label=category[0]))
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class SubmissionDropdown(nextcord.ui.Select):
|
||||
"""Defines dropdown menu for submission"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: commands.Bot,
|
||||
options: "list[nextcord.SelectOption]",
|
||||
):
|
||||
super().__init__(
|
||||
placeholder="Choose submission category.", min_values=1, max_values=1, options=options
|
||||
)
|
||||
self._bot = bot
|
||||
|
||||
async def callback(self, interaction: nextcord.Interaction):
|
||||
embed = interaction.message.embeds[0]
|
||||
embed.add_field(name="Category", value=self.values[0], inline=False)
|
||||
await interaction.message.edit(
|
||||
embed=embed,
|
||||
)
|
||||
|
||||
|
||||
class SubmissionDropdownView(nextcord.ui.View):
|
||||
"""Defines submission dropdown process view"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: commands.Bot,
|
||||
prev_interaction: nextcord.Interaction,
|
||||
options: "list[nextcord.SelectOption]",
|
||||
*,
|
||||
timeout: Optional[float] = 180,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.add_item(SubmissionDropdown(bot, options))
|
||||
self._prev_interaction = prev_interaction
|
||||
self.bot = bot
|
||||
|
||||
async def interaction_check(self, interaction: nextcord.Interaction) -> bool:
|
||||
return self._prev_interaction.user == interaction.user
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Submit Score", style=nextcord.ButtonStyle.green, custom_id="submit-score"
|
||||
)
|
||||
async def submit_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Defines button to submit a user's score."""
|
||||
verification_channel_id = highscores_data["verification_channel_id"]
|
||||
channel = self.bot.get_channel(verification_channel_id)
|
||||
|
||||
error_response = "Registered Verification channel does not exist or was never registered. \
|
||||
Contact server admins."
|
||||
assert channel is not None, await channel.send(error_response)
|
||||
|
||||
await channel.send(
|
||||
files=[await attch.to_file() for attch in interaction.message.attachments],
|
||||
view=VerificationView(),
|
||||
embed=interaction.message.embeds[0],
|
||||
)
|
||||
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
|
||||
submission_messages.pop(interaction.user.id)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Cancel", style=nextcord.ButtonStyle.red, custom_id="cancel-submission"
|
||||
)
|
||||
async def cancel_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Deletes message to cancel interaction."""
|
||||
submission_messages.pop(interaction.user.id)
|
||||
await interaction.message.delete()
|
||||
@@ -1,46 +0,0 @@
|
||||
"""View for verification buttons"""
|
||||
|
||||
import nextcord
|
||||
from utilities.utils import submit_score # pylint: disable=import-error
|
||||
|
||||
|
||||
class VerificationView(nextcord.ui.View):
|
||||
"""Defines views for verifying score submissions"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission"
|
||||
)
|
||||
async def approve_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Button for approving score submission."""
|
||||
content = interaction.message.embeds[0].to_dict()
|
||||
|
||||
for field in content["fields"]:
|
||||
if field["name"] == "Boss":
|
||||
boss_name = field["value"]
|
||||
elif field["name"] == "Category":
|
||||
category = field["value"]
|
||||
elif field["name"] == "Score":
|
||||
score = field["value"]
|
||||
elif field["name"] == "User":
|
||||
user = field["value"]
|
||||
|
||||
try:
|
||||
await submit_score(interaction, user, boss_name, category, score)
|
||||
await interaction.message.edit("Approved.", view=None)
|
||||
except UnboundLocalError:
|
||||
await interaction.send("Instruct user to submit with all fields.")
|
||||
await interaction.message.edit(view=None)
|
||||
|
||||
@nextcord.ui.button(
|
||||
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission"
|
||||
)
|
||||
async def deny_button(
|
||||
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
|
||||
): # pylint: disable=unused-argument
|
||||
"""Button for denying score submission."""
|
||||
await interaction.message.delete()
|
||||
@@ -1,9 +0,0 @@
|
||||
import os
|
||||
|
||||
from dotenv.main import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Discord config
|
||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
||||
BOT_PREFIX = "?"
|
||||
@@ -1,206 +0,0 @@
|
||||
{
|
||||
"bosses": [
|
||||
{
|
||||
"boss": "The Ambassador",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Araxxi",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Arch-Glacor",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Astellarn",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "The Barrows Brothers",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "The Barrows: Rise of the Six",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Beastmaster Durzag",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Black Stone Dragon",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Chaos Elemental",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Commander Zilyana",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Corporeal Beast",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Crassian Leviathan",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Croesus",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Dagannoth Kings",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "General Graardor",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Giant Mole",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Gregorovic",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Har-Aken",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Helwyr",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Kalphite King",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Kalphite Queen",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Kerapac",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "King Black Dragon",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Kree'arra",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "K'ril Tsutsaroth",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Legiones",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "The Magister",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Masuta the Ascended",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Nex",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Nex - Angel of Death",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Queen Black Dragon",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Raksha",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Rex Matriarchs",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "The Sanctum Guardian",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Seiryu the Azure Serpent",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Solak",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Taraket the Necromancer",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Telos",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "The Twin Furies",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "TzKal-Zuk",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "TzTok-Jad",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Verak Lith",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Vindicta & Gorvek",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Vorago",
|
||||
"hardmode": true
|
||||
},
|
||||
{
|
||||
"boss": "Yakamaru",
|
||||
"hardmode": false
|
||||
},
|
||||
{
|
||||
"boss": "Zamorak, Lord of Chaos",
|
||||
"hardmode": true
|
||||
}
|
||||
],
|
||||
"categories": {
|
||||
"Kill Count": {
|
||||
"hardmode": true,
|
||||
"ascending": false,
|
||||
"is_time_record": false
|
||||
},
|
||||
"Kill Time": {
|
||||
"hardmode": true,
|
||||
"ascending": true,
|
||||
"is_time_record": true
|
||||
},
|
||||
"Spooned Log": {
|
||||
"hardmode": false,
|
||||
"ascending": true,
|
||||
"is_time_record": false
|
||||
}
|
||||
},
|
||||
"highscore_size": 3
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
"""
|
||||
main file for pvm-highscores bot
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
import config
|
||||
from utilities.data_storage import open_highscores_config
|
||||
from utilities.data_storage import open_highscores_data
|
||||
from utilities.data_storage import open_message_map
|
||||
|
||||
logger = logging.getLogger("nextcord")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
highscores_config = open_highscores_config()
|
||||
highscores_data = open_highscores_data()
|
||||
highscores_message_map = open_message_map()
|
||||
|
||||
submission_messages: dict = {}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function for bot."""
|
||||
# Allows privledged intents for monitoring members joining, roles editing, and role assignments
|
||||
# These need to be enabled in the developer portal as well
|
||||
intents = nextcord.Intents.default()
|
||||
|
||||
# Required in order to read messages (eg. prefix commands)
|
||||
intents.message_content = True # pylint: disable=assigning-non-slot
|
||||
intents.messages = True # pylint: disable=assigning-non-slot
|
||||
|
||||
# To enable the guilds priveleged intent:
|
||||
# intents.guilds = True
|
||||
|
||||
# To enable the members priveleged intent:
|
||||
# intents.members = True
|
||||
|
||||
# Set custom status to "Listening to ?help"
|
||||
activity = nextcord.Activity(
|
||||
type=nextcord.ActivityType.listening, name=f"{config.BOT_PREFIX}help"
|
||||
)
|
||||
|
||||
bot = commands.Bot(
|
||||
commands.when_mentioned_or(config.BOT_PREFIX),
|
||||
intents=intents,
|
||||
activity=activity,
|
||||
)
|
||||
|
||||
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
|
||||
for folder in os.listdir("cogs"):
|
||||
bot.load_extension(f"cogs.{folder}")
|
||||
|
||||
@bot.listen()
|
||||
async def on_ready():
|
||||
"""When Discord is connected"""
|
||||
assert bot.user is not None
|
||||
print(f"{bot.user.name} has connected to Discord!")
|
||||
|
||||
# Run Discord bot
|
||||
bot.run(config.DISCORD_TOKEN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
[project]
|
||||
name = "agon"
|
||||
version = "0.1.0"
|
||||
description = "A highscores discord bot"
|
||||
authors = [{ name = "imcompiling", email = "<[email protected]>" }]
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"cryptography>=43.0.3",
|
||||
"nextcord>=2.6.0",
|
||||
"pandas>=2.2.3",
|
||||
"pymysql>=1.1.1",
|
||||
"python-dotenv>=1.0.1",
|
||||
"setuptools>=75.3.0",
|
||||
"sqlmodel>=0.0.22",
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["ruff>=0.7.2"]
|
||||
|
||||
[tool.ruff]
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
# Same as Black.
|
||||
line-length = 120
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.8
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["ALL"]
|
||||
ignore = [
|
||||
"B008", # I build SlashOptions as arguments.
|
||||
"COM812",
|
||||
"COM819",
|
||||
"D107", # don't want docstrings on init functions
|
||||
"D203",
|
||||
"D206",
|
||||
"D212",
|
||||
"D300",
|
||||
"E111",
|
||||
"E114",
|
||||
"E117",
|
||||
"ISC001",
|
||||
"ISC002",
|
||||
"PD011", # doesn't like me using '.values' on embeds....
|
||||
"Q000",
|
||||
"Q001",
|
||||
"Q002",
|
||||
"Q003",
|
||||
"W191",
|
||||
]
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.lint.flake8-annotations]
|
||||
mypy-init-return = true
|
||||
|
||||
[tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
quote-style = "double"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
# Enable auto-formatting of code examples in docstrings. Markdown,
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
#
|
||||
# This is currently disabled by default, but it is planned for this
|
||||
# to be opt-out in the future.
|
||||
docstring-code-format = true
|
||||
|
||||
# Set the line length limit used when formatting code snippets in
|
||||
# docstrings.
|
||||
#
|
||||
# This only has an effect when the `docstring-code-format` setting is
|
||||
# enabled.
|
||||
docstring-code-line-length = "dynamic"
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"lockFileMaintenance": {
|
||||
"automerge": true
|
||||
},
|
||||
"major": {
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
},
|
||||
"minor": {
|
||||
"automerge": true,
|
||||
"automergeType": "branch"
|
||||
},
|
||||
"patch": {
|
||||
"automerge": true,
|
||||
"automergeType": "branch"
|
||||
},
|
||||
"pin": {
|
||||
"automerge": true,
|
||||
"automergeType": "branch"
|
||||
},
|
||||
"ignoreTests": true,
|
||||
"dockerfile": {
|
||||
"minor": {
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
nextcord>=2.0.0rc1
|
||||
python-dotenv>=0.20.0
|
||||
@@ -1,75 +0,0 @@
|
||||
"""Functions for retreiving and storing highscores data and configurations."""
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
HIGHSCORES_CONFIG_FILE = "highscores_config.json"
|
||||
HIGHSCORES_DATA_FILE = "highscores_data.json"
|
||||
HIGHSCORES_MESSAGE_MAP_FILE = "highscores_message_map.json"
|
||||
|
||||
|
||||
def open_highscores_config():
|
||||
"""
|
||||
Opens the highscores config file and returns.
|
||||
@return: dict of highscores config.
|
||||
"""
|
||||
# Load bot config
|
||||
with open(HIGHSCORES_CONFIG_FILE, encoding="utf-8", mode="r") as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def open_highscores_data():
|
||||
"""
|
||||
Opens a highscores_data.json file if one exists, otherwise creates a skeleton of the data.
|
||||
@return: dict of highscores data.
|
||||
"""
|
||||
if os.path.exists(HIGHSCORES_DATA_FILE):
|
||||
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="r") as file:
|
||||
data = json.load(file)
|
||||
else:
|
||||
# create the json object
|
||||
data = {"highscore_channel_id": -1, "verification_channel_id": -1}
|
||||
config = open_highscores_config()
|
||||
for boss in config["bosses"]:
|
||||
data[boss["boss"]] = {"message_id": 0}
|
||||
boss_categories = {}
|
||||
for key, value in config["categories"].items():
|
||||
rank_list = []
|
||||
boss_categories[key] = rank_list
|
||||
if boss["hardmode"] and value["hardmode"]:
|
||||
rank_list = []
|
||||
boss_categories["Hardmode " + key] = rank_list
|
||||
data[boss["boss"]]["categories"] = boss_categories
|
||||
return data
|
||||
|
||||
|
||||
def open_message_map():
|
||||
"""
|
||||
Opens a highscore_message_map.json file if one exists. Otherwise returns empty dict.
|
||||
@return: dict message_map of message to boss.
|
||||
"""
|
||||
if os.path.exists(HIGHSCORES_MESSAGE_MAP_FILE):
|
||||
with open(HIGHSCORES_MESSAGE_MAP_FILE, encoding="utf-8", mode="r") as file:
|
||||
message_map = json.load(file)
|
||||
else:
|
||||
# create the json object
|
||||
message_map = {}
|
||||
return message_map
|
||||
|
||||
|
||||
def save_highscores_data(data):
|
||||
"""
|
||||
Saves the highscores data dict into a json file.
|
||||
@param data: dict of highscores data.
|
||||
"""
|
||||
with open(HIGHSCORES_DATA_FILE, encoding="utf-8", mode="w") as file:
|
||||
file.write(json.dumps(data, indent=4))
|
||||
|
||||
|
||||
def save_message_map(message_map):
|
||||
"""
|
||||
Saves the highscores message mao data dict into a json file.
|
||||
@param map: dict map of message to boss.
|
||||
"""
|
||||
with open(HIGHSCORES_MESSAGE_MAP_FILE, encoding="utf-8", mode="w") as file:
|
||||
file.write(json.dumps(message_map, indent=4))
|
||||
@@ -1,156 +0,0 @@
|
||||
"""
|
||||
Contains various utlity functions for highscores bot
|
||||
"""
|
||||
|
||||
import nextcord
|
||||
from highscores import highscores_config # pylint: disable=import-error
|
||||
from highscores import highscores_data # pylint: disable=import-error
|
||||
from highscores import highscores_message_map # pylint: disable=import-error
|
||||
from utilities.data_storage import save_highscores_data # pylint: disable=import-error
|
||||
from utilities.data_storage import save_message_map # pylint: disable=import-error
|
||||
|
||||
|
||||
def format_highscore_message(boss_name: str):
|
||||
"""
|
||||
returns a formatted string for a boss's highscore
|
||||
@param boss: the dictionary object for the boss
|
||||
@return the formatted string
|
||||
"""
|
||||
ret_string = f"**{boss_name}**\n```"
|
||||
boss_data = highscores_data[boss_name]
|
||||
|
||||
# Gather all categories and add to string
|
||||
for key, value in boss_data["categories"].items():
|
||||
ret_string += f"{key}".ljust(25, " ")
|
||||
ret_string += "\n"
|
||||
|
||||
highscore_size = highscores_config["highscore_size"]
|
||||
|
||||
# list rankings in a single line. Start with first place for each category and then continue
|
||||
for i in range(highscore_size):
|
||||
for key, value in boss_data["categories"].items():
|
||||
if len(value) > i:
|
||||
score = value[i]
|
||||
ret_string += f"{i+1}: {score[0]} - {score[1]}".ljust(25, " ")
|
||||
else:
|
||||
ret_string += f"{i+1}: submit your score".ljust(25, " ")
|
||||
ret_string += "\n"
|
||||
|
||||
return ret_string + "```"
|
||||
|
||||
|
||||
async def send_highscore_message(channel, boss_name: str) -> nextcord.Message:
|
||||
"""
|
||||
Function takes care of checking if a message exists for a boss, then
|
||||
creating and sending/editing the message
|
||||
@param channel: the channel to send the message in/search for message id
|
||||
@param boss: the boss to send the message about
|
||||
"""
|
||||
highscore_string = format_highscore_message(boss_name)
|
||||
|
||||
message_id = highscores_data[boss_name]["message_id"]
|
||||
try:
|
||||
message: nextcord.Message = await channel.fetch_message(message_id)
|
||||
await message.edit(content=highscore_string)
|
||||
except nextcord.NotFound:
|
||||
# message doesn't exist.
|
||||
message: nextcord.Message = await channel.send(highscore_string)
|
||||
message_id = message.id
|
||||
highscores_data[boss_name]["message_id"] = message_id
|
||||
# save message -> boss_name in message_map
|
||||
highscores_message_map[str(message_id)] = boss_name
|
||||
save_message_map(highscores_message_map)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
async def submit_score(
|
||||
interaction: nextcord.Interaction, user: str, boss_name: str, category: str, score: str
|
||||
):
|
||||
"""
|
||||
Adds a score to the high scores and sorts. Removes a score if larger than size limit.
|
||||
@param boss: the boss to add score to
|
||||
@param category: the category to add score to
|
||||
@param user: user who submitted score
|
||||
@param score: score to add
|
||||
"""
|
||||
# TODO update user submission to not have dupe users
|
||||
|
||||
# hardmode category, must have different category variable for config access.
|
||||
if category.find("Hardmode ") != -1:
|
||||
config_category = category.replace("Hardmode ", "")
|
||||
else:
|
||||
config_category = category
|
||||
|
||||
# capping to 12 characters because runescape names are 12 characters long.
|
||||
# Splitting on pipe. My discord has multiple names on account split by pipe.
|
||||
user = user.split("|")[0][:12]
|
||||
|
||||
# strip leading zeros and colon from score
|
||||
score = score.lstrip("0:")
|
||||
|
||||
# create score tuple for either time or int
|
||||
# define sort index
|
||||
if highscores_config["categories"][config_category]["is_time_record"]:
|
||||
if score.find(":") == -1:
|
||||
score_seconds = float(score)
|
||||
else:
|
||||
minutes = score.split(":")[0]
|
||||
seconds = score.split(":")[1]
|
||||
score_seconds = int(minutes) * 60 + float(seconds)
|
||||
score_tuple = (user, score, score_seconds)
|
||||
sort_index = 2
|
||||
|
||||
else:
|
||||
score_tuple = (user, int(score))
|
||||
sort_index = 1
|
||||
|
||||
scores: "list[tuple]" = highscores_data[boss_name]["categories"][category]
|
||||
|
||||
# if user is in an existing tuple, delete it.
|
||||
users_scores: "list[tuple]" = [score_tuple]
|
||||
for tup in scores:
|
||||
if user == tup[0]:
|
||||
# Add tup to users_scores, remove from scores
|
||||
users_scores.append(tup)
|
||||
|
||||
# sort users_scores
|
||||
users_scores.sort(
|
||||
key=lambda x: x[sort_index],
|
||||
reverse=not highscores_config["categories"][config_category]["ascending"],
|
||||
)
|
||||
|
||||
for index, tup in enumerate(users_scores):
|
||||
if index == 0:
|
||||
# first score is the best score.
|
||||
if tup not in scores:
|
||||
scores.append(tup)
|
||||
else:
|
||||
# discard all other scores from scores list.
|
||||
if tup in scores:
|
||||
scores.remove(tup)
|
||||
|
||||
# sort scores.
|
||||
scores.sort(
|
||||
key=lambda x: x[sort_index],
|
||||
reverse=not highscores_config["categories"][config_category]["ascending"],
|
||||
)
|
||||
|
||||
# enforce highscore size
|
||||
while len(scores) > highscores_config["highscore_size"]:
|
||||
scores.pop()
|
||||
highscores_data[boss_name]["categories"][category] = scores
|
||||
|
||||
# edit message
|
||||
highscore_channel_id = highscores_data["highscore_channel_id"]
|
||||
print(f"Editting message {highscore_channel_id}")
|
||||
channel = interaction.guild.get_channel(highscore_channel_id)
|
||||
|
||||
error_response = 'Registered Highscores channel does not exist or was never registered. \
|
||||
Register with "?register" command.'
|
||||
assert channel is not None, await interaction.send(error_response, ephemeral=True)
|
||||
|
||||
await send_highscore_message(channel, boss_name)
|
||||
|
||||
# save data
|
||||
save_highscores_data(highscores_data)
|
||||
@@ -0,0 +1,676 @@
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "agon"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "nextcord" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pymysql" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sqlmodel" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "cryptography", specifier = ">=43.0.3" },
|
||||
{ name = "nextcord", specifier = ">=2.6.0" },
|
||||
{ name = "pandas", specifier = ">=2.2.3" },
|
||||
{ name = "pymysql", specifier = ">=1.1.1" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||
{ name = "setuptools", specifier = ">=75.3.0" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.22" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.7.2" }]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
{ name = "aiosignal" },
|
||||
{ name = "attrs" },
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/07/4d2eb92798dc636e9512051b7fcfd3e6032a15c70af852abe777757e4c2d/aiohttp-3.11.0.tar.gz", hash = "sha256:f57a0de48dda792629e7952d34a0c7b81ea336bb9b721391c7c58145b237fe55", size = 7656057 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/a1/ed155cbbe9b518e4af3f7bcb914763962a6ffffcfe703915c77bdf5c3790/aiohttp-3.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f8f0d79b923070f25674e4ea8f3d61c9d89d24d9598d50ff32c5b9b23c79a25b", size = 701102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e2/d3e01a6ad245426cd819a48483db82000f110c6438fc7f41cda3cefa02e6/aiohttp-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:113bf06b029143e94a47c4f36e11a8b7e396e9d1f1fc8cea58e6b7e370cfed38", size = 461039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/9e/1bd08f529222c0dd4301a06894c3beb4784e8ef3179afee8b6860869bc92/aiohttp-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3e1ed8d152cccceffb1ee7a2ac227c16372e453fb11b3aeaa56783049b85d3f6", size = 453661 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/03/ffab64af75db3a8f33c8701cba25e06e0fb1858d050f8698077ecaece256/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2e82e515e268b965424ecabebd91834a41b36260b6ef5db015ee12ddb28ef3", size = 1676578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/23/22f2b6d1f57ddade6f7d6630f39cbab02e6be9a62b09b9b6874c1f769ae4/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1c49bc393d854d4421ebc174a0a41f9261f50d3694d8ca277146cbbcfd24ee7", size = 1731906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/e8/4f02ebea5f1032b382bb150a843ceb348b10f536c371e320892d1447ea98/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57e17c6d71f2dc857a8a1d09be1be7802e35d90fb4ba4b06cf1aab6414a57894", size = 1786772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/db/62ce138bd83b5905b2d368dd5f2eae93599c2c37586f2907acecd767c0e1/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12071dd2cc95ba81e0f2737bebcb98b2a8656015e87772e84e8fb9e635b5da6e", size = 1683964 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/6c/aee13936697dfb4ea879a9acb6d26a2012d28e2142ef6f92d706d8f4cd3d/aiohttp-3.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97056d3422594e0787733ac4c45bef58722d452f4dc6615fee42f59fe51707dd", size = 1616545 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/4d/5948c19f61e3e0c5a2a922ad32cff4614a89172b07f4a69d4c1169f79007/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ec5efbc872b00ddd85e3904059d274f284cff314e13f48776050ca2c58f451d", size = 1634428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/5a/b78ffab91436ccc9a82b4f43b24db2f70e9cabd19f414b417f25ca455060/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd505a1121ad5b666191840b7bd1d8cb917df2647deeca6f3474331b72452362", size = 1647244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/8a/459335e065715b92f8448a213f744526f2c0c4d5b78986092b10458af501/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:600b1d9f86a130131915e2f2127664311b33902c486b21a747d626f5144b4471", size = 1695006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/bd/9e2d7b0a192ecccdd46d042b9fb023db62e5de1fdf371c5b60fd5239c3d3/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8c47a0ba6c2b3d3e5715f8338d657badd21f778c6be16701922c65521c5ecfc9", size = 1726262 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/b2/c96436f71beafc3304e8de25942a2f3462358c018ebd31b1dada8cf6392d/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b323b5d3aef7dd811424c269322eec58a977c0c8152e650159e47210d900504", size = 1687536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3b/39f789385d12987daa4a2dd736b796c9770e03adda8602991bbae0c9c3a1/aiohttp-3.11.0-cp312-cp312-win32.whl", hash = "sha256:aabc4e92cb153636d6be54e84dad1b252ddb9aebe077942b6dcffe5e468d476a", size = 409129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/69/de71e1abde4340e9ea0b0ba67f56a4784ef3b4759d89cc07654487cc5ee1/aiohttp-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:508cfcc99534b1282595357592d8367b44392b21f6eb5d4dc021f8d0d809e94d", size = 435694 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/9a/768cea33c03a832a98292858a7bf2ec87f50b27bd8996eaafc5e9de96625/aiohttp-3.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c98a596ac20e8980cc6f34c0c92a113e98eb08f3997c150064d26d2aeb043e5a", size = 694285 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/e1/ae1a1c48e74b1a8cd2c3a0d9b872f7feae9276515a6bce29c1a7b4120e39/aiohttp-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ad14cdc0fba4df31c0f6e06c21928c5b924725cbf60d0ccc5f6e7132636250e9", size = 457450 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/30/b5b443ab48050daa0e6506c5974869bc8f37c48494d829ab83cef7239bf9/aiohttp-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:170fb2324826bb9f08055a8291f42192ae5ee2f25b2966c8f0f4537c61d73a7b", size = 450571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/32/fdf0f2754c9a51867b54fb4ebfc05445cfb9405d0cbad70441cb796f7a9e/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdad66685fcf2ad14ce522cf849d4a025f4fd206d6cfc3f403d9873e4c243b03", size = 1660460 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/be/8c8569b965ea993cca1bf0788c146c2b4902341cdcf2252b946a639c82ba/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8b95a63a8e8b5f0464bd8b1b0d59d2bec98a59b6aacc71e9be23df6989b3dfb", size = 1713465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/05/00656e6ed46de5730d2db43ad54970b757fef6d573de4ab3f8e12e4da63c/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7bcfcede95531589295f56e924702cef7f9685c9e4e5407592e04ded6a65bf3", size = 1770117 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/f4/bf2afd31461a8cfdd83769602f6ea9d401c2f4860c91e518513517d4adf0/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ecc2fb1a0a9d48cf773add34196cddf7e488e48e9596e090849751bf43098f4", size = 1670818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/c3/02c47e5e41b643687f781ba5e61011579850d7e04f1dc76050df71f62522/aiohttp-3.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fef105113d56e817cb9bcc609667ee461321413a7b972b03f5b4939f40f307c", size = 1598283 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/f0/2fbb9483c4cabbdc87696b8bf67b587481f48667a01ba2727cc9f959519c/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d33b4490026968bdc7f0729b9d87a3a6b1e09043557d2fc1c605c6072deb2f11", size = 1614447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/74/d9c2ea812bda194647e0072d27cfc9b362b658a44e67508e392b7c91b4bc/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6362f50a6f0e5482c4330d2151cb682779230683da0e155c15ec9fc58cb50b6a", size = 1614934 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/05/3b8379f464921a8eb24fe58e7518c746c55811758efc5862b2700caf5337/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f698aa61879df64425191d41213dfd99efdc1627e6398e6d7aa5c312fac9702", size = 1682139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/ea/e5e46efb8bd1ba8f50630848b563bb470a0f70b0dcd89b1331a0b1760887/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0e7a0762cc29cd3acd01a4d2b547b3af7956ad230ebb80b529a8e4f3e4740fe8", size = 1712695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/66/a263ffeaeca2933156334d1d4f4f7079d07b47d47d739ed95f3dc507311a/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e4fb7f5354d39490d8209aefdf5830b208d01c7293a2164e404312c3d8bc55", size = 1667638 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/43/8db1b8402b96510e864ad9595cc84d3d990c1f6e3407f00deb5f6d078f5d/aiohttp-3.11.0-cp313-cp313-win32.whl", hash = "sha256:6c5a6958f4366496004cf503d847093d464814543f157ef3b738bbf604232415", size = 407937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/7f/c548fc3b3069e3b5c3e3da5c559293be75d5b1cff1dd9eb33aefcad1ec4a/aiohttp-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ed360d6672a9423aad39902a4e9fe305464d20ed7931dbdba30a4625782d875", size = 433946 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "frozenlist" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "24.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "43.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nextcord"
|
||||
version = "2.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/0e/347795cec6bc841ed426bc5e112380335869e9ba0922c5ed7ec7bb8ab1a5/nextcord-2.6.0.tar.gz", hash = "sha256:ccf461157be682cbceaa474e8df4afa302351b8d6ced632f9ccb7aa647b092e7", size = 932913 }
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "2.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymysql"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2024.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/06/09d1276df977eece383d0ed66052fc24ec4550a61f8fbc0a11200e690496/ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313", size = 3243664 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/56/933d433c2489e4642487b835f53dd9ff015fb3d8fa459b09bb2ce42d7c4b/ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344", size = 10372090 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ea/1f0a22a6bcdd3fc26c73f63a025d05bd565901b729d56bcb093c722a6c4c/ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0", size = 10190037 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/74/aca75666e0d481fe394e76a8647c44ea919087748024924baa1a17371e3e/ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9", size = 9811998 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/a1/cf446a0d7f78ea1f0bd2b9171c11dfe746585c0c4a734b25966121eb4f5d/ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5", size = 10620626 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c1/82b27d09286ae855f5d03b1ad37cf243f21eb0081732d4d7b0d658d439cb/ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299", size = 10177598 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/42/c0acac22753bf74013d035a5ef6c5c4c40ad4d6686bfb3fda7c6f37d9b37/ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e", size = 11171963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/18/bb0befb7fb9121dd9009e6a72eb98e24f1bacb07c6f3ecb55f032ba98aed/ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29", size = 11856157 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/91/04e98d7d6e32eca9d1372be595f9abc7b7f048795e32eb2edbd8794d50bd/ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5", size = 11440331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/dc/3fe99f2ce10b76d389041a1b9f99e7066332e479435d4bebcceea16caff5/ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67", size = 12725354 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/7b/1daa712de1c5bc6cbbf9fa60e9c41cc48cda962dc6d2c4f2a224d2c3007e/ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2", size = 11010091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/db/1227a903587432eb569e57a95b15a4f191a71fe315cde4c0312df7bc85da/ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d", size = 10610687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/e2/dc41ee90c3085aadad4da614d310d834f641aaafddf3dfbba08210c616ce/ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2", size = 10254843 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/09/5f6cac1c91542bc5bd33d40b4c13b637bf64d7bb29e091dadb01b62527fe/ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2", size = 10730962 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/42/89a4b9a24ef7d00269e24086c417a006f9a3ffeac2c80f2629eb5ce140ee/ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16", size = 11101907 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/5c/efdb4777686683a8edce94ffd812783bddcd3d2454d38c5ac193fef7c500/ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc", size = 8611095 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/b8/28fbc6a4efa50178f973972d1c84b2d0a33cdc731588522ab751ac3da2f5/ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088", size = 9418283 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/77/b587cba6febd5e2003374f37eb89633f79f161e71084f94057c8653b7fb3/ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c", size = 8725228 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "75.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/db/722a42ffdc226e950c4757b3da7b56ff5c090bb265dccd707f7b8a3c6fee/setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef", size = 1336032 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/df/88ccbee85aefbca071db004fdc8f8d2507d55d5a9dc27ebb93c92edb1bd8/setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829", size = 1222710 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.36"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlmodel"
|
||||
version = "0.0.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/39/8641040ab0d5e1d8a1c2325ae89a01ae659fc96c61a43d158fb71c9a0bf0/sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e", size = 116392 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/b1/3af5104b716c420e40a6ea1b09886cae3a1b9f4538343875f637755cae5b/sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b", size = 28276 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2024.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/9c/9c0a9bfa683fc1be7fdcd9687635151544d992cccd48892dc5e0a5885a29/yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", size = 178163 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/af/e25615c7920396219b943b9ff8b34636ae3e1ad30777649371317d7f05f8/yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", size = 141839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/5e/363d9de3495c7c66592523f05d21576a811015579e0c87dd38c7b5788afd/yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", size = 94125 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a2/b65447626227ebe36f18f63ac551790068bf42c69bb22dfa3ae986170728/yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", size = 92048 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/f5/2ef86458446f85cde10582054fd5113495ef8ce8477da35aaaf26d2970ef/yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", size = 331472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/6b/1ba79758ba352cdf2ad4c20cab1b982dd369aa595bb0d7601fc89bf82bee/yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", size = 341260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/41/4e07c2afca3f9ed3da5b0e38d43d0280d9b624a3d5c478c425e5ce17775c/yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", size = 340882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c0/cd8e94618983c1b811af082e1a7ad7764edb3a6af2bc6b468e0e686238ba/yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", size = 336648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fc/73ec4340d391ffbb8f34eb4c55429784ec9f5bd37973ce86d52d67135418/yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", size = 325019 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/48/da3ebf418fc239d0a156b3bdec6b17a5446f8d2dea752299c6e47b143a85/yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", size = 342841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/79/107272745a470a8167924e353a5312eb52b5a9bb58e22686adc46c94f7ec/yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", size = 341433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/9c/6459668b3b8dcc11cd061fc53e12737e740fb6b1575b49c84cbffb387b3a/yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", size = 344927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/0b/93a17ed733aca8164fc3a01cb7d47b3f08854ce4f957cce67a6afdb388a0/yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", size = 355732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/63/ead2ed6aec3c59397e135cadc66572330325a0c24cd353cd5c94f5e63463/yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", size = 362123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/bf/f6b75b4c2fcf0e7bb56edc0ed74e33f37fac45dc40e5a52a3be66b02587a/yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", size = 356355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/1f/50a0257cd07eef65c8c65ad6a21f5fb230012d659e021aeb6ac8a7897bf6/yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", size = 83279 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/82/fafb2c1268d63d54ec08b3a254fbe51f4ef098211501df646026717abee3/yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", size = 89590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 },
|
||||
]
|
||||
Reference in New Issue
Block a user