Skip to content

Commit 642c079

Browse files
authored
Merge pull request #276 from python-discord/jb3/environ/python-3.12
3.12 + Updates
2 parents b03d30f + d04e74c commit 642c079

35 files changed

+1209
-1044
lines changed

.github/ISSUE_TEMPLATE/bug-report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ body:
77
attributes:
88
value: |
99
Thanks for taking the time to fill out this bug report!
10-
10+
1111
Other developers need to be able to reproduce your bug reports to fix them,
1212
so please fill the following form to the best of your abilities.
1313
- type: textarea

.github/ISSUE_TEMPLATE/feature.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ body:
77
attributes:
88
value: |
99
Thanks for taking the time to fill out this feature request!
10-
10+
1111
Developers need to be able to understand your request,
1212
to properly discuss it an implement it. Please fill this
1313
form to the best of your ability.

.github/workflows/forms-backend.yml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ jobs:
1616
uses: actions/checkout@v2
1717

1818
- name: Install Python Dependencies
19-
uses: HassanAbouelela/actions/setup-python@setup-python_v1.3.1
19+
uses: HassanAbouelela/actions/setup-python@setup-python_v1.6.0
2020
with:
21-
dev: true
22-
python_version: "3.9"
21+
python_version: "3.12"
22+
install_args: "--only dev"
2323

24-
# Use this formatting to show them as GH Actions annotations.
25-
- name: Run flake8
26-
run: |
27-
flake8 --format='::error file=%(path)s,line=%(row)d,col=%(col)d::[flake8] %(code)s: %(text)s'
24+
- name: Run pre-commit hooks
25+
run: SKIP=ruff-lint pre-commit run --all-files
26+
27+
# Run `ruff` using github formatting to enable automatic inline annotations.
28+
- name: Run ruff
29+
run: "ruff check --output-format=github ."
2830

2931
# Prepare the Pull Request Payload artifact. If this fails, we
3032
# we fail silently using the `continue-on-error` option. It's
@@ -117,5 +119,5 @@ jobs:
117119
namespace: forms
118120
manifests: |
119121
deployment.yaml
120-
images: 'ghcr.io/python-discord/forms-backend:${{ steps.sha_tag.outputs.tag }}'
121-
kubectl-version: 'latest'
122+
images: "ghcr.io/python-discord/forms-backend:${{ steps.sha_tag.outputs.tag }}"
123+
kubectl-version: "latest"

.pre-commit-config.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: check-merge-conflict
6+
- id: check-toml
7+
- id: check-yaml
8+
- id: end-of-file-fixer
9+
- id: trailing-whitespace
10+
args: [--markdown-linebreak-ext=md]
11+
12+
- repo: local
13+
hooks:
14+
- id: ruff-lint
15+
name: ruff linting
16+
description: Run ruff linting
17+
entry: poetry run ruff check --force-exclude
18+
language: system
19+
"types_or": [python, pyi]
20+
require_serial: true
21+
args: [--fix, --exit-non-zero-on-fix]
22+
23+
- id: ruff-format
24+
name: ruff formatting
25+
description: Run ruff formatting
26+
entry: poetry run ruff format --force-exclude
27+
language: system
28+
"types_or": [python, pyi]
29+
require_serial: true

Dockerfile

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
FROM --platform=linux/amd64 ghcr.io/chrislovering/python-poetry-base:3.9-slim
1+
FROM --platform=linux/amd64 ghcr.io/owl-corp/python-poetry-base:3.12-slim
22

33
# Allow service to handle stops gracefully
44
STOPSIGNAL SIGQUIT
55

6-
# Install C compiler and make
7-
RUN apt-get update && \
8-
apt-get install -y gcc make && \
9-
apt-get clean && rm -rf /var/lib/apt/lists/*
10-
116
# Install dependencies
127
WORKDIR /app
138
COPY pyproject.toml poetry.lock ./
@@ -24,4 +19,4 @@ ENV GIT_SHA=$git_sha
2419

2520
# Start the server with uvicorn
2621
ENTRYPOINT ["poetry", "run"]
27-
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8000", "-k", "uvicorn.workers.UvicornWorker", "backend:app"]
22+
CMD ["uvicorn", "backend:app", "--host", "0.0.0.0", "--port", "8000"]

backend/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
dsn=constants.FORMS_BACKEND_DSN,
2828
send_default_pii=True,
2929
release=SENTRY_RELEASE,
30-
environment=SENTRY_RELEASE
30+
environment=SENTRY_RELEASE,
3131
)
3232

3333
middleware = [
@@ -36,10 +36,10 @@
3636
allow_origins=["https://forms.pythondiscord.com"],
3737
allow_origin_regex=ALLOW_ORIGIN_REGEX,
3838
allow_headers=[
39-
"Content-Type"
39+
"Content-Type",
4040
],
4141
allow_methods=["*"],
42-
allow_credentials=True
42+
allow_credentials=True,
4343
),
4444
Middleware(DatabaseMiddleware),
4545
Middleware(AuthenticationMiddleware, backend=JWTAuthenticationBackend()),

backend/authentication/backend.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import typing as t
2-
31
import jwt
42
from starlette import authentication
53
from starlette.requests import Request
64

7-
from backend import constants
8-
from backend import discord
5+
from backend import constants, discord
6+
97
# We must import user such way here to avoid circular imports
108
from .user import User
119

@@ -19,20 +17,19 @@ def get_token_from_cookie(cookie: str) -> str:
1917
try:
2018
prefix, token = cookie.split()
2119
except ValueError:
22-
raise authentication.AuthenticationError(
23-
"Unable to split prefix and token from authorization cookie."
24-
)
20+
msg = "Unable to split prefix and token from authorization cookie."
21+
raise authentication.AuthenticationError(msg)
2522

2623
if prefix.upper() != "JWT":
27-
raise authentication.AuthenticationError(
28-
f"Invalid authorization cookie prefix '{prefix}'."
29-
)
24+
msg = f"Invalid authorization cookie prefix '{prefix}'."
25+
raise authentication.AuthenticationError(msg)
3026

3127
return token
3228

3329
async def authenticate(
34-
self, request: Request
35-
) -> t.Optional[tuple[authentication.AuthCredentials, authentication.BaseUser]]:
30+
self,
31+
request: Request,
32+
) -> tuple[authentication.AuthCredentials, authentication.BaseUser] | None:
3633
"""Handles JWT authentication process."""
3734
cookie = request.cookies.get("token")
3835
if not cookie:
@@ -48,21 +45,25 @@ async def authenticate(
4845
scopes = ["authenticated"]
4946

5047
if not payload.get("token"):
51-
raise authentication.AuthenticationError("Token is missing from JWT.")
48+
msg = "Token is missing from JWT."
49+
raise authentication.AuthenticationError(msg)
5250
if not payload.get("refresh"):
53-
raise authentication.AuthenticationError(
54-
"Refresh token is missing from JWT."
55-
)
51+
msg = "Refresh token is missing from JWT."
52+
raise authentication.AuthenticationError(msg)
5653

5754
try:
5855
user_details = payload.get("user_details")
5956
if not user_details or not user_details.get("id"):
60-
raise authentication.AuthenticationError("Improper user details.")
61-
except Exception:
62-
raise authentication.AuthenticationError("Could not parse user details.")
57+
msg = "Improper user details."
58+
raise authentication.AuthenticationError(msg) # noqa: TRY301
59+
except Exception: # noqa: BLE001
60+
msg = "Could not parse user details."
61+
raise authentication.AuthenticationError(msg)
6362

6463
user = User(
65-
token, user_details, await discord.get_member(request.state.db, user_details["id"])
64+
token,
65+
user_details,
66+
await discord.get_member(request.state.db, user_details["id"]),
6667
)
6768
if await user.fetch_admin_status(request.state.db):
6869
scopes.append("admin")

backend/authentication/user.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import typing
21
import typing as t
32

43
import jwt
@@ -16,7 +15,7 @@ def __init__(
1615
self,
1716
token: str,
1817
payload: dict[str, t.Any],
19-
member: typing.Optional[models.DiscordMember],
18+
member: models.DiscordMember | None,
2019
) -> None:
2120
self.token = token
2221
self.payload = payload
@@ -31,11 +30,11 @@ def is_authenticated(self) -> bool:
3130
@property
3231
def display_name(self) -> str:
3332
"""Return username and discriminator as display name."""
34-
return f"{self.payload['username']}#{self.payload['discriminator']}"
33+
return f"{self.payload["username"]}#{self.payload["discriminator"]}"
3534

3635
@property
3736
def discord_mention(self) -> str:
38-
return f"<@{self.payload['id']}>"
37+
return f"<@{self.payload["id"]}>"
3938

4039
@property
4140
def user_id(self) -> str:
@@ -61,9 +60,10 @@ async def get_user_roles(self, database: Database) -> list[str]:
6160
return roles
6261

6362
async def fetch_admin_status(self, database: Database) -> bool:
64-
self.admin = await database.admins.find_one(
65-
{"_id": self.payload["id"]}
66-
) is not None
63+
query = {"_id": self.payload["id"]}
64+
found_admin = await database.admins.find_one(query)
65+
66+
self.admin = found_admin is not None
6767

6868
return self.admin
6969

backend/constants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
OAUTH2_CLIENT_ID = os.getenv("OAUTH2_CLIENT_ID")
1919
OAUTH2_CLIENT_SECRET = os.getenv("OAUTH2_CLIENT_SECRET")
2020
OAUTH2_REDIRECT_URI = os.getenv(
21-
"OAUTH2_REDIRECT_URI", "https://forms.pythondiscord.com/callback"
21+
"OAUTH2_REDIRECT_URI",
22+
"https://forms.pythondiscord.com/callback",
2223
)
2324

2425
GIT_SHA = os.getenv("GIT_SHA", "dev")
@@ -28,7 +29,7 @@
2829

2930
SECRET_KEY = os.getenv("SECRET_KEY", binascii.hexlify(os.urandom(30)).decode())
3031
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
31-
DISCORD_GUILD = os.getenv("DISCORD_GUILD", 267624335836053506)
32+
DISCORD_GUILD = os.getenv("DISCORD_GUILD", "267624335836053506")
3233

3334
HCAPTCHA_API_SECRET = os.getenv("HCAPTCHA_API_SECRET")
3435

backend/discord.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import datetime
44
import json
5-
import typing
65

76
import httpx
87
import starlette.requests
@@ -17,7 +16,7 @@ async def fetch_bearer_token(code: str, redirect: str, *, refresh: bool) -> dict
1716
data = {
1817
"client_id": constants.OAUTH2_CLIENT_ID,
1918
"client_secret": constants.OAUTH2_CLIENT_SECRET,
20-
"redirect_uri": f"{redirect}/callback"
19+
"redirect_uri": f"{redirect}/callback",
2120
}
2221

2322
if refresh:
@@ -27,9 +26,13 @@ async def fetch_bearer_token(code: str, redirect: str, *, refresh: bool) -> dict
2726
data["grant_type"] = "authorization_code"
2827
data["code"] = code
2928

30-
r = await client.post(f"{constants.DISCORD_API_BASE_URL}/oauth2/token", headers={
31-
"Content-Type": "application/x-www-form-urlencoded"
32-
}, data=data)
29+
r = await client.post(
30+
f"{constants.DISCORD_API_BASE_URL}/oauth2/token",
31+
headers={
32+
"Content-Type": "application/x-www-form-urlencoded",
33+
},
34+
data=data,
35+
)
3336

3437
r.raise_for_status()
3538

@@ -38,9 +41,12 @@ async def fetch_bearer_token(code: str, redirect: str, *, refresh: bool) -> dict
3841

3942
async def fetch_user_details(bearer_token: str) -> dict:
4043
async with httpx.AsyncClient() as client:
41-
r = await client.get(f"{constants.DISCORD_API_BASE_URL}/users/@me", headers={
42-
"Authorization": f"Bearer {bearer_token}"
43-
})
44+
r = await client.get(
45+
f"{constants.DISCORD_API_BASE_URL}/users/@me",
46+
headers={
47+
"Authorization": f"Bearer {bearer_token}",
48+
},
49+
)
4450

4551
r.raise_for_status()
4652

@@ -52,15 +58,17 @@ async def _get_role_info() -> list[models.DiscordRole]:
5258
async with httpx.AsyncClient() as client:
5359
r = await client.get(
5460
f"{constants.DISCORD_API_BASE_URL}/guilds/{constants.DISCORD_GUILD}/roles",
55-
headers={"Authorization": f"Bot {constants.DISCORD_BOT_TOKEN}"}
61+
headers={"Authorization": f"Bot {constants.DISCORD_BOT_TOKEN}"},
5662
)
5763

5864
r.raise_for_status()
5965
return [models.DiscordRole(**role) for role in r.json()]
6066

6167

6268
async def get_roles(
63-
database: Database, *, force_refresh: bool = False
69+
database: Database,
70+
*,
71+
force_refresh: bool = False,
6472
) -> list[models.DiscordRole]:
6573
"""
6674
Get a list of all roles from the cache, or discord API if not available.
@@ -86,23 +94,26 @@ async def get_roles(
8694
if len(roles) == 0:
8795
# Fetch roles from the API and insert into the database
8896
roles = await _get_role_info()
89-
await collection.insert_many({
90-
"name": role.name,
91-
"id": role.id,
92-
"data": role.json(),
93-
"inserted_at": datetime.datetime.now(tz=datetime.timezone.utc),
94-
} for role in roles)
97+
await collection.insert_many(
98+
{
99+
"name": role.name,
100+
"id": role.id,
101+
"data": role.json(),
102+
"inserted_at": datetime.datetime.now(tz=datetime.UTC),
103+
}
104+
for role in roles
105+
)
95106

96107
return roles
97108

98109

99-
async def _fetch_member_api(member_id: str) -> typing.Optional[models.DiscordMember]:
110+
async def _fetch_member_api(member_id: str) -> models.DiscordMember | None:
100111
"""Get a member by ID from the configured guild using the discord API."""
101112
async with httpx.AsyncClient() as client:
102113
r = await client.get(
103114
f"{constants.DISCORD_API_BASE_URL}/guilds/{constants.DISCORD_GUILD}"
104115
f"/members/{member_id}",
105-
headers={"Authorization": f"Bot {constants.DISCORD_BOT_TOKEN}"}
116+
headers={"Authorization": f"Bot {constants.DISCORD_BOT_TOKEN}"},
106117
)
107118

108119
if r.status_code == 404:
@@ -113,8 +124,11 @@ async def _fetch_member_api(member_id: str) -> typing.Optional[models.DiscordMem
113124

114125

115126
async def get_member(
116-
database: Database, user_id: str, *, force_refresh: bool = False
117-
) -> typing.Optional[models.DiscordMember]:
127+
database: Database,
128+
user_id: str,
129+
*,
130+
force_refresh: bool = False,
131+
) -> models.DiscordMember | None:
118132
"""
119133
Get a member from the cache, or from the discord API.
120134
@@ -147,7 +161,7 @@ async def get_member(
147161
await collection.insert_one({
148162
"user": user_id,
149163
"data": member.json(),
150-
"inserted_at": datetime.datetime.now(tz=datetime.timezone.utc),
164+
"inserted_at": datetime.datetime.now(tz=datetime.UTC),
151165
})
152166
return member
153167

@@ -161,7 +175,9 @@ class UnauthorizedError(exceptions.HTTPException):
161175

162176

163177
async def _verify_access_helper(
164-
form_id: str, request: starlette.requests.Request, attribute: str
178+
form_id: str,
179+
request: starlette.requests.Request,
180+
attribute: str,
165181
) -> None:
166182
"""A low level helper to validate access to a form resource based on the user's scopes."""
167183
form = await request.state.db.forms.find_one({"_id": form_id})

0 commit comments

Comments
 (0)