Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
default_language_version:
python: python3.12
python: python3.14

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder

RUN apt-get update && apt-get install \
--no-install-recommends --no-install-suggests -y \
Expand All @@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \

COPY . ./

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS runtime
FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS runtime

LABEL org.opencontainers.image.source=https://github.com/ch00k/mottle

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

lint:
uv run ruff format .
uv run ruff check --fix .
uv run ruff check --fix --unsafe-fixes .
uv run mypy --config-file pyproject.toml .

test:
Expand Down
6 changes: 4 additions & 2 deletions mottle/logging.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from datetime import datetime
from typing import Any
from typing import TYPE_CHECKING, Any

from json_log_formatter import BUILTIN_ATTRS, JSONFormatter

if TYPE_CHECKING:
import logging

BUILTIN_ATTRS.add("taskName")


Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = { file = "LICENSE" }
readme = "README.md"
homepage = "https://mottle.it"
repository = "https://github.com/Ch00k/mottle"
requires-python = ">=3.12,<3.13"
requires-python = ">=3.14"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align project Python requirement with Docker base

Raising requires-python to >=3.14 will cause installs to fail in the existing Docker build because both builder and runtime images are still pinned to Python 3.12 (ghcr.io/astral-sh/uv:python3.12-bookworm-slim). With uv sync --locked in the Dockerfile, uv will refuse to install a project that declares >=3.14, so container builds (and any 3.12-based deploys) will break unless the base image is updated to 3.14 at the same time.

Useful? React with 👍 / 👎.


dependencies = [
"django==5.2.8",
Expand All @@ -30,7 +30,7 @@ dependencies = [
"json-log-formatter==1.1.1",
"daphne==4.2.1",
"django-hosts==7.0.0",
"tekore==6.0.0",
"tekore==6.1.0",
"croniter==6.0.0",
]

Expand All @@ -46,7 +46,7 @@ dev = [
"pytest-cov>=7.0.0",
"minify-html>=0.18.1",
]
debug = ["ipython==9.7.0", "pdbpp==0.11.7"]
debug = ["ipython==9.8.0", "pdbpp==0.11.7"]

[tool.ruff]
line-length = 120
Expand Down
2 changes: 1 addition & 1 deletion taskrunner/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import signal
import sys
import time
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from django.conf import settings
Expand All @@ -12,6 +11,7 @@
from taskrunner.schedules import event_updates, playlist_updates

if TYPE_CHECKING:
from collections.abc import Callable
from threading import Thread
from wsgiref.simple_server import WSGIServer

Expand Down
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
import uuid
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING
from unittest.mock import Mock

import pytest
Expand All @@ -23,6 +23,9 @@
)
from web.utils import MottleSpotifyClient

if TYPE_CHECKING:
from collections.abc import Awaitable, Callable


@pytest.fixture
async def spotify_user() -> SpotifyUser:
Expand Down
2 changes: 1 addition & 1 deletion urlshortener/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def save(self, *args: Any, **kwargs: Any) -> None:
super().save(*args, **kwargs)

@staticmethod
async def shorten(url: str) -> "ShortURL":
async def shorten(url: str) -> ShortURL:
short_url, _ = await ShortURL.objects.aget_or_create(url=url)
return short_url

Expand Down
6 changes: 5 additions & 1 deletion urlshortener/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from django.http import HttpRequest, HttpResponse
from typing import TYPE_CHECKING

from django.shortcuts import aget_object_or_404, redirect
from django.views.decorators.http import require_GET

from urlshortener.models import ShortURL

if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse


@require_GET
async def get_url(_: HttpRequest, hash: str) -> HttpResponse: # noqa: A002
Expand Down
906 changes: 553 additions & 353 deletions uv.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion web/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from typing import TYPE_CHECKING

from django.conf import settings
from django.http import HttpRequest

if TYPE_CHECKING:
from django.http import HttpRequest


def app_version(_: HttpRequest) -> dict[str, str]:
Expand Down
22 changes: 13 additions & 9 deletions web/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass, field
from datetime import date
from functools import total_ordering
from typing import TYPE_CHECKING

from tekore.model import (
FullArtist,
Expand All @@ -13,7 +13,11 @@
)

from .templatetags.tekore_model_extras import get_largest_image, get_smallest_image, get_spotify_url, humanize_duration
from .views_utils import AlbumMetadata, ArtistMetadata, PlaylistMetadata

if TYPE_CHECKING:
from datetime import date

from .views_utils import AlbumMetadata, ArtistMetadata, PlaylistMetadata


@total_ordering
Expand Down Expand Up @@ -57,7 +61,7 @@ def __lt__(self, other: object) -> bool:
return super().__lt__(other)

@staticmethod
def from_tekore_model(album: SimpleAlbum) -> "AlbumData":
def from_tekore_model(album: SimpleAlbum) -> AlbumData:
return AlbumData(
id=album.id,
name=album.name,
Expand All @@ -71,7 +75,7 @@ def from_tekore_model(album: SimpleAlbum) -> "AlbumData":
)

@staticmethod
async def from_metadata(metadata: AlbumMetadata) -> "AlbumData":
async def from_metadata(metadata: AlbumMetadata) -> AlbumData:
return AlbumData(
id=await metadata.id,
name=await metadata.name,
Expand Down Expand Up @@ -99,7 +103,7 @@ def __lt__(self, other: object) -> bool:
return super().__lt__(other)

@staticmethod
def from_tekore_model(artist: FullArtist) -> "ArtistData":
def from_tekore_model(artist: FullArtist) -> ArtistData:
return ArtistData(
id=artist.id,
name=artist.name,
Expand All @@ -110,7 +114,7 @@ def from_tekore_model(artist: FullArtist) -> "ArtistData":
)

@staticmethod
async def from_metadata(metadata: ArtistMetadata) -> "ArtistData":
async def from_metadata(metadata: ArtistMetadata) -> ArtistData:
return ArtistData(
id=await metadata.id,
name=await metadata.name,
Expand Down Expand Up @@ -142,7 +146,7 @@ def from_tekore_model(
track: FullTrack | FullPlaylistTrack | SimpleTrack,
album: AlbumData | None = None,
added_at: date | None = None,
) -> "TrackData":
) -> TrackData:
album_data: AlbumData | None

if isinstance(track, (FullTrack, FullPlaylistTrack)):
Expand Down Expand Up @@ -198,7 +202,7 @@ def __lt__(self, other: object) -> bool:
@staticmethod
async def from_metadata(
metadata: PlaylistMetadata,
) -> "PlaylistData":
) -> PlaylistData:
return PlaylistData(
id=await metadata.id,
name=await metadata.name,
Expand All @@ -213,7 +217,7 @@ async def from_metadata(
)

@staticmethod
def from_tekore_model(playlist: FullPlaylist | SimplePlaylist) -> "PlaylistData":
def from_tekore_model(playlist: FullPlaylist | SimplePlaylist) -> PlaylistData:
return PlaylistData(
id=playlist.id,
name=playlist.name,
Expand Down
12 changes: 6 additions & 6 deletions web/events/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Any, Optional
from typing import Any

from django.conf import settings
from sentry_sdk import capture_exception, capture_message
Expand Down Expand Up @@ -54,15 +54,15 @@ class MusicBrainzArtist:
bandsintown_url: str | None = None

@staticmethod
async def find(artist_spotify_id: str, artist_name: str) -> Optional["MusicBrainzArtist"]:
async def find(artist_spotify_id: str, artist_name: str) -> MusicBrainzArtist | None:
try:
return await MusicBrainzArtist.find_by_spotify_url(f"https://open.spotify.com/artist/{artist_spotify_id}")
except MusicBrainzException as e:
logger.warning(f"Failed to find artist by Spotify ID '{artist_spotify_id}': {e}. Trying search by name")
return await MusicBrainzArtist.find_by_name(artist_name)

@staticmethod
async def find_by_spotify_url(url: str) -> Optional["MusicBrainzArtist"]:
async def find_by_spotify_url(url: str) -> MusicBrainzArtist | None:
try:
resp = await asend_get_request(
async_musicbrainz_client, "url", params={"resource": url, "inc": "artist-rels"}
Expand Down Expand Up @@ -93,7 +93,7 @@ async def find_by_spotify_url(url: str) -> Optional["MusicBrainzArtist"]:
return artist

@staticmethod
async def find_by_name(artist_name: str) -> Optional["MusicBrainzArtist"]:
async def find_by_name(artist_name: str) -> MusicBrainzArtist | None:
if not artist_name:
raise MusicBrainzException("Artist name is empty")

Expand Down Expand Up @@ -138,7 +138,7 @@ async def find_by_name(artist_name: str) -> Optional["MusicBrainzArtist"]:
return None

@staticmethod
async def from_artist_id(artist_id: str) -> "MusicBrainzArtist":
async def from_artist_id(artist_id: str) -> MusicBrainzArtist:
try:
resp = await asend_get_request(
async_musicbrainz_client, f"artist/{artist_id}", params={"inc": "aliases+url-rels"}
Expand Down Expand Up @@ -234,7 +234,7 @@ def __post_init__(self) -> None:
@staticmethod
async def find(
artist_name: str, alternative_names: list[str], use_advanced_heuristics: bool = False
) -> "EventSourceArtist":
) -> EventSourceArtist:
if artist_name in alternative_names:
alternative_names.remove(artist_name)

Expand Down
6 changes: 4 additions & 2 deletions web/events/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
import string
import time
import timeit
from typing import Any
from typing import TYPE_CHECKING, Any

import httpx
from django.conf import settings
from lxml import html as lh
from prometheus_client import Counter, Histogram

from web.metrics import (
BANDSINTOWN_API_EXCEPTIONS,
Expand All @@ -36,6 +35,9 @@
)
from .exceptions import HTTPClientException, RetriesExhaustedException

if TYPE_CHECKING:
from prometheus_client import Counter, Histogram

logger = logging.getLogger(__name__)


Expand Down
2 changes: 1 addition & 1 deletion web/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def replace_unicode_characters(string: str) -> str:
)


def should_fetch_event(event_url: str, event_source: EventDataSource, events: dict[str, "Event"]) -> bool:
def should_fetch_event(event_url: str, event_source: EventDataSource, events: dict[str, Event]) -> bool:
existing_event = events.get(event_url)

if existing_event is None:
Expand Down
10 changes: 7 additions & 3 deletions web/middleware.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import logging
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING

from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse
from django_hosts import host
from django_htmx.middleware import HtmxDetails

from .models import SpotifyAuth
from .utils import MottleException, MottleSpotifyClient

if TYPE_CHECKING:
from collections.abc import Awaitable, Callable

from django_hosts import host
from django_htmx.middleware import HtmxDetails

logger = logging.getLogger(__name__)


Expand Down
8 changes: 6 additions & 2 deletions web/migrations/0002_encrypt_tokens.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Generated by Django 5.0.3 on 2024-04-22 18:22


from django.apps.registry import Apps
from typing import TYPE_CHECKING

from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor

import web.models

if TYPE_CHECKING:
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor


def unset_state_and_redirect_uri(apps: Apps, _: BaseDatabaseSchemaEditor) -> None:
SpotifyAuth = apps.get_model("web", "SpotifyAuth")
Expand Down
7 changes: 5 additions & 2 deletions web/migrations/0004_refactor_for_autoaccept.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Generated by Django 5.0.4 on 2024-06-05 07:04

import uuid
from typing import TYPE_CHECKING

import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor

if TYPE_CHECKING:
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor


def create_watch_configs(apps: Apps, _: BaseDatabaseSchemaEditor) -> None:
Expand Down
8 changes: 6 additions & 2 deletions web/migrations/0007_spotifyauth_token_scope.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Generated by Django 5.0.4 on 2024-08-03 17:41

from django.apps.registry import Apps
from typing import TYPE_CHECKING

from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor

if TYPE_CHECKING:
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor


def set_token_scope(apps: Apps, _: BaseDatabaseSchemaEditor) -> None:
Expand Down
Loading