Compare commits
54 Commits
413cabb7f9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 326934ca9b | |||
| 077dc25a5d | |||
| 69fe72512c | |||
| cefa490f5f | |||
| 28271623be | |||
| 5f350f1884 | |||
| ff54e2b053 | |||
| 244bed2c51 | |||
| 7dea88a1b3 | |||
| dd2ffced4c | |||
| 0427dbe6bd | |||
| 87477820ec | |||
| ff51dd7f8b | |||
| dc7e5abf4e | |||
| 228e0043d5 | |||
| 7d57f00e5d | |||
| 0090862359 | |||
| 3d17b6ef75 | |||
| 0dd78f1507 | |||
| c689fcb4e7 | |||
| 2f7b0af20c | |||
| f5bf263635 | |||
| e94d0a7714 | |||
| feeaba2c65 | |||
| 48d42c2a4b | |||
| 4b4dfa69fd | |||
| 1d865e605c | |||
| d79a6be274 | |||
| d8240aeae0 | |||
| 2df45a15f8 | |||
| c1a13d71f7 | |||
| aa5d55124d | |||
| 936f2e2fe3 | |||
| c4aac9740f | |||
| cd714ae430 | |||
| 46d0b2009e | |||
| 475ef1c7bf | |||
| 49baa96bcc | |||
| c4a1387460 | |||
| 98a9e2012a | |||
| de747811d5 | |||
| 9d0823721d | |||
| dd4515fd5b | |||
| aa00260c17 | |||
| 912a7aa6ae | |||
| f5006e629e | |||
| 93ddf8359f | |||
| 01ec0b54d3 | |||
| 5cd0d2b3e6 | |||
| c4b2e11f68 | |||
| 6c703f616e | |||
| 6577fe4396 | |||
| 1164e1e66c | |||
| 1cc0eaa3ad |
3
.github/workflows/last-pull.yml
vendored
3
.github/workflows/last-pull.yml
vendored
@@ -2,7 +2,7 @@ name: Last Pull for Yesterday
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
- cron: '0 10 * * *'
|
||||
env:
|
||||
TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }}
|
||||
TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
|
||||
@@ -13,6 +13,7 @@ env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_PORT: ${{ secrets.DB_PORT }}
|
||||
TOKEN_FILE: ${{ secrets.TOKEN_FILE }}
|
||||
jobs:
|
||||
pull-updates:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
3
.github/workflows/update.yml
vendored
3
.github/workflows/update.yml
vendored
@@ -2,7 +2,8 @@ name: Update Scores
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 4,8,10,16,18 * * *'
|
||||
- cron: '0 11-23/2 * * *'
|
||||
- cron: '0 0-4/2 * * *'
|
||||
env:
|
||||
TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }}
|
||||
TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
token.json
|
||||
wordle.db
|
||||
cache/
|
||||
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM python:3.10
|
||||
|
||||
RUN apt update && apt install libpq-dev
|
||||
|
||||
COPY pyproject.toml pyproject.toml
|
||||
COPY poetry.lock poetry.lock
|
||||
|
||||
RUN python -m pip install poetry && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install
|
||||
|
||||
|
||||
COPY wordlinator/ wordlinator/
|
||||
|
||||
EXPOSE 8050
|
||||
|
||||
CMD gunicorn --workers 8 -b "0.0.0.0:8050" "wordlinator.web:server"
|
||||
11
docker.sh
Executable file
11
docker.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker build -t wordlinator:latest .
|
||||
|
||||
if [ "$1" == "--debug" ]; then
|
||||
shift
|
||||
echo "Running debug mode..."
|
||||
docker run -d --rm -p 8050:8050 -e DASH_DEBUG=true -e DB_PORT -e DB_HOST -e DB_PASS "$@" --name wordlinator wordlinator:latest
|
||||
else
|
||||
docker run -d --rm -p 8050:8050 -e DB_PORT -e DB_HOST -e DB_PASS "$@" --name wordlinator wordlinator:latest
|
||||
fi
|
||||
4
docker_nas.sh
Executable file
4
docker_nas.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
DOCKER_CONTEXT=nas docker ps | grep wordlinator && DOCKER_CONTEXT=nas docker stop wordlinator || true
|
||||
DOCKER_CONTEXT=nas DB_PORT=49420 DB_HOST="localhost" ./docker.sh "$@" --network=host
|
||||
179
poetry.lock
generated
179
poetry.lock
generated
@@ -227,6 +227,25 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "dill"
|
||||
version = "0.3.5.1"
|
||||
description = "serialize all of python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
|
||||
[package.extras]
|
||||
graph = ["objgraph (>=1.7.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "diskcache"
|
||||
version = "5.4.0"
|
||||
description = "Disk Cache -- Disk and file backed persistent cache."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3"
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "0.8.3"
|
||||
@@ -565,6 +584,17 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "multiprocess"
|
||||
version = "0.70.13"
|
||||
description = "better multiprocessing and multithreading in python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
|
||||
|
||||
[package.dependencies]
|
||||
dill = ">=0.3.5.1"
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "0.960"
|
||||
@@ -694,7 +724,18 @@ optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
name = "psutil"
|
||||
version = "5.9.1"
|
||||
description = "Cross-platform lib for process and system monitoring in Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.extras]
|
||||
test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2"
|
||||
version = "2.9.3"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
category = "main"
|
||||
@@ -979,7 +1020,7 @@ watchdog = ["watchdog"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "dc9abe945b8d4f6344adf01af71c979dca1d0f9ad9def03496a9868823d885f8"
|
||||
content-hash = "dfea9fcb2c9b4d8e4fc586969ffe408dc992dcb97f1fd2d37d1cac8404eea5fa"
|
||||
|
||||
[metadata.files]
|
||||
anyio = [
|
||||
@@ -1193,6 +1234,14 @@ decorator = [
|
||||
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
|
||||
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||
]
|
||||
dill = [
|
||||
{file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"},
|
||||
{file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"},
|
||||
]
|
||||
diskcache = [
|
||||
{file = "diskcache-5.4.0-py3-none-any.whl", hash = "sha256:af3ec6d7f167bbef7b6c33d9ee22f86d3e8f2dd7131eb7c4703d8d91ccdc0cc4"},
|
||||
{file = "diskcache-5.4.0.tar.gz", hash = "sha256:8879eb8c9b4a2509a5e633d2008634fb2b0b35c2b36192d89655dbde02419644"},
|
||||
]
|
||||
executing = [
|
||||
{file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"},
|
||||
{file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"},
|
||||
@@ -1327,6 +1376,29 @@ mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
multiprocess = [
|
||||
{file = "multiprocess-0.70.13-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:b9a3be43ecee6776a9e7223af96914a0164f306affcf4624b213885172236b77"},
|
||||
{file = "multiprocess-0.70.13-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7e6a689da3490412caa7b3e27c3385d8aaa49135f3a353ace94ca47e4c926d37"},
|
||||
{file = "multiprocess-0.70.13-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:17cb4229aa43e6973679d67c66a454cbf8b6b0d038425cba3220ea5a06d61b58"},
|
||||
{file = "multiprocess-0.70.13-cp27-cp27m-win32.whl", hash = "sha256:99bb68dd0d5b3d30fe104721bee26e4637667112d5951b51feb81479fd560876"},
|
||||
{file = "multiprocess-0.70.13-cp27-cp27m-win_amd64.whl", hash = "sha256:6cdde49defcb933062df382ebc9b5299beebcd157a98b3a65291c1c94a2edc41"},
|
||||
{file = "multiprocess-0.70.13-pp27-pypy_73-macosx_10_7_x86_64.whl", hash = "sha256:92003c247436f8699b7692e95346a238446710f078500eb364bc23bb0503dd4f"},
|
||||
{file = "multiprocess-0.70.13-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:3ec1c8015e19182bfa01b5887a9c25805c48df3c71863f48fe83803147cde5d6"},
|
||||
{file = "multiprocess-0.70.13-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7415f61bddfffdade73396904551be8124a4a363322aa9c72d42e349c5fca39"},
|
||||
{file = "multiprocess-0.70.13-pp37-pypy37_pp73-manylinux_2_24_i686.whl", hash = "sha256:5436d1cd9f901f7ddc4f20b6fd0b462c87dcc00d941cc13eeb2401fc5bd00e42"},
|
||||
{file = "multiprocess-0.70.13-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:34e9703bd5b9fee5455c93a74e44dbabe55481c214d03be1e65f037be9d0c520"},
|
||||
{file = "multiprocess-0.70.13-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af0a48440aa8f793d8bb100f20102c12f192de5a608638819a998f2cc59e1fcd"},
|
||||
{file = "multiprocess-0.70.13-pp38-pypy38_pp73-manylinux_2_24_i686.whl", hash = "sha256:c4a97216e8319039c69a266252cc68a392b96f9e67e3ed02ad88be9e6f2d2969"},
|
||||
{file = "multiprocess-0.70.13-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:48315eefe02c35dd7560da3fa8af66d9f4a61b9dc8f7c40801c5f972ab4604b1"},
|
||||
{file = "multiprocess-0.70.13-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a6dca5f29f0224c855d0d5cad963476175cfc8de112d3eebe85914cb735f130"},
|
||||
{file = "multiprocess-0.70.13-pp39-pypy39_pp73-manylinux_2_24_i686.whl", hash = "sha256:5974bdad390ba466cc130288d2ef1048fdafedd01cf4641fc024f6088af70bfe"},
|
||||
{file = "multiprocess-0.70.13-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:01c1137d2f18d0cd262d0fdb7294b1fe9fc3e8dc8b126e506085434ae8eb3677"},
|
||||
{file = "multiprocess-0.70.13-py310-none-any.whl", hash = "sha256:0f4faf4811019efdb2f91db09240f893ee40cbfcb06978f3b8ed8c248e73babe"},
|
||||
{file = "multiprocess-0.70.13-py37-none-any.whl", hash = "sha256:62e556a0c31ec7176e28aa331663ac26c276ee3536b5e9bb5e850681e7a00f11"},
|
||||
{file = "multiprocess-0.70.13-py38-none-any.whl", hash = "sha256:7be9e320a41d2d0d0eddacfe693cfb07b4cb9c0d3d10007f4304255c15215778"},
|
||||
{file = "multiprocess-0.70.13-py39-none-any.whl", hash = "sha256:00ef48461d43d1e30f8f4b2e1b287ecaaffec325a37053beb5503e0d69e5a3cd"},
|
||||
{file = "multiprocess-0.70.13.tar.gz", hash = "sha256:2e096dd618a84d15aa369a9cf6695815e5539f853dc8fa4f4b9153b11b1d0b32"},
|
||||
]
|
||||
mypy = [
|
||||
{file = "mypy-0.960-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3a3e525cd76c2c4f90f1449fd034ba21fcca68050ff7c8397bb7dd25dd8b8248"},
|
||||
{file = "mypy-0.960-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7a76dc4f91e92db119b1be293892df8379b08fd31795bb44e0ff84256d34c251"},
|
||||
@@ -1417,63 +1489,52 @@ protobuf = [
|
||||
{file = "protobuf-3.20.1-py2.py3-none-any.whl", hash = "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388"},
|
||||
{file = "protobuf-3.20.1.tar.gz", hash = "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9"},
|
||||
]
|
||||
psycopg2-binary = [
|
||||
{file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
|
||||
psutil = [
|
||||
{file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"},
|
||||
{file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"},
|
||||
{file = "psutil-5.9.1-cp27-cp27m-win32.whl", hash = "sha256:0904727e0b0a038830b019551cf3204dd48ef5c6868adc776e06e93d615fc5fc"},
|
||||
{file = "psutil-5.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e7e10454cb1ab62cc6ce776e1c135a64045a11ec4c6d254d3f7689c16eb3efd2"},
|
||||
{file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:56960b9e8edcca1456f8c86a196f0c3d8e3e361320071c93378d41445ffd28b0"},
|
||||
{file = "psutil-5.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:44d1826150d49ffd62035785a9e2c56afcea66e55b43b8b630d7706276e87f22"},
|
||||
{file = "psutil-5.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7be9d7f5b0d206f0bbc3794b8e16fb7dbc53ec9e40bbe8787c6f2d38efcf6c9"},
|
||||
{file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd9246e4cdd5b554a2ddd97c157e292ac11ef3e7af25ac56b08b455c829dca8"},
|
||||
{file = "psutil-5.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29a442e25fab1f4d05e2655bb1b8ab6887981838d22effa2396d584b740194de"},
|
||||
{file = "psutil-5.9.1-cp310-cp310-win32.whl", hash = "sha256:20b27771b077dcaa0de1de3ad52d22538fe101f9946d6dc7869e6f694f079329"},
|
||||
{file = "psutil-5.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:58678bbadae12e0db55186dc58f2888839228ac9f41cc7848853539b70490021"},
|
||||
{file = "psutil-5.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3a76ad658641172d9c6e593de6fe248ddde825b5866464c3b2ee26c35da9d237"},
|
||||
{file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6a11e48cb93a5fa606306493f439b4aa7c56cb03fc9ace7f6bfa21aaf07c453"},
|
||||
{file = "psutil-5.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068935df39055bf27a29824b95c801c7a5130f118b806eee663cad28dca97685"},
|
||||
{file = "psutil-5.9.1-cp36-cp36m-win32.whl", hash = "sha256:0f15a19a05f39a09327345bc279c1ba4a8cfb0172cc0d3c7f7d16c813b2e7d36"},
|
||||
{file = "psutil-5.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:db417f0865f90bdc07fa30e1aadc69b6f4cad7f86324b02aa842034efe8d8c4d"},
|
||||
{file = "psutil-5.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:91c7ff2a40c373d0cc9121d54bc5f31c4fa09c346528e6a08d1845bce5771ffc"},
|
||||
{file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fea896b54f3a4ae6f790ac1d017101252c93f6fe075d0e7571543510f11d2676"},
|
||||
{file = "psutil-5.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3054e923204b8e9c23a55b23b6df73a8089ae1d075cb0bf711d3e9da1724ded4"},
|
||||
{file = "psutil-5.9.1-cp37-cp37m-win32.whl", hash = "sha256:d2d006286fbcb60f0b391741f520862e9b69f4019b4d738a2a45728c7e952f1b"},
|
||||
{file = "psutil-5.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b14ee12da9338f5e5b3a3ef7ca58b3cba30f5b66f7662159762932e6d0b8f680"},
|
||||
{file = "psutil-5.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f36c16012ba9cfc742604df189f2f28d2720e23ff7d1e81602dbe066be9fd1"},
|
||||
{file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:944c4b4b82dc4a1b805329c980f270f170fdc9945464223f2ec8e57563139cf4"},
|
||||
{file = "psutil-5.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6750a73a9c4a4e689490ccb862d53c7b976a2a35c4e1846d049dcc3f17d83b"},
|
||||
{file = "psutil-5.9.1-cp38-cp38-win32.whl", hash = "sha256:a8746bfe4e8f659528c5c7e9af5090c5a7d252f32b2e859c584ef7d8efb1e689"},
|
||||
{file = "psutil-5.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:79c9108d9aa7fa6fba6e668b61b82facc067a6b81517cab34d07a84aa89f3df0"},
|
||||
{file = "psutil-5.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28976df6c64ddd6320d281128817f32c29b539a52bdae5e192537bc338a9ec81"},
|
||||
{file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b88f75005586131276634027f4219d06e0561292be8bd6bc7f2f00bdabd63c4e"},
|
||||
{file = "psutil-5.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645bd4f7bb5b8633803e0b6746ff1628724668681a434482546887d22c7a9537"},
|
||||
{file = "psutil-5.9.1-cp39-cp39-win32.whl", hash = "sha256:32c52611756096ae91f5d1499fe6c53b86f4a9ada147ee42db4991ba1520e574"},
|
||||
{file = "psutil-5.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f65f9a46d984b8cd9b3750c2bdb419b2996895b005aefa6cbaba9a143b1ce2c5"},
|
||||
{file = "psutil-5.9.1.tar.gz", hash = "sha256:57f1819b5d9e95cdfb0c881a8a5b7d542ed0b7c522d575706a80bedc848c8954"},
|
||||
]
|
||||
psycopg2 = [
|
||||
{file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"},
|
||||
{file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"},
|
||||
{file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"},
|
||||
{file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"},
|
||||
{file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"},
|
||||
{file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"},
|
||||
{file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"},
|
||||
{file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"},
|
||||
{file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"},
|
||||
{file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"},
|
||||
{file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"},
|
||||
]
|
||||
ptyprocess = [
|
||||
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||
|
||||
@@ -15,9 +15,12 @@ python-dateutil = "^2.8.2"
|
||||
httpx = "^0.23.0"
|
||||
Authlib = "^1.0.1"
|
||||
peewee = "^3.14.10"
|
||||
psycopg2-binary = "^2.9.3"
|
||||
dash = "^2.4.1"
|
||||
gunicorn = "^20.1.0"
|
||||
psycopg2 = "^2.9.3"
|
||||
diskcache = "^5.4.0"
|
||||
multiprocess = "^0.70.13"
|
||||
psutil = "^5.9.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^22.3.0"
|
||||
@@ -33,6 +36,10 @@ show-user = "wordlinator.app:sync_show_user"
|
||||
show-missing = "wordlinator.app:sync_show_missing"
|
||||
tweet-missing = "wordlinator.app:sync_tweet_missing"
|
||||
db-load = "wordlinator.app:load_db_scores"
|
||||
add-user = "wordlinator.app:sync_add_user"
|
||||
create-round = "wordlinator.app:create_round"
|
||||
copy-users = "wordlinator.app:copy_users"
|
||||
gs-user-sync = "wordlinator.app:sync_gsheet_users"
|
||||
|
||||
[tool.mypy]
|
||||
ignore_missing_imports = true
|
||||
|
||||
6
serve.sh
6
serve.sh
@@ -1,3 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
poetry run gunicorn -b "127.0.0.1:8050" "wordlinator.web:server"
|
||||
RELOAD=""
|
||||
if $DEBUG; then
|
||||
RELOAD="--reload"
|
||||
fi
|
||||
poetry run gunicorn $RELOAD -b "0.0.0.0:8050" "wordlinator.web:server"
|
||||
|
||||
@@ -9,6 +9,7 @@ import wordlinator.db.pg
|
||||
import wordlinator.sheets
|
||||
import wordlinator.twitter
|
||||
import wordlinator.utils
|
||||
import wordlinator.utils.scores
|
||||
|
||||
|
||||
async def get_scores(
|
||||
@@ -69,22 +70,57 @@ def print_score_table(wordle_day, scores):
|
||||
print_missing_names(wordle_day, scoreless_names)
|
||||
|
||||
|
||||
def _save_db_scores(wordle_day: wordlinator.utils.WordleDay, scores: dict):
|
||||
def _save_db_scores(
|
||||
wordle_day: wordlinator.utils.WordleDay, scores: dict, twitter_scores=None
|
||||
):
|
||||
twitter_scores = twitter_scores or {}
|
||||
db = wordlinator.db.pg.WordleDb()
|
||||
hole_data = wordle_day.golf_hole
|
||||
if not hole_data:
|
||||
return
|
||||
game_no = hole_data.game_no
|
||||
|
||||
db_users = db.get_users()
|
||||
db_holes = db.get_holes(game_no, ensure_all=True)
|
||||
db_scores = db.get_scores(game_no)
|
||||
|
||||
db_scores_by_user = wordlinator.utils.scores.ScoreMatrix(db_scores).by_user(
|
||||
usernames=list(scores.keys())
|
||||
)
|
||||
|
||||
to_update = []
|
||||
to_create = []
|
||||
|
||||
for user, score_list in scores.items():
|
||||
if not db.get_user(user):
|
||||
db_user_scores = db_scores_by_user.get(user)
|
||||
if not db_user_scores:
|
||||
continue
|
||||
for day, score_entry in enumerate(score_list, start=1):
|
||||
db.add_score(user, game_no, day, score_entry)
|
||||
db_user_match = [u for u in db_users if u.username == user]
|
||||
if not db_user_match:
|
||||
rich.print(f"[yellow]User {user} not in database, cannot add scores.")
|
||||
continue
|
||||
db_user = db_user_match[0]
|
||||
twitter_score = twitter_scores.get(user, None)
|
||||
changes = db_user_scores.get_changes(
|
||||
score_list, twitter_score, db_user, db_holes
|
||||
)
|
||||
to_update.extend(changes["update"])
|
||||
to_create.extend(changes["create"])
|
||||
|
||||
if to_update:
|
||||
db.bulk_update_scores(to_update)
|
||||
|
||||
if to_create:
|
||||
db.bulk_insert_scores(to_create)
|
||||
return
|
||||
|
||||
|
||||
async def main_update(
|
||||
wordle_day: wordlinator.utils.WordleDay = wordlinator.utils.WORDLE_TODAY,
|
||||
):
|
||||
if not wordle_day.golf_hole:
|
||||
rich.print(f"[yellow]{wordle_day.date} isn't a #WordleGolf day!")
|
||||
exit()
|
||||
sheets_client = wordlinator.sheets.SheetsClient(wordle_day=wordle_day)
|
||||
|
||||
today_scores = await get_scores(wordle_day=wordle_day)
|
||||
@@ -95,7 +131,7 @@ async def main_update(
|
||||
updated_scores = sheets_client.update_scores(today_scores)
|
||||
|
||||
rich.print("[green]Saving scores in db...")
|
||||
_save_db_scores(wordle_day, updated_scores)
|
||||
_save_db_scores(wordle_day, updated_scores, today_scores)
|
||||
|
||||
print_score_table(wordle_day, today_scores)
|
||||
|
||||
@@ -136,6 +172,11 @@ def _get_day():
|
||||
days.add_argument(
|
||||
"--wordle-day", type=int, help="The wordle day number for the score report."
|
||||
)
|
||||
days.add_argument(
|
||||
"--date",
|
||||
type=wordlinator.utils.date_from_string,
|
||||
help="a YYYY-MM-DD format date to pull a score report.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
wordle_day = wordlinator.utils.WORDLE_TODAY
|
||||
if args.wordle_day:
|
||||
@@ -144,6 +185,8 @@ def _get_day():
|
||||
wordle_day = wordlinator.utils.WordleDay.from_wordle_no(
|
||||
wordle_day.wordle_no - args.days_ago
|
||||
)
|
||||
elif args.date:
|
||||
wordle_day = wordlinator.utils.WordleDay.from_date(args.date)
|
||||
return wordle_day
|
||||
|
||||
|
||||
@@ -154,6 +197,115 @@ def load_db_scores():
|
||||
_save_db_scores(wordle_day, scores)
|
||||
|
||||
|
||||
def _add_user_args():
|
||||
parser = argparse.ArgumentParser("add-user")
|
||||
parser.add_argument("username", help="The user twitter handle or name.")
|
||||
parser.add_argument(
|
||||
"--no-check-twitter",
|
||||
dest="check_twitter",
|
||||
action="store_false",
|
||||
default=True,
|
||||
help="don't check Twitter for user's scores",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g", "--games", nargs="*", help="The game/round number(s) to enroll the user."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--unenroll-games",
|
||||
nargs="*",
|
||||
help="Game/round number(s) to unenroll the user.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
async def add_user(username, games=None, unenroll_games=None, check_twitter=True):
|
||||
db = wordlinator.db.pg.WordleDb()
|
||||
|
||||
user = db.get_user(username)
|
||||
if not user:
|
||||
rich.print(f"[green]Creating user {username}")
|
||||
user_id = None
|
||||
if check_twitter:
|
||||
twitter = wordlinator.twitter.TwitterClient()
|
||||
user_id = await twitter.get_user_twitter_id(username)
|
||||
if not user_id:
|
||||
check_twitter = False
|
||||
rich.print(
|
||||
f"[yellow]No twitter ID found for {username}, "
|
||||
"disabling twitter check"
|
||||
)
|
||||
user_id = user_id or f"{username}-NA"
|
||||
user = db.add_user(username, user_id, check_twitter=check_twitter)
|
||||
for round in games or []:
|
||||
rich.print(f"[green]Adding {username} to round {round}")
|
||||
db.add_user_to_round(username, round)
|
||||
for round in unenroll_games or []:
|
||||
rich.print(f"[green]Removing {username} from round {round}")
|
||||
db.remove_user_from_round(username, round)
|
||||
|
||||
|
||||
def create_round():
|
||||
parser = argparse.ArgumentParser("create-round")
|
||||
parser.add_argument("round_no", type=int, help="The round number to create.")
|
||||
parser.add_argument(
|
||||
"start_date",
|
||||
type=wordlinator.utils.date_from_string,
|
||||
help="the YYYY-mm-DD format date the round starts.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
db = wordlinator.db.pg.WordleDb()
|
||||
db.get_or_create_round(args.round_no, args.start_date)
|
||||
db.create_round_holes(args.round_no)
|
||||
|
||||
|
||||
def copy_users():
|
||||
parser = argparse.ArgumentParser("copy-users")
|
||||
parser.add_argument("from_round", type=int, help="The source round number.")
|
||||
parser.add_argument("to_round", type=int, help="The destination round.")
|
||||
args = parser.parse_args()
|
||||
|
||||
wordlinator.db.pg.WordleDb().copy_players_from_round(args.from_round, args.to_round)
|
||||
|
||||
|
||||
async def pull_gsheets_users(round_no):
|
||||
db = wordlinator.db.pg.WordleDb()
|
||||
db_users = db.get_users_by_round(round_no=round_no)
|
||||
|
||||
round = db.get_or_create_round(round_no)
|
||||
wordle_day = wordlinator.utils.WordleDay.from_date(round.end_date)
|
||||
|
||||
sheets = wordlinator.sheets.SheetsClient(wordle_day=wordle_day)
|
||||
sheets_users = sheets.get_users()
|
||||
|
||||
for user in sheets_users:
|
||||
db_match = [u for u in db_users if u.username == user]
|
||||
if not db_match:
|
||||
rich.print(f"[yellow]Adding {user} to Round {round_no}")
|
||||
await add_user(user, games=[round_no])
|
||||
|
||||
for user in db_users:
|
||||
if user.username not in sheets_users:
|
||||
rich.print(f"[yellow]Removing {user.username} from Round {round_no}")
|
||||
db.remove_user_from_round(user.username, round_no)
|
||||
|
||||
|
||||
def sync_gsheet_users():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("round_no", help="The round to sync.")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(pull_gsheets_users(args.round_no))
|
||||
|
||||
|
||||
def sync_add_user():
|
||||
args = _add_user_args()
|
||||
asyncio.run(
|
||||
add_user(args.username, args.games, args.unenroll_games, args.check_twitter)
|
||||
)
|
||||
|
||||
|
||||
def sync_main():
|
||||
wordle_day = _get_day()
|
||||
asyncio.run(main(wordle_day=wordle_day))
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import datetime
|
||||
import os
|
||||
import typing
|
||||
|
||||
import peewee
|
||||
|
||||
@@ -22,6 +24,9 @@ class User(BaseModel):
|
||||
twitter_id = peewee.CharField(unique=True)
|
||||
check_twitter = peewee.BooleanField(default=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.username}, Check Twitter: {self.check_twitter}>"
|
||||
|
||||
class Meta:
|
||||
table_name = "user_tbl"
|
||||
|
||||
@@ -29,6 +34,22 @@ class User(BaseModel):
|
||||
class Game(BaseModel):
|
||||
game_id = peewee.AutoField()
|
||||
game = peewee.IntegerField(null=False)
|
||||
start_date = peewee.DateField(null=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Game: Round {self.game}, Start {self.start_date}>"
|
||||
|
||||
@property
|
||||
def end_date(self):
|
||||
return self.start_date + datetime.timedelta(days=17)
|
||||
|
||||
|
||||
class Player(BaseModel):
|
||||
user_id = peewee.ForeignKeyField(User, "user_id", null=False)
|
||||
game_id = peewee.ForeignKeyField(Game, "game_id", null=False)
|
||||
|
||||
class Meta:
|
||||
primary_key = peewee.CompositeKey("user_id", "game_id")
|
||||
|
||||
|
||||
class Hole(BaseModel):
|
||||
@@ -36,12 +57,16 @@ class Hole(BaseModel):
|
||||
hole = peewee.IntegerField(null=False)
|
||||
game_id = peewee.ForeignKeyField(Game, "game_id", null=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Hole #{self.hole}>"
|
||||
|
||||
|
||||
class Score(BaseModel):
|
||||
score = peewee.IntegerField(null=False)
|
||||
user_id = peewee.ForeignKeyField(User, "user_id", null=False)
|
||||
game_id = peewee.ForeignKeyField(Game, "game_id", null=False)
|
||||
hole_id = peewee.ForeignKeyField(Hole, "hole_id", null=False)
|
||||
tweet_id = peewee.CharField(max_length=255, null=True)
|
||||
|
||||
class Meta:
|
||||
primary_key = peewee.CompositeKey("user_id", "game_id", "hole_id")
|
||||
@@ -54,57 +79,198 @@ class WordleDb:
|
||||
except peewee.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_users(self):
|
||||
return list(User.select())
|
||||
|
||||
def get_users_by_round(self, round_no=None, round_id=None):
|
||||
with db.atomic():
|
||||
query = (
|
||||
User.select(User, Player.user_id, Game.game)
|
||||
.join(Player, on=(Player.user_id == User.user_id))
|
||||
.join(Game, on=(Game.game_id == Player.game_id))
|
||||
)
|
||||
if round_no:
|
||||
query = query.filter(Game.game == round_no)
|
||||
elif round_id:
|
||||
query = query.filter(Game.game_id == round_id)
|
||||
return list(query)
|
||||
|
||||
def get_user_id(self, username):
|
||||
user = self.get_user(username)
|
||||
return user.twitter_id if user else None
|
||||
with db.atomic():
|
||||
user = self.get_user(username)
|
||||
return user.twitter_id if user else None
|
||||
|
||||
def add_user(self, username, user_id):
|
||||
return User.create(username=username, twitter_id=user_id)
|
||||
def add_user(self, username, user_id, check_twitter=True):
|
||||
with db.atomic():
|
||||
return User.create(
|
||||
username=username, twitter_id=user_id, check_twitter=check_twitter
|
||||
)
|
||||
|
||||
def get_or_create_round(self, round_no):
|
||||
try:
|
||||
return Game.get(Game.game == round_no)
|
||||
except peewee.DoesNotExist:
|
||||
return Game.create(game=round_no)
|
||||
def get_rounds(self):
|
||||
with db.atomic():
|
||||
return list(sorted(Game.select(), key=lambda d: d.start_date))
|
||||
|
||||
def get_or_create_round(self, round_no, start_date=None):
|
||||
with db.atomic():
|
||||
try:
|
||||
return Game.get(Game.game == round_no)
|
||||
except peewee.DoesNotExist:
|
||||
if not start_date:
|
||||
raise ValueError(
|
||||
f"Round {round_no} does not exist, "
|
||||
"and no start_date provide to create it"
|
||||
)
|
||||
return Game.create(game=round_no, start_date=start_date)
|
||||
|
||||
def get_or_create_hole(self, round_no, hole_no):
|
||||
round = self.get_or_create_round(round_no)
|
||||
with db.atomic():
|
||||
round = self.get_or_create_round(round_no)
|
||||
|
||||
try:
|
||||
return Hole.get(Hole.hole == hole_no, Hole.game_id == round.game_id)
|
||||
except peewee.DoesNotExist:
|
||||
return Hole.create(hole=hole_no, game_id=round.game_id)
|
||||
try:
|
||||
return Hole.get(Hole.hole == hole_no, Hole.game_id == round.game_id)
|
||||
except peewee.DoesNotExist:
|
||||
return Hole.create(hole=hole_no, game_id=round.game_id)
|
||||
|
||||
def get_holes(self, round_no, ensure_all=False):
|
||||
with db.atomic():
|
||||
round = self.get_or_create_round(round_no)
|
||||
if ensure_all:
|
||||
self.create_round_holes(round_no)
|
||||
return list(Hole.select().filter(game_id=round.game_id))
|
||||
|
||||
def create_round_holes(self, round_no):
|
||||
for hole_no in range(1, 19):
|
||||
self.get_or_create_hole(round_no, hole_no)
|
||||
with db.atomic():
|
||||
for hole_no in range(1, 19):
|
||||
self.get_or_create_hole(round_no, hole_no)
|
||||
|
||||
def get_or_create_player_round(self, user_id, game_id):
|
||||
with db.atomic():
|
||||
try:
|
||||
return Player.get(Player.user_id == user_id, Player.game_id == game_id)
|
||||
except peewee.DoesNotExist:
|
||||
return Player.create(user_id=user_id, game_id=game_id)
|
||||
|
||||
def add_user_to_round(self, username, round_no):
|
||||
with db.atomic():
|
||||
user = self.get_user(username)
|
||||
if not user:
|
||||
raise ValueError(f"No user found with username {username}")
|
||||
round = self.get_or_create_round(round_no)
|
||||
return self.get_or_create_player_round(user.user_id, round.game_id)
|
||||
|
||||
def remove_user_from_round(self, username, round_no):
|
||||
with db.atomic():
|
||||
user = self.get_user(username)
|
||||
if not user:
|
||||
raise ValueError(f"No user found with username {username}")
|
||||
round = self.get_or_create_round(round_no)
|
||||
try:
|
||||
player = Player.get(user_id=user.user_id, game_id=round.game_id)
|
||||
player.delete_instance()
|
||||
except peewee.DoesNotExist:
|
||||
return
|
||||
|
||||
def copy_players_from_round(self, from_round_no, to_round_no):
|
||||
with db.atomic():
|
||||
to_round = self.get_or_create_round(to_round_no)
|
||||
|
||||
for user in self.get_users_by_round(from_round_no):
|
||||
self.get_or_create_player_round(user.user_id, to_round.game_id)
|
||||
|
||||
def add_score(self, username, game, hole, score):
|
||||
if not score:
|
||||
return
|
||||
with db.atomic():
|
||||
if not score:
|
||||
return
|
||||
|
||||
user = self.get_user(username)
|
||||
if not user:
|
||||
raise ValueError(f'No Such User "{username}"')
|
||||
hole = self.get_or_create_hole(game, hole)
|
||||
user = self.get_user(username)
|
||||
if not user:
|
||||
raise ValueError(f'No Such User "{username}"')
|
||||
hole = self.get_or_create_hole(game, hole)
|
||||
|
||||
try:
|
||||
score_obj = Score.get(
|
||||
Score.user_id == user.user_id,
|
||||
Score.game_id == hole.game_id,
|
||||
Score.hole_id == hole.hole_id,
|
||||
)
|
||||
score_obj.score = score
|
||||
score_obj.save()
|
||||
return score_obj
|
||||
except peewee.DoesNotExist:
|
||||
return Score.create(
|
||||
score=score,
|
||||
user_id=user.user_id,
|
||||
game_id=hole.game_id,
|
||||
hole_id=hole.hole_id,
|
||||
try:
|
||||
score_obj = Score.get(
|
||||
Score.user_id == user.user_id,
|
||||
Score.game_id == hole.game_id,
|
||||
Score.hole_id == hole.hole_id,
|
||||
)
|
||||
score_obj.score = score
|
||||
score_obj.save()
|
||||
return score_obj
|
||||
except peewee.DoesNotExist:
|
||||
return Score.create(
|
||||
score=score,
|
||||
user_id=user.user_id,
|
||||
game_id=hole.game_id,
|
||||
hole_id=hole.hole_id,
|
||||
)
|
||||
|
||||
def get_scores(self, round_no=None, round_id=None):
|
||||
with db.atomic():
|
||||
if round_no:
|
||||
round = self.get_or_create_round(round_no)
|
||||
elif round_id:
|
||||
round = Game.get_by_id(round_id)
|
||||
else:
|
||||
raise ValueError("Must provide Round Number or Round ID")
|
||||
res = (
|
||||
Score.select(
|
||||
Score,
|
||||
Hole.hole,
|
||||
Hole.hole_id,
|
||||
Game.game_id,
|
||||
User.username,
|
||||
User.user_id,
|
||||
Player.game_id,
|
||||
)
|
||||
.join(Player, on=(Score.user_id == Player.user_id))
|
||||
.switch(Score)
|
||||
.join(Hole, on=(Score.hole_id == Hole.hole_id))
|
||||
.join(Game, on=(Hole.game_id == Game.game_id))
|
||||
.switch(Score)
|
||||
.join(User, on=(Score.user_id == User.user_id))
|
||||
.filter(Player.game_id == round.game_id)
|
||||
.filter(Score.game_id == round.game_id)
|
||||
)
|
||||
return list(res) if res else []
|
||||
|
||||
def get_scores(self, round_no):
|
||||
round = self.get_or_create_round(round_no)
|
||||
return Score.select().filter(Score.game_id == round.game_id)
|
||||
def bulk_insert_scores(self, scores: typing.List[typing.Dict]):
|
||||
with db.atomic() as txn:
|
||||
for batch in peewee.chunked(scores, 50):
|
||||
Score.insert_many(batch).execute()
|
||||
|
||||
def bulk_update_scores(self, scores: typing.List[Score]):
|
||||
query_str = """UPDATE score
|
||||
SET score = {score}, tweet_id = {tweet_id}
|
||||
WHERE user_id = {user_id} AND game_id = {game_id} AND hole_id = {hole_id}"""
|
||||
for score in scores:
|
||||
query = query_str.format(
|
||||
score=score.score,
|
||||
tweet_id=score.tweet_id or "NULL",
|
||||
user_id=score.user_id.user_id,
|
||||
game_id=score.game_id.game_id,
|
||||
hole_id=score.hole_id.hole_id,
|
||||
)
|
||||
db.execute_sql(query)
|
||||
|
||||
def get_users_without_score(self, round_no, hole_no, tweetable=True):
|
||||
hole = self.get_or_create_hole(round_no, hole_no)
|
||||
# Find users who *have* played in this round,
|
||||
# but have no score on the current hole
|
||||
query_str = """SELECT u.username, player.game_id
|
||||
FROM user_tbl u
|
||||
JOIN player ON player.user_id = u.user_id
|
||||
WHERE (
|
||||
player.game_id = {}
|
||||
) AND NOT EXISTS (
|
||||
SELECT FROM score WHERE score.user_id = u.user_id AND score.hole_id = {}
|
||||
)
|
||||
""".format(
|
||||
hole.game_id, hole.hole_id
|
||||
)
|
||||
|
||||
if tweetable:
|
||||
# Restrict to users who post scores on twitter
|
||||
query_str += " AND u.check_twitter = true"
|
||||
|
||||
res = db.execute_sql(query_str)
|
||||
return [r[0] for r in res]
|
||||
|
||||
@@ -17,7 +17,15 @@ import wordlinator.utils
|
||||
|
||||
BASE_URL = "https://api.twitter.com/2"
|
||||
WORDLE_RE = re.compile(
|
||||
r"Wordle(\w+)? (?P<number>\d+) (?P<score>[X\d])/6", re.IGNORECASE
|
||||
r"""
|
||||
Wordle # Line Leader
|
||||
(\w+)? # etta factor (catches ...Golf, etc)
|
||||
\s # single space
|
||||
(?P<number>\d+) # game number
|
||||
\s # single space
|
||||
(?P<score>[X\d])/6 # score out of 6
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
TOKEN = os.getenv("TWITTER_TOKEN")
|
||||
|
||||
@@ -60,6 +68,7 @@ class WordleTweet:
|
||||
wordle_day: wordlinator.utils.WordleDay
|
||||
raw_score: int
|
||||
user: TwitterUser
|
||||
tweet_id: str
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
@@ -86,6 +95,7 @@ class WordleTweet:
|
||||
return cls(
|
||||
created_at=created,
|
||||
text=tweet["text"],
|
||||
tweet_id=tweet["id"],
|
||||
wordle_day=wordlinator.utils.WordleDay.from_wordle_no(wordle_no),
|
||||
raw_score=score,
|
||||
user=twitter_user,
|
||||
@@ -131,17 +141,22 @@ class TwitterClient(httpx.AsyncClient):
|
||||
async def get_user_by(self, username: str):
|
||||
return await self.get(self.USER_PATH.format(username=username))
|
||||
|
||||
async def get_user_twitter_id(self, username: str):
|
||||
user_id = None
|
||||
twitter_user = await self.get_user_by(username)
|
||||
if twitter_user.is_success:
|
||||
user_id = twitter_user.json().get("data", {}).get("id", None)
|
||||
return user_id
|
||||
|
||||
async def get_user_id(self, username: str):
|
||||
db_user = self.db.get_user(username)
|
||||
if db_user:
|
||||
return db_user.twitter_id if db_user.check_twitter else False
|
||||
else:
|
||||
twitter_user = await self.get_user_by(username)
|
||||
user_id = None
|
||||
if twitter_user.is_success:
|
||||
user_id = twitter_user.json().get("data", {}).get("id", None)
|
||||
user_id = await self.get_user_twitter_id(username)
|
||||
if user_id:
|
||||
self.db.add_user(username, user_id)
|
||||
self.db.add_user_to_round(username, self.wordle_day.golf_hole.game_no)
|
||||
return user_id
|
||||
|
||||
def _start_timestamp(self):
|
||||
@@ -196,17 +211,28 @@ class TwitterClient(httpx.AsyncClient):
|
||||
async def get_wordlegolf_tweets(self):
|
||||
return self._build_wordle_tweets(await self.search_tweets("#WordleGolf"))
|
||||
|
||||
def open_tweet(self, msg):
|
||||
@classmethod
|
||||
def open_tweet(cls, msg):
|
||||
param = urllib.parse.urlencode({"text": msg})
|
||||
webbrowser.open(f"{self.TWEET_INTENT_URL}?{param}")
|
||||
webbrowser.open(f"{cls.TWEET_INTENT_URL}?{param}")
|
||||
|
||||
async def notify_missing(self, names):
|
||||
@classmethod
|
||||
def full_notify_link(cls, names):
|
||||
header = "Still missing a few #WordleGolf Players today!"
|
||||
msg = header
|
||||
while names:
|
||||
while len(msg) < self.MAX_TWEET_LENGTH and names:
|
||||
msg += f" @{names.pop()}"
|
||||
param = urllib.parse.urlencode({"text": msg})
|
||||
return f"{cls.TWEET_INTENT_URL}?{param}"
|
||||
|
||||
@classmethod
|
||||
async def notify_missing(cls, names):
|
||||
header = "Still missing a few #WordleGolf Players today!"
|
||||
msg = header
|
||||
while names:
|
||||
while len(msg) < cls.MAX_TWEET_LENGTH and names:
|
||||
msg += f" @{names.pop()}"
|
||||
self.open_tweet(msg)
|
||||
cls.open_tweet(msg)
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
WORDLE_DAY_ZERO = datetime.date(2021, 6, 19)
|
||||
|
||||
WORDLE_GOLF_ROUND_DATES = [datetime.date(2022, 5, 9), datetime.date(2022, 5, 31)]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GolfHole:
|
||||
game_no: int
|
||||
hole_no: int
|
||||
|
||||
@classmethod
|
||||
def from_date(cls, date: datetime.date):
|
||||
for game_no, start_date in enumerate(WORDLE_GOLF_ROUND_DATES, start=1):
|
||||
hole_value = (date - start_date).days + 1
|
||||
if 1 <= hole_value <= 18:
|
||||
return cls(game_no=game_no, hole_no=hole_value)
|
||||
return None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class WordleDay:
|
||||
wordle_no: int
|
||||
date: datetime.date
|
||||
golf_hole: typing.Optional[GolfHole]
|
||||
|
||||
@classmethod
|
||||
def from_wordle_no(cls, wordle_no: int):
|
||||
wordle_no = int(wordle_no)
|
||||
date = WORDLE_DAY_ZERO + datetime.timedelta(days=wordle_no)
|
||||
golf_hole = GolfHole.from_date(date)
|
||||
return cls(wordle_no=wordle_no, date=date, golf_hole=golf_hole)
|
||||
|
||||
@classmethod
|
||||
def from_date(cls, date: datetime.date):
|
||||
wordle_no = (date - WORDLE_DAY_ZERO).days
|
||||
golf_hole = GolfHole.from_date(date)
|
||||
return cls(wordle_no=wordle_no, date=date, golf_hole=golf_hole)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.wordle_no == other.wordle_no
|
||||
|
||||
|
||||
WORDLE_TODAY = WordleDay.from_date(datetime.date.today())
|
||||
73
wordlinator/utils/__init__.py
Normal file
73
wordlinator/utils/__init__.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import argparse
|
||||
import dataclasses
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import wordlinator.db.pg
|
||||
|
||||
WORDLE_DAY_ZERO = datetime.date(2021, 6, 19)
|
||||
|
||||
WORDLE_GOLF_ROUNDS = wordlinator.db.pg.WordleDb().get_rounds()
|
||||
|
||||
|
||||
def date_from_string(datestr: str):
|
||||
try:
|
||||
return datetime.date.fromisoformat(datestr)
|
||||
except ValueError:
|
||||
msg = "Invalid date string, expected format: YYYY-mm-DD"
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GolfHole:
|
||||
game_no: int
|
||||
hole_no: int
|
||||
|
||||
@classmethod
|
||||
def from_date(cls, date: datetime.date):
|
||||
for round in WORDLE_GOLF_ROUNDS:
|
||||
if round.start_date <= date <= round.end_date:
|
||||
hole_no = (date - round.start_date).days + 1
|
||||
return cls(game_no=round.game, hole_no=hole_no)
|
||||
return None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class WordleDay:
|
||||
wordle_no: int
|
||||
date: datetime.date
|
||||
golf_hole: typing.Optional[GolfHole]
|
||||
|
||||
@classmethod
|
||||
def from_wordle_no(cls, wordle_no: int):
|
||||
wordle_no = int(wordle_no)
|
||||
date = WORDLE_DAY_ZERO + datetime.timedelta(days=wordle_no)
|
||||
golf_hole = GolfHole.from_date(date)
|
||||
return cls(wordle_no=wordle_no, date=date, golf_hole=golf_hole)
|
||||
|
||||
@classmethod
|
||||
def from_date(cls, date: datetime.date):
|
||||
wordle_no = (date - WORDLE_DAY_ZERO).days
|
||||
golf_hole = GolfHole.from_date(date)
|
||||
return cls(wordle_no=wordle_no, date=date, golf_hole=golf_hole)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.wordle_no == other.wordle_no
|
||||
|
||||
|
||||
# Designed so that "today" will be the current date in CST
|
||||
# Regardless of where the code is run
|
||||
def get_today_central():
|
||||
today = (
|
||||
datetime.datetime.now(datetime.timezone.utc)
|
||||
.astimezone(datetime.timezone(datetime.timedelta(hours=-5), name="US Central"))
|
||||
.date()
|
||||
)
|
||||
return today
|
||||
|
||||
|
||||
def get_wordle_today():
|
||||
return WordleDay.from_date(get_today_central())
|
||||
|
||||
|
||||
WORDLE_TODAY = get_wordle_today()
|
||||
220
wordlinator/utils/scores.py
Normal file
220
wordlinator/utils/scores.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import collections
|
||||
import itertools
|
||||
import typing
|
||||
|
||||
import wordlinator.db.pg
|
||||
import wordlinator.twitter
|
||||
|
||||
############
|
||||
# Mappings #
|
||||
############
|
||||
|
||||
SCORE_NAME_MAP = {
|
||||
1: "Hole-in-1",
|
||||
2: "Eagle",
|
||||
3: "Birdie",
|
||||
4: "Par",
|
||||
5: "Bogey",
|
||||
6: "Double Bogey",
|
||||
7: "Fail",
|
||||
}
|
||||
|
||||
|
||||
###############
|
||||
# ScoreMatrix #
|
||||
###############
|
||||
|
||||
T = typing.TypeVar("T", bound="ScoreContainer")
|
||||
Score = wordlinator.db.pg.Score
|
||||
User = wordlinator.db.pg.User
|
||||
Hole = wordlinator.db.pg.Hole
|
||||
WordleTweet = wordlinator.twitter.WordleTweet
|
||||
|
||||
|
||||
class ScoreContainer:
|
||||
def __init__(self, scores: typing.List[Score]):
|
||||
self._scores = scores
|
||||
|
||||
@staticmethod
|
||||
def _get_attribute(score: Score, attribute_path: typing.List[str]):
|
||||
attribute = score
|
||||
for path_part in attribute_path:
|
||||
attribute = getattr(attribute, path_part)
|
||||
return attribute
|
||||
|
||||
def dict_by(
|
||||
self, attribute_path: str, container_class: typing.Type[T]
|
||||
) -> typing.Dict[typing.Any, T]:
|
||||
data_dict = collections.defaultdict(list)
|
||||
path_parts = attribute_path.split(".")
|
||||
|
||||
for score in self._scores:
|
||||
data_dict[self._get_attribute(score, path_parts)].append(score)
|
||||
|
||||
return {k: container_class(v) for k, v in data_dict.items()}
|
||||
|
||||
|
||||
class ScoreRow(ScoreContainer):
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return sum(s.score for s in self._scores)
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self._scores)
|
||||
|
||||
@property
|
||||
def average(self) -> float:
|
||||
return round(self.total / len(self._scores), 2)
|
||||
|
||||
|
||||
class UserRow(ScoreRow):
|
||||
def __init__(self, scores, username=None):
|
||||
super().__init__(scores)
|
||||
self.username = username or scores[0].user_id.username
|
||||
|
||||
@property
|
||||
def golf_score(self) -> int:
|
||||
return self.total - (self.count * 4)
|
||||
|
||||
@property
|
||||
def progressive_score_list(self) -> typing.List[int]:
|
||||
score_progress = list(
|
||||
itertools.accumulate(
|
||||
self.sorted_scores(), func=lambda t, e: t + (e.score - 4), initial=0
|
||||
)
|
||||
)[1:]
|
||||
return score_progress
|
||||
|
||||
def sorted_scores(self):
|
||||
yield from sorted(self._scores, key=lambda s: s.hole_id.hole)
|
||||
|
||||
def raw_values(self):
|
||||
yield from (s.score for s in self.sorted_scores())
|
||||
|
||||
def _present_format(self, score):
|
||||
if score.tweet_id:
|
||||
return (
|
||||
f"[{score.score}]"
|
||||
f"(https://twitter.com/{self.username}/status/{score.tweet_id})"
|
||||
)
|
||||
return score.score
|
||||
|
||||
def presentation_values(self, hole_no=None):
|
||||
res = {s.hole_id.hole: self._present_format(s) for s in self.sorted_scores()}
|
||||
if hole_no:
|
||||
for i in range(1, hole_no + 1):
|
||||
if i not in res:
|
||||
res[i] = ""
|
||||
return res
|
||||
|
||||
def user_row(self, hole_no=None):
|
||||
return {
|
||||
"Name": self.username,
|
||||
"Score": self.golf_score,
|
||||
**self.presentation_values(hole_no=hole_no),
|
||||
}
|
||||
|
||||
def get_changes(
|
||||
self,
|
||||
sheets_scores: typing.List[int],
|
||||
twitter_score: typing.Optional[WordleTweet],
|
||||
db_user: User,
|
||||
db_holes: typing.List[Hole],
|
||||
) -> typing.Dict[str, typing.List[Score]]:
|
||||
current_scores = list(self.sorted_scores())
|
||||
|
||||
results: typing.Dict[str, typing.List[typing.Any]] = {
|
||||
"update": [],
|
||||
"create": [],
|
||||
}
|
||||
|
||||
for day, score in enumerate(sheets_scores, start=1):
|
||||
try:
|
||||
score = int(score)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
hole = [h for h in db_holes if h.hole == day][0]
|
||||
score_match = [s for s in current_scores if s.hole_id.hole == day]
|
||||
|
||||
tweet_id = None
|
||||
if twitter_score and twitter_score.wordle_day.golf_hole.hole_no == day:
|
||||
tweet_id = twitter_score.tweet_id
|
||||
|
||||
if not score_match:
|
||||
results["create"].append(
|
||||
{
|
||||
"score": score,
|
||||
"user_id": db_user.user_id,
|
||||
"game_id": hole.game_id.game_id,
|
||||
"hole_id": hole.hole_id,
|
||||
"tweet_id": tweet_id,
|
||||
}
|
||||
)
|
||||
else:
|
||||
saved_score = score_match[0]
|
||||
if saved_score.score != score or (
|
||||
tweet_id and saved_score.tweet_id != tweet_id
|
||||
):
|
||||
saved_score.score = score
|
||||
if tweet_id:
|
||||
saved_score.tweet_id = tweet_id
|
||||
results["update"].append(saved_score)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
class ScoreMatrix(ScoreContainer):
|
||||
def __init__(self, *args, usernames=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.usernames = usernames or []
|
||||
|
||||
def by_user(self, usernames: typing.List[str] = []):
|
||||
res = self.dict_by("user_id.username", UserRow)
|
||||
for username in usernames or self.usernames:
|
||||
if username not in res:
|
||||
res[username] = UserRow([], username)
|
||||
return res
|
||||
|
||||
def for_user(self, username):
|
||||
user_scores = [s for s in self._scores if s.user_id.username == username]
|
||||
return UserRow(scores=user_scores, username=username)
|
||||
|
||||
def by_hole(self):
|
||||
return self.dict_by("hole_id.hole", ScoreRow)
|
||||
|
||||
def for_hole(self, hole_no):
|
||||
hole_scores = [s for s in self._scores if s.hole_id.hole == hole_no]
|
||||
return ScoreRow(hole_scores)
|
||||
|
||||
def _level_counts(self, level_scores: "ScoreMatrix"):
|
||||
hole_dict = level_scores.by_hole()
|
||||
return {k: v.count for k, v in sorted(hole_dict.items())}
|
||||
|
||||
def score_breakdown(self):
|
||||
by_score_dict = self.dict_by("score", ScoreMatrix)
|
||||
return {
|
||||
SCORE_NAME_MAP[k]: self._level_counts(v)
|
||||
for k, v in sorted(by_score_dict.items())
|
||||
}
|
||||
|
||||
def user_rows(self, wordle_day):
|
||||
hole_no = wordle_day.golf_hole.hole_no
|
||||
return [u.user_row(hole_no=hole_no) for u in self.by_user().values()]
|
||||
|
||||
def top_by_day(self):
|
||||
user_dict = {u: r.progressive_score_list for u, r in self.by_user().items()}
|
||||
days = max(map(len, user_dict.values()))
|
||||
|
||||
rankings = collections.defaultdict(list)
|
||||
for day_idx in range(days):
|
||||
day_scores = {
|
||||
u: v[day_idx] for u, v in user_dict.items() if len(v) >= day_idx + 1
|
||||
}
|
||||
tops = [(u, v) for u, v in sorted(day_scores.items(), key=lambda t: t[1])][
|
||||
:20
|
||||
]
|
||||
for (user, score) in tops:
|
||||
rankings[user].append((day_idx + 1, score))
|
||||
return rankings
|
||||
148
wordlinator/utils/web.py
Normal file
148
wordlinator/utils/web.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from dash import dcc
|
||||
|
||||
###############
|
||||
# Date Helper #
|
||||
###############
|
||||
|
||||
|
||||
def _date_range(game):
|
||||
return f"{game.start_date} to {game.end_date}"
|
||||
|
||||
|
||||
def get_date_dropdown(dates, wordle_day=None):
|
||||
options = [
|
||||
{"label": f"Round {d.game} ({_date_range(d)})", "value": d.game_id}
|
||||
for d in dates
|
||||
]
|
||||
|
||||
value = dates[-1].game_id
|
||||
if wordle_day:
|
||||
if wordle_day.golf_hole:
|
||||
match = [d for d in dates if d.game == wordle_day.golf_hole.game_no]
|
||||
else:
|
||||
match = [d for d in dates[::-1] if d.end_date <= wordle_day.date]
|
||||
value = match[0].game_id
|
||||
|
||||
return dcc.Dropdown(
|
||||
id="round-selector-dropdown",
|
||||
options=options,
|
||||
value=value,
|
||||
clearable=False,
|
||||
)
|
||||
|
||||
|
||||
######################
|
||||
# Formatting Helpers #
|
||||
######################
|
||||
|
||||
|
||||
def format_string(col, condition):
|
||||
return "{" + col["id"] + "}" + f" {condition}"
|
||||
|
||||
|
||||
def column_formats(col, pct):
|
||||
return [
|
||||
{
|
||||
"if": {"column_id": col["id"]},
|
||||
"maxWidth": f"{pct}%",
|
||||
"width": f"{pct}%",
|
||||
"minWidth": f"{pct}%",
|
||||
},
|
||||
# Plain and markdown over-par
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, "> 4"),
|
||||
},
|
||||
"backgroundColor": "red",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[5]"'),
|
||||
},
|
||||
"backgroundColor": "red",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[6]"'),
|
||||
},
|
||||
"backgroundColor": "red",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[7]"'),
|
||||
},
|
||||
"backgroundColor": "red",
|
||||
},
|
||||
# Plain and Markdown par
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, "= 4"),
|
||||
},
|
||||
"backgroundColor": "orange",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[4]"'),
|
||||
},
|
||||
"backgroundColor": "orange",
|
||||
},
|
||||
# Plain and markdown under par
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, "< 4"),
|
||||
},
|
||||
"backgroundColor": "green",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[3]"'),
|
||||
},
|
||||
"backgroundColor": "green",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[2]"'),
|
||||
},
|
||||
"backgroundColor": "green",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, 'contains "[1]"'),
|
||||
},
|
||||
"backgroundColor": "green",
|
||||
},
|
||||
# Plain no score
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, "is nil"),
|
||||
},
|
||||
"backgroundColor": "white",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": format_string(col, "= ''"),
|
||||
},
|
||||
"backgroundColor": "white",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def column_formatting(hole_columns):
|
||||
pct = round((100 - (10 + 5)) / len(hole_columns), 2)
|
||||
return [
|
||||
entry
|
||||
for format_list in [column_formats(hole, pct) for hole in hole_columns]
|
||||
for entry in format_list
|
||||
]
|
||||
@@ -1,73 +1,139 @@
|
||||
import collections
|
||||
import functools
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
|
||||
import dash
|
||||
import dash.long_callback
|
||||
import diskcache
|
||||
import flask
|
||||
import flask.views
|
||||
import plotly.graph_objs
|
||||
|
||||
import wordlinator.db.pg as db
|
||||
import wordlinator.twitter
|
||||
import wordlinator.utils
|
||||
import wordlinator.utils.scores
|
||||
import wordlinator.utils.web
|
||||
|
||||
app = dash.Dash(name="WordleGolf")
|
||||
TTL_TIME = 30 if os.getenv("DEBUG") else 90
|
||||
LEADERBOARD_COUNT = 20
|
||||
VALUE_RE = re.compile(r"\[(?P<value>-?\d+)\]")
|
||||
|
||||
###################
|
||||
# Setup Functions #
|
||||
###################
|
||||
|
||||
assets_dir = pathlib.Path(__file__).parent / "assets"
|
||||
app = dash.Dash(
|
||||
name="WordleGolf",
|
||||
title="#WordleGolf",
|
||||
assets_folder=str(assets_dir.resolve()),
|
||||
suppress_callback_exceptions=True,
|
||||
)
|
||||
|
||||
|
||||
def _golf_score(score_list):
|
||||
scores = [s.score for s in score_list]
|
||||
score_count = len(scores)
|
||||
score = sum(scores) - (score_count * 4)
|
||||
return score
|
||||
def get_ttl_hash(seconds=TTL_TIME):
|
||||
return round(time.time() / seconds)
|
||||
|
||||
|
||||
def _get_user_scorelist(username, scores):
|
||||
scores = list(sorted(scores, key=lambda s: s.hole_id.hole))
|
||||
return {
|
||||
"Name": username,
|
||||
"Score": _golf_score(scores),
|
||||
**{f"Hole {s.hole_id.hole}": s.score for s in scores},
|
||||
}
|
||||
cache = diskcache.Cache("./cache")
|
||||
long_callback_manager = dash.long_callback.DiskcacheLongCallbackManager(
|
||||
cache, cache_by=get_ttl_hash
|
||||
)
|
||||
|
||||
|
||||
def _format_string(col, condition):
|
||||
return "{" + col["id"] + "}" + f" {condition}"
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _games_from_db(ttl_hash=None):
|
||||
return db.WordleDb().get_rounds()
|
||||
|
||||
|
||||
def _column_formats(col):
|
||||
return [
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": _format_string(col, "> 4"),
|
||||
},
|
||||
"backgroundColor": "red",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": _format_string(col, "= 4"),
|
||||
},
|
||||
"backgroundColor": "orange",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": col["id"],
|
||||
"filter_query": _format_string(col, "< 4"),
|
||||
},
|
||||
"backgroundColor": "green",
|
||||
},
|
||||
def games_from_db():
|
||||
return _games_from_db()
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _wordle_today(ttl_hash=None):
|
||||
today = wordlinator.utils.get_wordle_today()
|
||||
if today.golf_hole:
|
||||
return today
|
||||
last_completed_round = [
|
||||
game for game in games_from_db()[::-1] if game.start_date <= today.date
|
||||
]
|
||||
last_round = last_completed_round[0]
|
||||
return wordlinator.utils.WordleDay.from_date(last_round.end_date)
|
||||
|
||||
|
||||
def get_scores():
|
||||
score_list = db.WordleDb().get_scores(2)
|
||||
scores_by_user = collections.defaultdict(list)
|
||||
for score in score_list:
|
||||
scores_by_user[score.user_id.username].append(score)
|
||||
def wordle_today():
|
||||
return _wordle_today(get_ttl_hash())
|
||||
|
||||
table_rows = [
|
||||
_get_user_scorelist(username, scores)
|
||||
for username, scores in scores_by_user.items()
|
||||
]
|
||||
|
||||
def round_wordle_day(round_id):
|
||||
wt = wordle_today()
|
||||
rounds = games_from_db()
|
||||
|
||||
matching_round = [r for r in rounds if r.game_id == round_id][0]
|
||||
if matching_round.game == wt.golf_hole.game_no:
|
||||
return wt
|
||||
return wordlinator.utils.WordleDay.from_date(matching_round.end_date)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=3)
|
||||
def _scores_from_db(round_id, ttl_hash=None):
|
||||
wordle_db = db.WordleDb()
|
||||
scores = wordle_db.get_scores(round_id=round_id)
|
||||
users = wordle_db.get_users_by_round(round_id=round_id)
|
||||
usernames = [u.username for u in users]
|
||||
return wordlinator.utils.scores.ScoreMatrix(scores, usernames=usernames)
|
||||
|
||||
|
||||
def scores_from_db(round_id):
|
||||
return _scores_from_db(round_id, get_ttl_hash())
|
||||
|
||||
|
||||
#######################
|
||||
# Leaderboard helpers #
|
||||
#######################
|
||||
|
||||
|
||||
def get_leaderboard(round_id):
|
||||
score_matrix = scores_from_db(round_id)
|
||||
user_scores = score_matrix.by_user()
|
||||
top_20 = dict(
|
||||
list(sorted(user_scores.items(), key=lambda u: u[1].golf_score))[
|
||||
:LEADERBOARD_COUNT
|
||||
]
|
||||
)
|
||||
return dash.dash_table.DataTable(
|
||||
[{"Name": k, "Score": v.golf_score} for k, v in top_20.items()],
|
||||
style_as_list_view=True,
|
||||
style_table={"width": "40%", "margin": "auto"},
|
||||
style_cell={"textAlign": "center"},
|
||||
)
|
||||
|
||||
|
||||
#################
|
||||
# Score Helpers #
|
||||
#################
|
||||
|
||||
|
||||
def _get_scores(round_id):
|
||||
score_matrix = scores_from_db(round_id)
|
||||
round_day = round_wordle_day(round_id)
|
||||
table_rows = score_matrix.user_rows(round_day)
|
||||
return table_rows
|
||||
|
||||
|
||||
def get_scores(round_id):
|
||||
round_day = round_wordle_day(round_id)
|
||||
table_rows = _get_scores(round_id)
|
||||
|
||||
hole_columns = [
|
||||
{"name": f"Hole {i}", "id": f"Hole {i}", "type": "numeric"}
|
||||
for i in range(1, wordlinator.utils.WORDLE_TODAY.golf_hole.hole_no + 1)
|
||||
{"name": f"{i}", "id": f"{i}", "type": "text", "presentation": "markdown"}
|
||||
for i in range(1, round_day.golf_hole.hole_no + 1)
|
||||
]
|
||||
columns = [
|
||||
{"name": "Name", "id": "Name", "type": "text"},
|
||||
@@ -75,24 +141,325 @@ def get_scores():
|
||||
*hole_columns,
|
||||
]
|
||||
|
||||
color_formatting = wordlinator.utils.web.column_formatting(hole_columns)
|
||||
formatting = [
|
||||
format_entry
|
||||
for column_formats in [_column_formats(col) for col in hole_columns]
|
||||
for format_entry in column_formats
|
||||
{
|
||||
"if": {"column_id": "Name"},
|
||||
"textAlign": "center",
|
||||
"width": "10%",
|
||||
"maxWidth": "10%",
|
||||
"minWidth": "10%",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": "Score"},
|
||||
"textAlign": "center",
|
||||
"width": "5%",
|
||||
"maxWidth": "5%",
|
||||
"minWidth": "5%",
|
||||
},
|
||||
*color_formatting,
|
||||
]
|
||||
return dash.dash_table.DataTable(
|
||||
table_rows, columns, style_data_conditional=formatting, sort_action="native"
|
||||
table_rows,
|
||||
columns,
|
||||
id="user-scores-table",
|
||||
style_table={
|
||||
"width": "80%",
|
||||
"margin": "auto",
|
||||
"height": "600px",
|
||||
"overflowY": "auto",
|
||||
},
|
||||
fixed_rows={"headers": True, "data": 0},
|
||||
filter_action="native",
|
||||
filter_options={"case": "insensitive"},
|
||||
style_cell={"textAlign": "center"},
|
||||
style_data={"width": "10%"},
|
||||
style_as_list_view=True,
|
||||
style_data_conditional=formatting,
|
||||
sort_action="custom",
|
||||
sort_mode="single",
|
||||
sort_by=[{"column_id": "Name", "direction": "asc"}],
|
||||
)
|
||||
|
||||
|
||||
app.layout = dash.html.Div(children=[dash.html.H1("#WordleGolf"), get_scores()])
|
||||
#################
|
||||
# Stats Helpers #
|
||||
#################
|
||||
|
||||
|
||||
def _get_summary_rows(score_matrix):
|
||||
day_dict = score_matrix.by_hole()
|
||||
|
||||
totals = {
|
||||
"Score": "Total",
|
||||
**{day: scores.count for day, scores in day_dict.items()},
|
||||
}
|
||||
|
||||
averages = {
|
||||
"Score": "Daily Average",
|
||||
**{day: scores.average for day, scores in day_dict.items()},
|
||||
}
|
||||
|
||||
return [totals, averages]
|
||||
|
||||
|
||||
def _stats_dict(round_id):
|
||||
score_matrix = scores_from_db(round_id)
|
||||
table_rows = [{"Score": k, **v} for k, v in score_matrix.score_breakdown().items()]
|
||||
table_rows.extend(_get_summary_rows(score_matrix))
|
||||
return table_rows
|
||||
|
||||
|
||||
def get_daily_stats(round_id):
|
||||
table_rows = _stats_dict(round_id)
|
||||
|
||||
columns = [
|
||||
{"name": n, "id": n}
|
||||
for n in (
|
||||
"Score",
|
||||
*[
|
||||
f"{i}"
|
||||
for i in range(1, round_wordle_day(round_id).golf_hole.hole_no + 1)
|
||||
],
|
||||
)
|
||||
]
|
||||
return dash.dash_table.DataTable(
|
||||
table_rows,
|
||||
columns=columns,
|
||||
style_as_list_view=True,
|
||||
style_data_conditional=[
|
||||
{"if": {"filter_query": "{Score} = 'Total'"}, "fontWeight": "bold"},
|
||||
{"if": {"filter_query": "{Score} = 'Daily Average'"}, "fontWeight": "bold"},
|
||||
],
|
||||
style_table={"width": "80%", "margin": "auto"},
|
||||
)
|
||||
|
||||
|
||||
#################
|
||||
# Graph Helpers #
|
||||
#################
|
||||
|
||||
|
||||
SCORE_COLOR_DICT = {
|
||||
"Hole-in-1": "black",
|
||||
"Eagle": "darkgreen",
|
||||
"Birdie": "lightgreen",
|
||||
"Par": "white",
|
||||
"Bogey": "palevioletred",
|
||||
"Double Bogey": "orangered",
|
||||
"Fail": "darkred",
|
||||
}
|
||||
|
||||
|
||||
def get_line_graph(round_id):
|
||||
rows = _stats_dict(round_id)
|
||||
figure = plotly.graph_objs.Figure()
|
||||
total = [r for r in rows if r["Score"] == "Total"][0]
|
||||
rows = [r for r in rows if r["Score"] not in ("Total", "Daily Average")]
|
||||
total.pop("Score")
|
||||
for row in rows:
|
||||
score = row.pop("Score")
|
||||
y_values = []
|
||||
for k in row.keys():
|
||||
row_val = row.get(k)
|
||||
total_val = total.get(k)
|
||||
pct = row_val / total_val * 100
|
||||
y_values.append(pct)
|
||||
figure.add_trace(
|
||||
plotly.graph_objs.Scatter(
|
||||
x=list(row.keys()),
|
||||
y=y_values,
|
||||
fill="tonexty",
|
||||
name=score,
|
||||
line={"color": SCORE_COLOR_DICT[score]},
|
||||
stackgroup="dailies",
|
||||
)
|
||||
)
|
||||
figure.update_xaxes(tickvals=list(total.keys()), title_text="Days")
|
||||
figure.update_yaxes(title_text="Percent")
|
||||
figure.update_layout(yaxis_range=[0, 100])
|
||||
return dash.dcc.Graph(figure=figure)
|
||||
|
||||
|
||||
#####################
|
||||
# Line Race Helpers #
|
||||
#####################
|
||||
|
||||
|
||||
def line_race_graph(round_id):
|
||||
score_matrix = scores_from_db(round_id)
|
||||
tops_by_day = score_matrix.top_by_day()
|
||||
round_day = round_wordle_day(round_id)
|
||||
hole_no = round_day.golf_hole.hole_no
|
||||
|
||||
figure = plotly.graph_objs.Figure()
|
||||
figure.update_yaxes(autorange="reversed")
|
||||
figure.update_xaxes(tickmode="linear", tick0=1, dtick=1)
|
||||
annotation_names = collections.defaultdict(list)
|
||||
for name, entries in tops_by_day.items():
|
||||
figure.add_trace(
|
||||
plotly.graph_objs.Scatter(
|
||||
name=name,
|
||||
mode="lines+markers",
|
||||
x=[e[0] for e in entries],
|
||||
y=[e[1] for e in entries],
|
||||
)
|
||||
)
|
||||
if entries[-1][0] == hole_no:
|
||||
annotation_names[entries[-1]].append(name)
|
||||
|
||||
annotations = [
|
||||
{"x": k[0], "y": k[1], "text": ", ".join(v)}
|
||||
for k, v in annotation_names.items()
|
||||
]
|
||||
figure.update_layout(annotations=annotations)
|
||||
return dash.dcc.Graph(figure=figure)
|
||||
|
||||
|
||||
#############
|
||||
# App Setup #
|
||||
#############
|
||||
|
||||
app.layout = dash.html.Div(
|
||||
children=[
|
||||
dash.html.H1("#WordleGolf", style={"textAlign": "center"}, id="title"),
|
||||
dash.html.Div(
|
||||
wordlinator.utils.web.get_date_dropdown(
|
||||
games_from_db(), wordle_day=wordle_today()
|
||||
),
|
||||
id="round-selector",
|
||||
style={"maxWidth": "300px"},
|
||||
),
|
||||
dash.dcc.Tabs(
|
||||
id="main-tabs",
|
||||
value="leaderboard",
|
||||
children=[
|
||||
dash.dcc.Tab(label="Leaderboard", value="leaderboard"),
|
||||
dash.dcc.Tab(label="Statistics", value="statistics"),
|
||||
dash.dcc.Tab(label="User Scores", value="user-scores"),
|
||||
],
|
||||
),
|
||||
dash.dcc.Loading(dash.html.Div(id="tab-content"), id="tab-content-loading"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@app.callback(
|
||||
dash.dependencies.Output("tab-content", "children"),
|
||||
[
|
||||
dash.dependencies.Input("main-tabs", "value"),
|
||||
dash.dependencies.Input("round-selector-dropdown", "value"),
|
||||
],
|
||||
)
|
||||
def render_tab(tab, round_id):
|
||||
if tab == "leaderboard":
|
||||
return [
|
||||
dash.html.Div(
|
||||
[
|
||||
dash.html.H2(
|
||||
f"Leaderboard - Top {LEADERBOARD_COUNT}",
|
||||
style={"textAlign": "center"},
|
||||
),
|
||||
dash.dcc.Loading(
|
||||
id="leaderboard-race-loading",
|
||||
children=dash.html.Div(
|
||||
line_race_graph(round_id), id="leaderboard-race"
|
||||
),
|
||||
),
|
||||
dash.dcc.Loading(
|
||||
id="leaderboard-loading",
|
||||
children=dash.html.Div(
|
||||
get_leaderboard(round_id), id="leaderboard"
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
elif tab == "user-scores":
|
||||
return [
|
||||
dash.html.Div(
|
||||
[
|
||||
dash.html.H2("User Scores", style={"textAlign": "center"}),
|
||||
dash.dcc.Loading(
|
||||
id="user-scores-loading",
|
||||
children=dash.html.Div(get_scores(round_id), id="user-scores"),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
elif tab == "statistics":
|
||||
return [
|
||||
dash.html.Div(
|
||||
[
|
||||
dash.html.H2("Score Graph", style={"textAlign": "center"}),
|
||||
dash.dcc.Loading(
|
||||
id="stats-graph-loading",
|
||||
children=dash.html.Div(
|
||||
get_line_graph(round_id), id="stats-graph"
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
dash.html.Div(
|
||||
[
|
||||
dash.html.H2("Daily Stats", style={"textAlign": "center"}),
|
||||
dash.dcc.Loading(
|
||||
id="daily-stats-loading",
|
||||
children=dash.html.Div(
|
||||
get_daily_stats(round_id), id="daily-stats"
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@app.callback(
|
||||
dash.dependencies.Output("user-scores-table", "data"),
|
||||
dash.dependencies.Input("user-scores-table", "sort_by"),
|
||||
dash.dependencies.State("user-scores-table", "data"),
|
||||
)
|
||||
def sort_scores(sort_by, data):
|
||||
if not sort_by:
|
||||
return data
|
||||
sort_by = sort_by[0]
|
||||
|
||||
def _sort_val(entry):
|
||||
col_id = sort_by["column_id"]
|
||||
raw_val = entry.get(col_id)
|
||||
if raw_val is None or raw_val == "":
|
||||
return math.inf
|
||||
if col_id == "Name":
|
||||
return raw_val
|
||||
if isinstance(raw_val, int) or raw_val.isdigit():
|
||||
return int(raw_val)
|
||||
match = VALUE_RE.match(raw_val).groupdict()["value"]
|
||||
return int(match)
|
||||
|
||||
data = sorted(data, key=_sort_val, reverse=sort_by["direction"] == "desc")
|
||||
return data
|
||||
|
||||
|
||||
server = app.server
|
||||
|
||||
|
||||
class GetLinkView(flask.views.View):
|
||||
methods = ["GET"]
|
||||
|
||||
def dispatch_request(self):
|
||||
today = wordle_today()
|
||||
missing_users = db.WordleDb().get_users_without_score(
|
||||
today.golf_hole.game_no, today.golf_hole.hole_no
|
||||
)
|
||||
link = wordlinator.twitter.TwitterClient.full_notify_link(missing_users)
|
||||
return flask.redirect(link)
|
||||
|
||||
|
||||
server.add_url_rule("/tweet_link", view_func=GetLinkView.as_view("tweet_link"))
|
||||
|
||||
|
||||
def serve(debug=True):
|
||||
app.run_server(debug=debug)
|
||||
app.run(debug=debug)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
4
wordlinator/web/assets/style.css
Normal file
4
wordlinator/web/assets/style.css
Normal file
@@ -0,0 +1,4 @@
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
Reference in New Issue
Block a user