Skip to content

Commit a839b10

Browse files
authored
Weekly codewar challenge (#447)
1 parent 18f061e commit a839b10

File tree

8 files changed

+210
-1
lines changed

8 files changed

+210
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""init codewars challenge
2+
3+
Revision ID: dece77ed5036
4+
Revises: 2ba5cc3efce8
5+
Create Date: 2021-01-04 20:23:50.479134+00:00
6+
7+
"""
8+
from datetime import datetime
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision = "dece77ed5036"
16+
down_revision = "2ba5cc3efce8"
17+
branch_labels = None
18+
depends_on = None
19+
20+
QUESTIONS = [
21+
"550f22f4d758534c1100025a",
22+
"525f3eda17c7cd9f9e000b39",
23+
"54521e9ec8e60bc4de000d6c",
24+
"52bb6539a4cf1b12d90005b7",
25+
"5296bc77afba8baa690002d7",
26+
"5235c913397cbf2508000048",
27+
"521c2db8ddc89b9b7a0000c1",
28+
"55983863da40caa2c900004e",
29+
"52742f58faf5485cae000b9a",
30+
"52a78825cdfc2cfc87000005",
31+
"54acd76f7207c6a2880012bb",
32+
"5679d5a3f2272011d700000d",
33+
]
34+
35+
codewars = sa.sql.table(
36+
"codewars_challenge",
37+
sa.Column("id", sa.Text, primary_key=True),
38+
sa.Column("added_at", sa.DateTime(timezone=True), default=datetime.now, nullable=False),
39+
sa.Column(
40+
"posted_at",
41+
sa.DateTime(timezone=True),
42+
default=None,
43+
onupdate=datetime.now,
44+
nullable=True,
45+
),
46+
)
47+
48+
49+
def upgrade():
50+
# ### commands auto generated by Alembic - please adjust! ###
51+
op.create_table(
52+
"codewars_challenge",
53+
sa.Column("id", sa.Text(), nullable=False),
54+
sa.Column("added_at", sa.DateTime(timezone=True), nullable=False, default=datetime.utcnow),
55+
sa.Column("posted_at", sa.DateTime(timezone=True), nullable=True),
56+
sa.PrimaryKeyConstraint("id"),
57+
)
58+
# ### end Alembic commands ###
59+
60+
op.bulk_insert(codewars, [{"id": q} for q in QUESTIONS])
61+
62+
63+
def downgrade():
64+
# ### commands auto generated by Alembic - please adjust! ###
65+
op.drop_table("codewars_challenge")
66+
# ### end Alembic commands ###

pyslackersweb/sirbot/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from aiohttp import web
22

33
from .views import routes
4+
from .context import background_jobs
45

56

67
async def app_factory() -> web.Application:
@@ -12,4 +13,7 @@ async def app_factory() -> web.Application:
1213
)
1314

1415
sirbot.add_routes(routes)
16+
17+
sirbot.cleanup_ctx.extend([background_jobs])
18+
1519
return sirbot

pyslackersweb/sirbot/context.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import datetime
2+
3+
from typing import AsyncGenerator
4+
5+
import pytz
6+
7+
from aiohttp import web
8+
9+
from . import tasks
10+
11+
12+
async def background_jobs(app: web.Application) -> AsyncGenerator[None, None]:
13+
scheduler = app["scheduler"]
14+
pg = app["pg"]
15+
slack_client = app["slack_client"]
16+
17+
scheduler.add_job(
18+
tasks.post_slack_codewars_challenge,
19+
"interval",
20+
start_date=datetime.datetime(year=2021, month=1, day=7, hour=18, tzinfo=pytz.UTC),
21+
weeks=1,
22+
args=(slack_client, pg),
23+
)
24+
25+
yield

pyslackersweb/sirbot/database.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import logging
2+
3+
import asyncpg
4+
5+
from pyslackersweb.util.log import ContextAwareLoggerAdapter
6+
7+
8+
logger = ContextAwareLoggerAdapter(logging.getLogger(__name__))
9+
10+
11+
async def get_challenge(conn: asyncpg.connection.Connection) -> str:
12+
return await conn.fetchval(
13+
"""
14+
UPDATE codewars_challenge SET posted_at=now() WHERE id=(
15+
SELECT id FROM codewars_challenge WHERE posted_at IS NULL ORDER BY RANDOM() LIMIT 1
16+
) RETURNING id""",
17+
)

pyslackersweb/sirbot/models.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from datetime import datetime
2+
3+
import sqlalchemy as sa
4+
5+
from pyslackersweb.models import metadata
6+
7+
codewars = sa.Table(
8+
"codewars_challenge",
9+
metadata,
10+
sa.Column("id", sa.Text, primary_key=True),
11+
sa.Column("added_at", sa.DateTime(timezone=True), default=datetime.now, nullable=False),
12+
sa.Column(
13+
"posted_at",
14+
sa.DateTime(timezone=True),
15+
default=None,
16+
onupdate=datetime.now,
17+
nullable=True,
18+
),
19+
)

pyslackersweb/sirbot/tasks.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import logging
2+
3+
import slack
4+
import asyncpg.pool
5+
6+
from slack.events import Message
7+
from slack.io.abc import SlackAPI
8+
9+
from pyslackersweb.util.log import ContextAwareLoggerAdapter
10+
11+
from . import database
12+
13+
logger = ContextAwareLoggerAdapter(logging.getLogger(__name__))
14+
15+
16+
async def post_slack_codewars_challenge(
17+
slack_client: SlackAPI,
18+
pg: asyncpg.pool.Pool,
19+
) -> None:
20+
async with pg.acquire() as conn:
21+
challenge = await database.get_challenge(conn)
22+
23+
message = Message()
24+
message["channel"] = "CEFJ9TJNL" # advent_of_code
25+
if challenge:
26+
message["attachments"] = [
27+
{
28+
"fallback": f"Codewars challenge: https://www.codewars.com/kata/{challenge}",
29+
"color": "#ff0000",
30+
"title": "Weekly Codewars challenge",
31+
"title_link": f"https://www.codewars.com/kata/{challenge}",
32+
"text": "Discuss in this thread",
33+
"footer": "Codewars",
34+
"footer_icon": "https://codewars.com/favicon.ico",
35+
}
36+
]
37+
else:
38+
message["attachments"] = [
39+
{
40+
"fallback": "Codewars challenge: no challenge found",
41+
"color": "#ff0000",
42+
"title": "Weekly Codewars challenge",
43+
"title_link": "https://www.codewars.com",
44+
"text": "No challenge found :sad-panda:, please seed new challenges ",
45+
"footer": "Codewars",
46+
"footer_icon": "https://codewars.com/favicon.ico",
47+
}
48+
]
49+
await slack_client.query(slack.methods.CHAT_POST_MESSAGE, data=message)

tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ async def client(monkeypatch, aiohttp_client, slack_client_ctx):
3636
await conn.fetch(delete(pyslackersweb.models.domains))
3737
await conn.fetch(delete(pyslackersweb.models.SlackUsers))
3838
await conn.fetch(delete(pyslackersweb.models.SlackChannels))
39+
await conn.fetch(delete(pyslackersweb.sirbot.models.codewars))

tests/test_sirbot.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import json
12
import pytest
23

34
from slack import methods
45

5-
from pyslackersweb.sirbot import settings
6+
from pyslackersweb.sirbot import settings, tasks, models
7+
from sqlalchemy.dialects.postgresql import insert as pg_insert
68

79

810
@pytest.mark.parametrize(
@@ -57,3 +59,29 @@ async def test_readthedocs_notification_missing_name(client):
5759

5860
# Assert we did not send a message to slack
5961
assert not client.app["slack_client"]._request.called
62+
63+
64+
async def test_task_codewars_challenge(client, caplog):
65+
await tasks.post_slack_codewars_challenge(client.app["slack_client"], client.app["pg"])
66+
67+
mocked_request = client.app["slack_client"]._request
68+
mocked_request.assert_called_once()
69+
mocked_request_args = mocked_request.call_args.args
70+
71+
payload = json.loads(mocked_request_args[3])
72+
assert payload["channel"] == "CEFJ9TJNL"
73+
assert "No challenge found" in payload["attachments"][0]["text"]
74+
75+
async with client.app["pg"].acquire() as conn:
76+
await conn.execute(pg_insert(models.codewars).values(id="foo"))
77+
78+
client.app["slack_client"]._request.reset_mock()
79+
await tasks.post_slack_codewars_challenge(client.app["slack_client"], client.app["pg"])
80+
81+
mocked_request = client.app["slack_client"]._request
82+
mocked_request.assert_called_once()
83+
mocked_request_args = mocked_request.call_args.args
84+
85+
payload = json.loads(mocked_request_args[3])
86+
assert payload["channel"] == "CEFJ9TJNL"
87+
assert payload["attachments"][0]["title_link"] == "https://www.codewars.com/kata/foo"

0 commit comments

Comments
 (0)