Compare commits

..

2 Commits

Author SHA1 Message Date
briquet 10ad3e1d73 🔒️(backend) upgrade aiohttp to fix CVE-2026-111064
Upgrade aiohttp to 3.14.5 in the backend + agents

https://avd.aquasec.com/nvd/cve-2026-111064
2026-10-20 13:52:40 +02:00
briquet 0ef87e36f9 ⚡️(backend) add opt-in psycopg connection pool
Each request opened and closed its own Postgres connection which added a
big overhead. The goal is to add Django's native psycopg pool behind a new
DB_PSYCOPG_POOL_ENABLED settings with explicit min/max size and keepalive.

Each pool lives in one process, and gunicorn's sync workers serve a single
request at a time which means that a given worker won't use more than one
connection. The default pool minimum size is set to 1, which keeps always
one connection open and the maximum size to 3 as headroom for code touching
the database from an extra thread.

Pooled connections can sit idle for hours, so TCP keepalives are enabled
to keep load balancers and NAT from dropping them silently, and each
connection is health checked on checkout so a broken one is replaced
instead of failing the request.
2026-10-20 12:39:35 +02:00
33 changed files with 221 additions and 637 deletions
-21
View File
@@ -8,25 +8,6 @@ and this project adheres to
## [Unreleased]
### Fixed
- 🐛(backend) return RFC 6749 errors from the application token endpoint
## [1.36.1] - 2026-10-21
### Added
### Changed
- 🔖(helm) release chart 0.0.28
### Fixed
- 🔒️(docker) distrust compromised eMudhra emSign G3 CA roots
- 🔒️(backend) upgrade pillow to 13.0.0 to fix CVE-2026-112403
## [1.36.0] - 2026-10-20
### Added
- ⚡️(backend) add UPPER(email) index for case-insensitive user lookups
@@ -41,8 +22,6 @@ and this project adheres to
- 🔒️(backend) throttle the start recording endpoint
- 🐛(backend) put a timeout on every LiveKit API call
- 🔒️(backend) upgrade aiohttp to 3.14.5 to fix CVE-2026-111064
- ♿️(frontend) announce the microphone turning off on shortcut #1816
- 🐛(release) use env bash in prepare-release script for NixOS
## [1.35.0] - 2026-10-13
+4 -8
View File
@@ -1,16 +1,14 @@
# Django Meet
# ---- base image to inherit from ----
FROM python:3.13.16-alpine3.24 AS base
FROM python:3.13.15-alpine3.24 AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip
# Upgrade system packages to install security updates
RUN apk update && \
apk upgrade && \
apk add --no-cache --upgrade 'ca-certificates>=20261020-r0' && \
update-ca-certificates
apk upgrade
# ---- Back-end builder image ----
FROM base AS back-builder
@@ -25,7 +23,7 @@ ENV UV_LINK_MODE=copy
ENV UV_PYTHON_DOWNLOADS=0
# install uv
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
WORKDIR /app
@@ -41,8 +39,6 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# ---- mails ----
FROM node:22-alpine AS mail-builder
RUN apk upgrade --no-cache ca-certificates
COPY ./src/mail /mail/app
WORKDIR /mail/app
@@ -124,7 +120,7 @@ USER root:root
RUN apk add postgresql-client
# Install development dependencies
RUN --mount=from=ghcr.io/astral-sh/uv:0.13.4,source=/uv,target=/bin/uv \
RUN --mount=from=ghcr.io/astral-sh/uv:0.10.9,source=/uv,target=/bin/uv \
uv sync --all-extras --locked
# Restore the un-privileged user running the application
+48 -45
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#!/bin/bash
set -e # Exit on error
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
@@ -10,59 +10,60 @@ NC='\033[0m' # No Color
# Function to print colored output
print_info() {
echo -e "${GREEN}[INFO]${NC} $1"
echo -e "${GREEN}[INFO]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Function to update npm package version
update_npm_version() {
local component=$1
print_info "Updating $component version..."
cd "src/$component"
npm version "$VERSION" --no-git-tag-version
cd -
local component=$1
print_info "Updating $component version..."
cd "src/$component"
npm version "$VERSION" --no-git-tag-version
cd -
}
# Function to update Python project version in pyproject.toml
update_python_version() {
local component=$1
print_info "Updating $component version..."
cd "src/$component"
local component=$1
print_info "Updating $component version..."
cd "src/$component"
if [ ! -f "pyproject.toml" ]; then
print_error "pyproject.toml not found in src/$component!"
exit 1
fi
if [ ! -f "pyproject.toml" ]; then
print_error "pyproject.toml not found in src/$component!"
exit 1
fi
if grep -q '^version = "' pyproject.toml; then
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml
rm pyproject.toml.bak
print_info "Updated pyproject.toml version to $VERSION"
else
print_error "Could not find version line in pyproject.toml"
exit 1
fi
if grep -q '^version = "' pyproject.toml; then
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml
rm pyproject.toml.bak
print_info "Updated pyproject.toml version to $VERSION"
else
print_error "Could not find version line in pyproject.toml"
exit 1
fi
cd -
cd -
}
# Check if we're in a git repository
if ! git rev-parse --git-dir >/dev/null 2>&1; then
print_error "Not a git repository. Please run this script from the root of your project."
exit 1
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not a git repository. Please run this script from the root of your project."
exit 1
fi
# Check if working directory is clean
if ! git diff-index --quiet HEAD --; then
print_error "Working directory is not clean. Please commit or stash your changes first."
exit 1
print_error "Working directory is not clean. Please commit or stash your changes first."
exit 1
fi
# Ask user for release version number
@@ -71,8 +72,8 @@ read -r -p "Enter release version number (e.g., 1.2.3): " VERSION
# Validate version format (basic semver check)
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
print_error "Invalid version format. Please use semantic versioning (e.g., 1.2.3)"
exit 1
print_error "Invalid version format. Please use semantic versioning (e.g., 1.2.3)"
exit 1
fi
print_info "Release version: $VERSION"
@@ -80,8 +81,8 @@ print_info "Release version: $VERSION"
# Check if branch already exists
BRANCH_NAME="release/$VERSION"
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
print_error "Branch $BRANCH_NAME already exists!"
exit 1
print_error "Branch $BRANCH_NAME already exists!"
exit 1
fi
# Create and checkout new branch
@@ -128,8 +129,8 @@ cd -
print_info "Updating CHANGELOG..."
if [ ! -f "CHANGELOG.md" ]; then
print_error "CHANGELOG.md not found in project root!"
exit 1
print_error "CHANGELOG.md not found in project root!"
exit 1
fi
# Get current date in YYYY-MM-DD format
@@ -137,21 +138,23 @@ CURRENT_DATE=$(date +%Y-%m-%d)
# Replace [Unreleased] with [version number] - YYYY-MM-DD
if grep -q '\[Unreleased\]' CHANGELOG.md; then
sed -i.bak "s/\[Unreleased\]/[$VERSION] - $CURRENT_DATE/" CHANGELOG.md
sed -i.bak "s/\[Unreleased\]/[$VERSION] - $CURRENT_DATE/" CHANGELOG.md
# Add new [Unreleased] section after the header
# This adds it after the line containing "Semantic Versioning"
sed -i.bak "/Semantic Versioning/a\\
# Add new [Unreleased] section after the header
# This adds it after the line containing "Semantic Versioning"
sed -i.bak "/Semantic Versioning/a\\
\\
## [Unreleased]
" CHANGELOG.md
rm CHANGELOG.md.bak
print_info "Updated CHANGELOG.md"
rm CHANGELOG.md.bak
print_info "Updated CHANGELOG.md"
else
print_warning "Could not find [Unreleased] section in CHANGELOG.md"
print_warning "Could not find [Unreleased] section in CHANGELOG.md"
fi
# Summary
echo ""
print_info "Release preparation complete!"
@@ -175,4 +178,4 @@ print_warning "Next steps:"
echo " 1. Review the changes: git status"
echo " 2. Commit the changes: git add . && git commit -m 'Release $VERSION'"
echo " 3. Push the branch: git push origin $BRANCH_NAME"
echo ""
echo ""
+2 -6
View File
@@ -1,8 +1,6 @@
# ---- Front-end image ----
FROM node:22-alpine AS frontend-deps
RUN apk upgrade --no-cache ca-certificates
WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
@@ -43,8 +41,6 @@ COPY ./docker/dinum-frontend/fonts/ \
# ---- Addons builder image ----
FROM node:20-alpine AS addons-builder
RUN apk upgrade --no-cache ca-certificates
WORKDIR /home/addons/outlook
COPY ./src/addons/outlook/package.json ./package.json
@@ -58,12 +54,12 @@ RUN npx webpack --mode production
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.30.5-alpine3.24 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
USER root
RUN apk upgrade --no-cache libexpat && \
apk add --no-cache --upgrade 'pcre2>=10.49-r0' 'tiff>=4.7.2-r0' \
'libx11>=1.8.14-r0' 'ca-certificates>=20261020-r0' && \
'libx11>=1.8.14-r0' && \
apk del curl
USER nginx
+19 -87
View File
@@ -94,38 +94,8 @@ paths:
token_type: "Bearer"
expires_in: 3600
scope: "rooms:list rooms:retrieve rooms:create"
'400':
description: |
Invalid request. When several parameters are invalid, `error` reports
the most relevant one (client credentials first, then grant type) and
`error_description` lists every invalid parameter.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
examples:
missingClientCredentials:
summary: Missing client credentials
value:
error: "invalid_client"
error_description: "client_id : This field is required.; client_secret : This field is required."
unsupportedGrantType:
summary: Unsupported grant type
value:
error: "unsupported_grant_type"
error_description: "grant_type : 'authorization_code' is not a valid choice."
missingParameter:
summary: Missing required parameter
value:
error: "invalid_request"
error_description: "grant_type : This field is required."
invalidScope:
summary: Scope is not a valid email address
value:
error: "invalid_scope"
error_description: "Scope should be a valid email address."
'401':
description: Client authentication failed
description: Authentication failed
content:
application/json:
schema:
@@ -134,13 +104,22 @@ paths:
invalidCredentials:
summary: Invalid credentials
value:
error: "invalid_client"
error_description: "Invalid credentials"
error: "Invalid credentials"
inactiveApplication:
summary: Application is inactive
value:
error: "invalid_client"
error_description: "Application is inactive"
error: "Application is inactive"
'400':
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
examples:
userNotFound:
summary: User not found
value:
error: "User not found."
'403':
description: Access denied - cannot delegate user
content:
@@ -151,36 +130,7 @@ paths:
delegationDenied:
summary: Domain not authorized
value:
error: "invalid_scope"
error_description: "This application is not authorized for this email domain."
'404':
description: |
User not found. Only returned when the application is not allowed
to create users.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
examples:
userNotFound:
summary: User not found
value:
error: "invalid_scope"
error_description: "User not found."
'409':
description: |
The user could not be created or retrieved because of a concurrent
request. Retrying the request should succeed.
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
examples:
provisionalUserConflict:
summary: Concurrent user creation
value:
error: "server_error"
error_description: "Failed to create or retrieve provisional user."
error: "This application is not authorized for this email domain."
'415':
description: |
Unsupported media type. The request body must be sent as
@@ -188,13 +138,12 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthError'
$ref: '#/components/schemas/Error'
examples:
unsupportedMediaType:
summary: Unsupported request content type
value:
error: "invalid_request"
error_description: "Unsupported media type 'text/plain' in request."
detail: 'Unsupported media type "text/plain" in request.'
/rooms:
get:
@@ -603,28 +552,11 @@ components:
OAuthError:
type: object
description: OAuth 2.0 error response (RFC 6749, section 5.2)
required:
- error
- error_description
description: OAuth2-compliant error response
properties:
error:
type: string
description: |
Error code. `server_error` is not defined by RFC 6749 for the token
endpoint and is only returned on an unexpected server-side failure.
enum:
- invalid_request
- invalid_client
- unsupported_grant_type
- invalid_scope
- server_error
example: "invalid_client"
error_description:
type: string
description: |
Human-readable description of the error. Contains printable ASCII
characters only, without double quotes or backslashes.
description: Human-readable error description
example: "Invalid credentials"
Error:
+2 -19
View File
@@ -393,28 +393,11 @@ components:
OAuthError:
type: object
description: OAuth 2.0 error response (RFC 6749, section 5.2)
required:
- error
- error_description
description: OAuth2-compliant error response
properties:
error:
type: string
description: |
Error code. `server_error` is not defined by RFC 6749 for the token
endpoint and is only returned on an unexpected server-side failure.
enum:
- invalid_request
- invalid_client
- unsupported_grant_type
- invalid_scope
- server_error
example: "invalid_client"
error_description:
type: string
description: |
Human-readable description of the error. Contains printable ASCII
characters only, without double quotes or backslashes.
description: Human-readable error description
example: "Invalid credentials"
Error:
+3 -4
View File
@@ -1,9 +1,8 @@
FROM python:3.14.8-slim AS base
FROM python:3.14.7-slim AS base
# Install system dependencies required by LiveKit, fetching packages over HTTPS only for Bureautix proxy
RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sources \
&& apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libglib2.0-0 \
libgobject-2.0-0 \
libpcre2-8-0 \
@@ -19,7 +18,7 @@ ENV UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=0
# Install uv
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
WORKDIR /app
@@ -42,7 +41,7 @@ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=0
COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
WORKDIR /app
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.36.1"
version = "1.35.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.7.0",
+4 -4
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]]
name = "agents"
version = "1.36.1"
version = "1.35.0"
source = { virtual = "." }
dependencies = [
{ name = "boto3" },
@@ -314,11 +314,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.10.19"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
+2 -6
View File
@@ -52,14 +52,10 @@ _logger = logging.getLogger(__name__)
def error_message(response) -> Any:
"""Return the message of an error response, as DRF or the view wrote it.
An OAuth 2.0 error (RFC 6749 §5.2) carries its message in
``error_description``, its ``error`` being only a code.
"""
"""Return the message of an error response, as DRF or the view wrote it."""
data = getattr(response, "data", None)
if isinstance(data, Mapping):
return data.get("detail") or data.get("error_description") or data.get("error")
return data.get("detail") or data.get("error")
return None
@@ -1,98 +0,0 @@
"""OAuth 2.0 error responses for the external API token endpoint (RFC 6749 §5.2)."""
import unicodedata
from django.http import Http404
from rest_framework import exceptions as drf_exceptions
from rest_framework import status as drf_status
from core.api import exception_handler as default_exception_handler
OAUTH2_ERROR_INVALID_REQUEST = "invalid_request"
OAUTH2_ERROR_INVALID_CLIENT = "invalid_client"
OAUTH2_ERROR_UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"
OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope"
OAUTH2_ERROR_SERVER_ERROR = "server_error"
OAUTH2_CLIENT_CREDENTIALS_FIELDS = {"client_id", "client_secret"}
class OAuth2Error(drf_exceptions.APIException):
"""Token endpoint error carrying an RFC 6749 error code as its DRF code."""
def __init__(self, error, description, status_code=drf_status.HTTP_400_BAD_REQUEST):
super().__init__(detail=description, code=error)
self.status_code = status_code
def _get_validation_error_code(codes):
"""Map serializer error codes to the most relevant OAuth 2.0 error code."""
if OAUTH2_CLIENT_CREDENTIALS_FIELDS & codes.keys():
return OAUTH2_ERROR_INVALID_CLIENT
if "invalid_choice" in codes.get("grant_type", []):
return OAUTH2_ERROR_UNSUPPORTED_GRANT_TYPE
return OAUTH2_ERROR_INVALID_REQUEST
def _get_error_code(exc):
"""Return the OAuth 2.0 error code matching an exception."""
if isinstance(exc, OAuth2Error):
return exc.get_codes()
if isinstance(exc, drf_exceptions.ValidationError) and isinstance(exc.detail, dict):
return _get_validation_error_code(exc.get_codes())
if isinstance(
exc, (drf_exceptions.AuthenticationFailed, drf_exceptions.NotAuthenticated)
):
return OAUTH2_ERROR_INVALID_CLIENT
return OAUTH2_ERROR_INVALID_REQUEST
def _format_error_description(data):
"""Flatten DRF error data into "field : message; field : message"."""
if isinstance(data, dict):
if data.keys() == {"detail"}:
return _format_error_description(data["detail"])
return "; ".join(
f"{field} : {_format_error_description(messages)}"
for field, messages in data.items()
)
if isinstance(data, list):
return " ".join(_format_error_description(message) for message in data)
return str(data)
def _sanitize_error_description(description):
"""Restrict a description to the characters allowed by RFC 6749 §5.2.
Only printable ASCII is allowed, except for the double quote and the backslash.
Translated messages are transliterated so they stay readable ("é" becomes "e").
"""
ascii_description = (
unicodedata.normalize("NFKD", description)
.encode("ascii", "ignore")
.decode("ascii")
.replace('"', "'")
.replace("\\", "/")
)
return "".join(char for char in ascii_description if " " <= char <= "~")
def oauth2_exception_handler(exc, context):
"""Render errors as RFC 6749 §5.2 responses: {"error", "error_description"}.
Status codes and headers set by the default handler are kept.
"""
response = default_exception_handler(exc, context)
# A disabled feature flag raises Http404: keep it looking like a missing route.
if response is None or isinstance(exc, Http404):
return response
response.data = {
"error": _get_error_code(exc),
"error_description": _sanitize_error_description(
_format_error_description(response.data)
),
}
return response
+19 -25
View File
@@ -35,7 +35,7 @@ from ..services.provisional_user_service import (
ProvisionalUserIntegrityError,
ProvisionalUserService,
)
from . import authentication, exceptions, permissions, serializers
from . import authentication, permissions, serializers
class ApplicationViewSet(audit.AuditViewMixin, viewsets.ViewSet):
@@ -44,10 +44,6 @@ class ApplicationViewSet(audit.AuditViewMixin, viewsets.ViewSet):
audit_actor = None
audit_claimed_client_id = None
def get_exception_handler(self):
"""Render errors as OAuth 2.0 error responses (RFC 6749 section 5.2)."""
return exceptions.oauth2_exception_handler
@decorators.action(
detail=False,
methods=["post"],
@@ -96,33 +92,31 @@ class ApplicationViewSet(audit.AuditViewMixin, viewsets.ViewSet):
try:
validate_email(email)
except ValidationError as e:
raise exceptions.OAuth2Error(
exceptions.OAUTH2_ERROR_INVALID_SCOPE,
"Scope should be a valid email address.",
) from e
except ValidationError:
return drf_response.Response(
{
"error": "Scope should be a valid email address.",
},
status=drf_status.HTTP_400_BAD_REQUEST,
)
if not application.can_delegate_email(email):
raise exceptions.OAuth2Error(
exceptions.OAUTH2_ERROR_INVALID_SCOPE,
"This application is not authorized for this email domain.",
status_code=drf_status.HTTP_403_FORBIDDEN,
return drf_response.Response(
{
"error": "This application is not authorized for this email domain.",
},
status=drf_status.HTTP_403_FORBIDDEN,
)
try:
user, created = ProvisionalUserService().get_or_create(email, client_id)
except ProvisionalUserCreationDisabledError as not_found_error:
raise exceptions.OAuth2Error(
exceptions.OAUTH2_ERROR_INVALID_SCOPE,
"User not found.",
status_code=drf_status.HTTP_404_NOT_FOUND,
) from not_found_error
except ProvisionalUserIntegrityError as integrity_error:
raise exceptions.OAuth2Error(
exceptions.OAUTH2_ERROR_SERVER_ERROR,
"Failed to create or retrieve provisional user.",
status_code=drf_status.HTTP_409_CONFLICT,
) from integrity_error
raise drf_exceptions.NotFound("User not found.") from not_found_error
except ProvisionalUserIntegrityError:
return drf_response.Response(
{"error": "Failed to create or retrieve provisional user."},
status=drf_status.HTTP_409_CONFLICT,
)
scope = " ".join(application.scopes or [])
+1 -18
View File
@@ -33,7 +33,6 @@ class ThingViewSet(audit.AuditViewMixin, viewsets.ViewSet):
permission_classes = []
audit_actions = {"list": "thing.list", "create": "thing.create"}
error = None
error_body = {"error": "Already exists."}
def list(self, request):
if self.error is not None:
@@ -42,7 +41,7 @@ class ThingViewSet(audit.AuditViewMixin, viewsets.ViewSet):
return Response([])
def create(self, request):
return Response(self.error_body, status=409)
return Response({"error": "Already exists."}, status=409)
def destroy(self, request, pk=None):
return Response(status=204)
@@ -199,22 +198,6 @@ def test_error_message_of_a_view_response(audit_events):
assert event["error"] == {"message": "Already exists."}
def test_error_message_of_an_oauth2_error_response(audit_events):
"""An OAuth 2.0 error reports its description rather than its code."""
view = ThingViewSet.as_view(
{"post": "create"},
error_body={"error": "server_error", "error_description": "Already exists."},
)
response = view(APIRequestFactory().post("/things/"))
assert response.status_code == 409
[event] = find_events(audit_events, "thing.create")
assert event["error"] == {"message": "Already exists."}
def test_actions_missing_from_the_map_are_not_audited(audit_events):
"""Only the actions listed in ``audit_actions`` emit events."""
view = ThingViewSet.as_view({"delete": "destroy"})
+15 -124
View File
@@ -27,11 +27,6 @@ from core.services import provisional_user_service
pytestmark = pytest.mark.django_db
INVALID_CREDENTIALS_ERROR = {
"error": "invalid_client",
"error_description": "Invalid credentials",
}
def test_api_applications_generate_token_application_disabled(settings):
"""When APPLICATION_ENABLED is False, the endpoint should return 404."""
@@ -58,7 +53,6 @@ def test_api_applications_generate_token_application_disabled(settings):
)
assert response.status_code == 404
assert response.json() == {"detail": "Not found."}
def test_api_applications_generate_token_success(settings):
@@ -146,7 +140,7 @@ def test_api_applications_generate_token_invalid_credentials_query_count(
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
def test_api_applications_generate_token_form_urlencoded(settings):
@@ -207,7 +201,7 @@ def test_api_applications_generate_token_form_urlencoded_invalid_credentials():
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
def test_api_applications_generate_token_form_urlencoded_missing_fields():
@@ -221,14 +215,8 @@ def test_api_applications_generate_token_form_urlencoded_missing_fields():
)
assert response.status_code == 400
assert response.json() == {
"error": "invalid_client",
"error_description": (
"client_id : This field is required.; "
"client_secret : This field is required.; "
"scope : This field is required."
),
}
for field in ("client_id", "client_secret", "scope"):
assert field in response.data
def test_api_applications_generate_token_form_urlencoded_invalid_grant_type():
@@ -252,12 +240,7 @@ def test_api_applications_generate_token_form_urlencoded_invalid_grant_type():
)
assert response.status_code == 400
assert response.json() == {
"error": "unsupported_grant_type",
"error_description": (
"grant_type : 'authorization_code' is not a valid choice."
),
}
assert "grant_type" in response.data
def test_api_applications_generate_token_form_urlencoded_special_characters():
@@ -300,75 +283,6 @@ def test_api_applications_generate_token_unsupported_media_type():
)
assert response.status_code == 415
assert response.json() == {
"error": "invalid_request",
"error_description": "Unsupported media type 'text/plain' in request.",
}
def test_api_applications_generate_token_missing_fields():
"""Missing fields should return an RFC 6749 error listing every invalid
field, reported as "invalid_client" when client credentials are missing."""
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
{},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"error": "invalid_client",
"error_description": (
"client_id : This field is required.; "
"client_secret : This field is required.; "
"grant_type : This field is required.; "
"scope : This field is required."
),
}
def test_api_applications_generate_token_missing_grant_type():
"""A request with client credentials but no grant_type should return
an "invalid_request" error."""
user = UserFactory(email="user@example.com")
plain_secret = "test-secret-123"
application = ApplicationFactory(client_secret=plain_secret, is_active=True)
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
{
"client_id": application.client_id,
"client_secret": plain_secret,
"scope": user.email,
},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"error": "invalid_request",
"error_description": "grant_type : This field is required.",
}
def test_api_applications_generate_token_error_description_charset():
"""Translated error descriptions should only contain the characters
allowed by RFC 6749 section 5.2: printable ASCII, except double quotes
and backslashes."""
user = UserFactory(email="user@example.com")
application = ApplicationFactory(is_active=True)
response = _post_token(
application.client_id, "", user.email, HTTP_ACCEPT_LANGUAGE="fr-fr"
)
assert response.status_code == 400
assert response.json() == {
"error": "invalid_client",
"error_description": "client_secret : Ce champ ne peut etre vide.",
}
def test_api_applications_generate_token_invalid_client_id():
@@ -388,7 +302,7 @@ def test_api_applications_generate_token_invalid_client_id():
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
def test_api_applications_generate_token_invalid_client_secret():
@@ -409,7 +323,7 @@ def test_api_applications_generate_token_invalid_client_secret():
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
def test_token_unknown_client_id_with_valid_secret():
@@ -430,7 +344,7 @@ def test_token_unknown_client_id_with_valid_secret():
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
def test_token_rejects_secret_owned_by_another_application():
@@ -452,7 +366,7 @@ def test_token_rejects_secret_owned_by_another_application():
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
def test_api_applications_generate_token_inactive_application():
@@ -474,10 +388,7 @@ def test_api_applications_generate_token_inactive_application():
)
assert response.status_code == 401
assert response.json() == {
"error": "invalid_client",
"error_description": "Application is inactive",
}
assert "Application is inactive" in str(response.data)
def test_api_applications_generate_token_inactive_application_wrong_secret():
@@ -498,7 +409,7 @@ def test_api_applications_generate_token_inactive_application_wrong_secret():
)
assert response.status_code == 401
assert response.json() == INVALID_CREDENTIALS_ERROR
assert "Invalid credentials" in str(response.data)
assert "inactive" not in str(response.data).lower()
@@ -520,10 +431,7 @@ def test_api_applications_generate_token_invalid_email_format():
)
assert response.status_code == 400
assert response.json() == {
"error": "invalid_scope",
"error_description": "Scope should be a valid email address.",
}
assert "scope should be a valid email address." in str(response.data).lower()
def test_api_applications_generate_token_domain_not_authorized():
@@ -546,12 +454,7 @@ def test_api_applications_generate_token_domain_not_authorized():
)
assert response.status_code == 403
assert response.json() == {
"error": "invalid_scope",
"error_description": (
"This application is not authorized for this email domain."
),
}
assert "not authorized for this email domain" in str(response.data)
def test_api_applications_generate_token_domain_authorized():
@@ -599,10 +502,7 @@ def test_api_applications_generate_token_user_not_found():
)
assert response.status_code == 404
assert response.json() == {
"error": "invalid_scope",
"error_description": "User not found.",
}
assert "User not found" in str(response.data)
@freeze_time("2023-01-15 12:00:00")
@@ -815,7 +715,7 @@ def test_api_applications_token_new_user_race_condition(mock_get_by_email, setti
def test_api_applications_token_new_user_race_condition_unrecoverable(
mock_get_or_create, settings
):
"""Should return 409 when ProvisionalUserIntegrityError is raised."""
"""Should return 500 when ProvisionalUserIntegrityError is raised."""
settings.APPLICATION_ALLOW_USER_CREATION = True
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
@@ -839,10 +739,6 @@ def test_api_applications_token_new_user_race_condition_unrecoverable(
)
assert response.status_code == 409
assert response.json() == {
"error": "server_error",
"error_description": "Failed to create or retrieve provisional user.",
}
assert mock_get_or_create.call_count == 1
@@ -1173,9 +1069,6 @@ def test_api_applications_generate_token_domain_denied_is_audited(audit_events):
assert event["entity"]["target"]["id"] == str(application.pk)
assert event["lasuite"]["details"] == {"requested_domain": "random.com"}
assert event["http"]["response"] == {"status_code": 403}
assert event["error"] == {
"message": "This application is not authorized for this email domain."
}
def test_api_applications_generate_token_invalid_email_is_audited(audit_events):
@@ -1191,7 +1084,6 @@ def test_api_applications_generate_token_invalid_email_is_audited(audit_events):
assert event["event"]["reason"] == "validation_error"
assert event["lasuite"]["actor"] == {"type": "application"}
assert event["http"]["response"] == {"status_code": 400}
assert event["error"] == {"message": "Scope should be a valid email address."}
assert "details" not in event["lasuite"]
@@ -1210,7 +1102,6 @@ def test_api_applications_generate_token_unknown_user_is_audited(audit_events):
assert event["event"]["reason"] == "not_found"
assert event["lasuite"]["details"] == {"requested_domain": "example.com"}
assert event["http"]["response"] == {"status_code": 404}
assert event["error"] == {"message": "User not found."}
def test_api_applications_generate_token_provisioning_is_audited(
+1 -2
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.36.1"
version = "1.35.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -66,7 +66,6 @@ dependencies = [
"urllib3==2.8.0",
"phonenumbers==9.0.37",
"cryptography==50.0.1", # CVE-2026-69247
"pillow==13.0.0", # CVE-2026-112403
]
[project.urls]
+60 -62
View File
@@ -316,11 +316,11 @@ redis = [
[[package]]
name = "certifi"
version = "2026.10.19"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
@@ -1339,7 +1339,7 @@ wheels = [
[[package]]
name = "meet"
version = "1.36.1"
version = "1.35.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -1373,7 +1373,6 @@ dependencies = [
{ name = "mozilla-django-oidc" },
{ name = "nested-multipart-parser" },
{ name = "phonenumbers" },
{ name = "pillow" },
{ name = "posthog" },
{ name = "psycopg", extra = ["binary", "pool"] },
{ name = "pydantic" },
@@ -1440,7 +1439,6 @@ requires-dist = [
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
{ name = "phonenumbers", specifier = "==9.0.37" },
{ name = "pillow", specifier = "==13.0.0" },
{ name = "posthog", specifier = "==7.44.0" },
{ name = "psycopg", extras = ["binary", "pool"], specifier = "==3.3.4" },
{ name = "pydantic", specifier = "==2.13.4" },
@@ -1666,64 +1664,64 @@ wheels = [
[[package]]
name = "pillow"
version = "13.0.0"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/e2/9132d38df320e8099e2642d9fa538f18a7dfebf68a875a31725dc2a115aa/pillow-13.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00", size = 4190620, upload-time = "2026-10-16T22:16:02.527Z" },
{ url = "https://files.pythonhosted.org/packages/cb/be/436dac0996876f65ef1916ffb711c5ef4af2ebc57414b6f16642e07f6b47/pillow-13.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033", size = 4290286, upload-time = "2026-10-16T22:16:04.861Z" },
{ url = "https://files.pythonhosted.org/packages/60/16/7c1007162e4169ea0d5bec47e2e0b2174f56c50cf5b8c02a1dc23e41037e/pillow-13.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599", size = 3723169, upload-time = "2026-10-16T22:16:06.956Z" },
{ url = "https://files.pythonhosted.org/packages/08/bc/578ab2eaa04705a8978bd9be0fe15c3d43a8fc95097c4f7e39540e79e4fa/pillow-13.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe", size = 5568469, upload-time = "2026-10-16T22:16:08.664Z" },
{ url = "https://files.pythonhosted.org/packages/98/c8/ccede3dcc4f274b80fb49199eed1ef34ba05566c8b71976b9d1f111da2e3/pillow-13.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313", size = 4852187, upload-time = "2026-10-16T22:16:10.693Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3c/cd0770e96d20bfd990db024bcbd43897ff3e88da1f4782cf90ea231b1d4f/pillow-13.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369", size = 6515908, upload-time = "2026-10-16T22:16:12.896Z" },
{ url = "https://files.pythonhosted.org/packages/ef/fe/f5cdaa2d1c4086e757430f368c271a58ee29cfe7378e7ae7b9d964015c25/pillow-13.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582", size = 7142544, upload-time = "2026-10-16T22:16:14.844Z" },
{ url = "https://files.pythonhosted.org/packages/fb/b5/a54f6d44adbcd23737c8c12953f42be107e4cca64e247c0ca3a5b563061c/pillow-13.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81", size = 6613097, upload-time = "2026-10-16T22:16:16.893Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b4/9e6c1207c21b2e91db0d15c16d30f3701ce5add1bd4da0e787761e66cb6e/pillow-13.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502", size = 7224846, upload-time = "2026-10-16T22:16:19.195Z" },
{ url = "https://files.pythonhosted.org/packages/78/9f/d3c3cb0518f3dd1eee0ef501535f53d15597a53c5c4118d178086143397a/pillow-13.0.0-cp313-cp313-win32.whl", hash = "sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8", size = 6516013, upload-time = "2026-10-16T22:16:21.389Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ed/1b16fb66a37e1b9e41370b93b2c614c7da761a5b2a41dc333f7af4639962/pillow-13.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42", size = 7284777, upload-time = "2026-10-16T22:16:23.393Z" },
{ url = "https://files.pythonhosted.org/packages/87/7d/8d02eba9f7ecc87f8e90f731e032cc4773802f2a843763afd0c66a535ff4/pillow-13.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f", size = 2698826, upload-time = "2026-10-16T22:16:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/8e/45/fbad9c7f5e2f1745c373a434d46072bc84eddfefaa28b852425ac957fda6/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", size = 4190721, upload-time = "2026-10-16T22:16:27.524Z" },
{ url = "https://files.pythonhosted.org/packages/a2/bf/2074902a259a90692c5dbf132885d21671644cbbd3f690047516243a55db/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", size = 4290236, upload-time = "2026-10-16T22:16:29.562Z" },
{ url = "https://files.pythonhosted.org/packages/ff/a5/3d1bb93b8ea38d15d21356c1b8e9f50971ac592aaa248f7fb71739094c0e/pillow-13.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", size = 3723093, upload-time = "2026-10-16T22:16:31.249Z" },
{ url = "https://files.pythonhosted.org/packages/84/a1/be060135d85187b25fa8b2573786c0e45c62ae27527fff5257675eb97f4a/pillow-13.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4", size = 5572253, upload-time = "2026-10-16T22:16:33.103Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f4/0bf6f1ac9ba7ee2ffe9914aeb410a1035a0c1a3c42f122388d7fc950bc4b/pillow-13.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d", size = 4852269, upload-time = "2026-10-16T22:16:35.102Z" },
{ url = "https://files.pythonhosted.org/packages/83/16/b603274617b3e6728da3a0133d4fba1ea8d7fbe0c4ea5fef9cbffa398b8b/pillow-13.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", size = 6516058, upload-time = "2026-10-16T22:16:37.228Z" },
{ url = "https://files.pythonhosted.org/packages/1d/f2/1371707ce438ba9a43f9d24089828494ec6f9396e28cd7903aa704dd6306/pillow-13.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", size = 7142574, upload-time = "2026-10-16T22:16:39.172Z" },
{ url = "https://files.pythonhosted.org/packages/b9/a9/62cf68587f910bc87c5951bcb6e9430c7643a65076adf5f18b4bf350e2bd/pillow-13.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", size = 6614295, upload-time = "2026-10-16T22:16:41.117Z" },
{ url = "https://files.pythonhosted.org/packages/6c/c9/9e9b48178649730299be0bafbf5f810a5b6977538f5a92d47e4ba5f5d608/pillow-13.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", size = 7224841, upload-time = "2026-10-16T22:16:43.118Z" },
{ url = "https://files.pythonhosted.org/packages/8f/48/c44e91384b1b6c072a714a63d00608acc16a6a968735e478e4be4384e5ac/pillow-13.0.0-cp314-cp314-win32.whl", hash = "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", size = 6508694, upload-time = "2026-10-16T22:16:45.883Z" },
{ url = "https://files.pythonhosted.org/packages/50/e2/7ad17747517e4c813dca78257d6bb593466f8231baf1384bc98d92745edb/pillow-13.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", size = 7281830, upload-time = "2026-10-16T22:16:47.725Z" },
{ url = "https://files.pythonhosted.org/packages/41/9f/e5b51f2ce8ef4563215334035403ce61a244f93a2e65c24a940131618f87/pillow-13.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", size = 2695468, upload-time = "2026-10-16T22:16:49.515Z" },
{ url = "https://files.pythonhosted.org/packages/97/73/a53502f731f6c84805a4ce87a8881e86d07a65aee64728b900d112dc703e/pillow-13.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa", size = 5578196, upload-time = "2026-10-16T22:16:51.291Z" },
{ url = "https://files.pythonhosted.org/packages/21/50/7e949959a56b5559c78ef172b70fc6025706acb289f3f648a15329e4fa6f/pillow-13.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f", size = 4857276, upload-time = "2026-10-16T22:16:53.399Z" },
{ url = "https://files.pythonhosted.org/packages/c9/52/43ded0ac08e77e39d17bd6d894162572201b5e8fdc4a022b14c55a1960a4/pillow-13.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", size = 6607008, upload-time = "2026-10-16T22:16:55.525Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2e/4a57ed216d5bf877387cba4d44c02aa87abe51ad2e06404aa6b597c4da39/pillow-13.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", size = 7224281, upload-time = "2026-10-16T22:16:57.738Z" },
{ url = "https://files.pythonhosted.org/packages/5c/3d/2929b248e135752d63aaeabf53f6de797e905b7b6263f9c1e2adade20aba/pillow-13.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", size = 6695600, upload-time = "2026-10-16T22:16:59.842Z" },
{ url = "https://files.pythonhosted.org/packages/bc/33/fb0da992cf084ed1ee6ee05e96b6191e35c351eb707ca2df874feb24ce47/pillow-13.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", size = 7300589, upload-time = "2026-10-16T22:17:01.686Z" },
{ url = "https://files.pythonhosted.org/packages/bb/af/c30dd9ef6a3dce84ecb1b2393cac629047a32e66279056c6cb46c3ffd65c/pillow-13.0.0-cp314-cp314t-win32.whl", hash = "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", size = 6517204, upload-time = "2026-10-16T22:17:03.722Z" },
{ url = "https://files.pythonhosted.org/packages/85/d5/ef3a8ae7609c8ab26570abf18194882534b480d245a9148b895a2262c004/pillow-13.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", size = 7286695, upload-time = "2026-10-16T22:17:05.543Z" },
{ url = "https://files.pythonhosted.org/packages/84/3c/0861db0c69a03689ebe3e7599d35668a414deb5aaafd8255215e4e62e3d3/pillow-13.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", size = 2698098, upload-time = "2026-10-16T22:17:07.674Z" },
{ url = "https://files.pythonhosted.org/packages/6e/eb/e86a41962a3a31ad495a3d6a912d9002b01bba4a6fa71c28703abdca7f69/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521", size = 4191213, upload-time = "2026-10-16T22:17:09.449Z" },
{ url = "https://files.pythonhosted.org/packages/dd/04/7e7445e706afec84ef06dc25b2c77f9cecff242c23730c62928ad1089d4a/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc", size = 4290650, upload-time = "2026-10-16T22:17:11.196Z" },
{ url = "https://files.pythonhosted.org/packages/46/85/d534089068ac40c0f61f650389a296c812eab745a8a733f81feec39ae955/pillow-13.0.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac", size = 3723078, upload-time = "2026-10-16T22:17:12.772Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2b/00b5c3b401df538b5dbe311d3899730fae5ba1e21cb8b80a128887ac6747/pillow-13.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d", size = 5572292, upload-time = "2026-10-16T22:17:14.539Z" },
{ url = "https://files.pythonhosted.org/packages/72/f5/a6b0610f946e9b54ae888ba7ad29d6e2f8d9b11f69e212d70416b7186c09/pillow-13.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782", size = 4852222, upload-time = "2026-10-16T22:17:16.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/d0/c0ff25bb0427ec70f32aac995453321fc3c4494a46c7292ff02d9d260b04/pillow-13.0.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071", size = 6527349, upload-time = "2026-10-16T22:17:18.534Z" },
{ url = "https://files.pythonhosted.org/packages/85/11/e46fa0bcc0fc117f1eacc206b3d51f04d583fab328b4b740672d0438d83f/pillow-13.0.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf", size = 7152873, upload-time = "2026-10-16T22:17:20.415Z" },
{ url = "https://files.pythonhosted.org/packages/7c/53/2bdeca705f3a58679fa9201233193c86a30238340ca29b3a152c3fcc719b/pillow-13.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3", size = 6626871, upload-time = "2026-10-16T22:17:22.361Z" },
{ url = "https://files.pythonhosted.org/packages/14/de/3829030f209722ade3753bb456b805a0e7f885cf885b483407365c82e9ca/pillow-13.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932", size = 7236439, upload-time = "2026-10-16T22:17:24.352Z" },
{ url = "https://files.pythonhosted.org/packages/44/b1/67fc37388382bc036a74f04da5bb6fcbf90ec3ac1f45893e06176c373fd3/pillow-13.0.0-cp315-cp315-win32.whl", hash = "sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81", size = 6508688, upload-time = "2026-10-16T22:17:26.314Z" },
{ url = "https://files.pythonhosted.org/packages/44/05/fdb6a79aee10861165526b41b7b440e36ee1ff58613b2fbefe3acbf06d4f/pillow-13.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6", size = 7281107, upload-time = "2026-10-16T22:17:28.198Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9c/6c6f7dc2a566467baf121394e83de25ac287cffe34cc030726beb4be0224/pillow-13.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0", size = 2695513, upload-time = "2026-10-16T22:17:30.013Z" },
{ url = "https://files.pythonhosted.org/packages/23/16/e9120defdb8bff693b582d035312acc6f530cdc2bc5238a87f410efccd82/pillow-13.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f", size = 5578129, upload-time = "2026-10-16T22:17:31.642Z" },
{ url = "https://files.pythonhosted.org/packages/47/22/76faf2bb2448f48a84cf894c1e2bb1090d7d2b1aa521ece131cb2f05d631/pillow-13.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88", size = 4857428, upload-time = "2026-10-16T22:17:33.43Z" },
{ url = "https://files.pythonhosted.org/packages/b3/64/0861ea4e03c50b14af9ed9dd8b9f388ab42f445c623a9fcbab19aef7aa55/pillow-13.0.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb", size = 6612582, upload-time = "2026-10-16T22:17:35.415Z" },
{ url = "https://files.pythonhosted.org/packages/5c/20/cd1e9b3f60a9f3d5ebe46da36955c26004092a501f7f2c42f9fcc26611bc/pillow-13.0.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe", size = 7230176, upload-time = "2026-10-16T22:17:37.362Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ef/ffb0a2fc85690714fb7ac662956949645d57f15bf4426f520da361e7b6e3/pillow-13.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22", size = 6703640, upload-time = "2026-10-16T22:17:39.313Z" },
{ url = "https://files.pythonhosted.org/packages/e0/c2/6402ee3ccb3363aeed8fdb3f9fe56703ac5377a539f0b81b53999d7e121f/pillow-13.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630", size = 7309231, upload-time = "2026-10-16T22:17:41.021Z" },
{ url = "https://files.pythonhosted.org/packages/fc/96/d6b82e8b8c06ad143b66b12e0b31051e4bf489b4fabca1f390a14fe1ebde/pillow-13.0.0-cp315-cp315t-win32.whl", hash = "sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2", size = 6517200, upload-time = "2026-10-16T22:17:42.886Z" },
{ url = "https://files.pythonhosted.org/packages/60/d4/0062e6005e9b270b165015aafd343364ac378ec5a685431ea42baad33357/pillow-13.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43", size = 7287488, upload-time = "2026-10-16T22:17:45.56Z" },
{ url = "https://files.pythonhosted.org/packages/e4/81/b8eb349746be7bce47dd95337c6634e84910af91b59aaa9159c034c614fa/pillow-13.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8", size = 2698073, upload-time = "2026-10-16T22:17:47.556Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
]
[[package]]
+2 -4
View File
@@ -1,7 +1,5 @@
FROM node:22-alpine AS frontend-deps
RUN apk upgrade --no-cache ca-certificates
USER node
WORKDIR /home/frontend/
@@ -47,12 +45,12 @@ ENV VITE_MEDIA_BASE_URL=${VITE_MEDIA_BASE_URL}
RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.30.5-alpine3.24 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
USER root
RUN apk upgrade --no-cache libexpat && \
apk add --no-cache --upgrade 'pcre2>=10.49-r0' 'tiff>=4.7.2-r0' \
'libx11>=1.8.14-r0' 'ca-certificates>=20261020-r0' && \
'libx11>=1.8.14-r0' && \
apk del curl
USER nginx
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.36.1",
"version": "1.35.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.36.1",
"version": "1.35.0",
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
"@fontsource-variable/lexend": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.36.1",
"version": "1.35.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -32,7 +32,6 @@ import { useDeviceInUse } from '../livekit/hooks/useDeviceInUse'
import { useDeviceMissing } from '../livekit/hooks/useDeviceMissing'
import { useJoinTracks } from '../livekit/hooks/useJoinTracks'
import { SilentMicDetector } from './SilentMicDetector'
import { type PreviewTrack, toggleTrack } from '../utils/toggleTrack'
const styles = {
page: css({
@@ -189,6 +188,22 @@ const styles = {
}),
}
type PreviewTrack = LocalAudioTrack | LocalVideoTrack
const toggleTrack =
(enabled: boolean, save: (v: boolean) => void, track?: PreviewTrack) =>
async () => {
save(!enabled)
try {
await (enabled ? track?.mute() : track?.unmute())
} catch (err) {
save(enabled)
reportError('join_preview_failure', err as Error, {
path: 'join_preview',
})
}
}
const switchTrackDevice =
(save: (id: string) => void, track?: PreviewTrack) => async (id: string) => {
try {
@@ -78,7 +78,7 @@ export const AudioDevicesControl = ({
{...trackProps}
isDisabled={!canPublishTrack}
kind={kind}
toggle={trackProps.toggle as () => Promise<boolean | undefined>}
toggle={trackProps.toggle as () => Promise<void>}
overrideToggleButtonProps={{
...(hideMenu
? {
@@ -44,7 +44,7 @@ export type ToggleDeviceProps<T extends ToggleSource> = {
toggle: (
forceState?: boolean,
captureOptions?: CaptureOptionsBySource<T>
) => Promise<boolean | undefined>
) => Promise<void | boolean | undefined>
context?: 'room' | 'join'
kind: 'audioinput' | 'videoinput'
overrideToggleButtonProps?: Partial<ToggleButtonProps>
@@ -155,8 +155,8 @@ export const ToggleDevice = <T extends ToggleSource>({
handler: async () => {
const nextState = !enabled
try {
const enabledAfter = await toggle(nextState)
if (enabledAfter !== nextState) return
const didChange = await toggle(nextState)
if (didChange === false) return
const message = t(nextState ? 'turnedOn' : 'turnedOff', {
keyPrefix: `selectDevice.${kind}`,
@@ -94,9 +94,9 @@ export const VideoDeviceControl = ({
const toggle = trackProps.toggle as (
forceState: boolean,
captureOptions: VideoCaptureOptions
) => Promise<boolean | undefined>
) => Promise<void>
return toggle(!trackProps.enabled, {
await toggle(!trackProps.enabled, {
processor: processor,
...(videoPublishResolution && {
resolution: VideoPresets[videoPublishResolution].resolution,
@@ -1,51 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { reportError } from '@/features/analytics/telemetry'
import { type PreviewTrack, toggleTrack } from './toggleTrack'
vi.mock('@/features/analytics/telemetry', () => ({ reportError: vi.fn() }))
const fakeTrack = (overrides: Partial<Record<'mute' | 'unmute', unknown>>) =>
({
mute: vi.fn().mockResolvedValue(undefined),
unmute: vi.fn().mockResolvedValue(undefined),
...overrides,
}) as unknown as PreviewTrack
describe('toggleTrack', () => {
it('resolves to false once an enabled track is muted', async () => {
const save = vi.fn()
const track = fakeTrack({})
await expect(toggleTrack(true, save, track)()).resolves.toBe(false)
expect(track.mute).toHaveBeenCalled()
expect(save).toHaveBeenCalledWith(false)
})
it('resolves to true once a disabled track is unmuted', async () => {
const save = vi.fn()
const track = fakeTrack({})
await expect(toggleTrack(false, save, track)()).resolves.toBe(true)
expect(track.unmute).toHaveBeenCalled()
expect(save).toHaveBeenCalledWith(true)
})
it('resolves to the initial state when the track fails to toggle', async () => {
const save = vi.fn()
const error = new Error('mute failed')
const track = fakeTrack({ mute: vi.fn().mockRejectedValue(error) })
await expect(toggleTrack(true, save, track)()).resolves.toBe(true)
expect(save.mock.calls).toEqual([[false], [true]])
expect(reportError).toHaveBeenCalledWith('join_preview_failure', error, {
path: 'join_preview',
})
})
it('resolves to the requested state when there is no track', async () => {
const save = vi.fn()
await expect(toggleTrack(false, save)()).resolves.toBe(true)
expect(save).toHaveBeenCalledWith(true)
})
})
@@ -1,24 +0,0 @@
import { type LocalAudioTrack, type LocalVideoTrack } from 'livekit-client'
import { reportError } from '@/features/analytics/telemetry'
export type PreviewTrack = LocalAudioTrack | LocalVideoTrack
/**
* Toggle a join screen preview track. Resolves to the enabled state the
* track ends in, so a failed mute or unmute resolves to the initial state.
*/
export const toggleTrack =
(enabled: boolean, save: (v: boolean) => void, track?: PreviewTrack) =>
async () => {
save(!enabled)
try {
await (enabled ? track?.mute() : track?.unmute())
return !enabled
} catch (err) {
save(enabled)
reportError('join_preview_failure', err as Error, {
path: 'join_preview',
})
return enabled
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.29
version: 0.0.28
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.36.1",
"version": "1.35.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.36.1",
"version": "1.35.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.6.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.36.1",
"version": "1.35.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.36.1",
"version": "1.35.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.36.1",
"version": "1.35.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.36.1",
"version": "1.35.0",
"author": "",
"license": "ISC",
"description": "",
+1 -3
View File
@@ -1,6 +1,4 @@
FROM python:3.13.16-alpine3.24 AS base
RUN apk add --no-cache --upgrade 'ca-certificates>=20261020-r0'
FROM python:3.13-alpine3.24 AS base
# Install ffmpeg for audio/video processing (format conversion, extraction, compression)
+1 -2
View File
@@ -1,10 +1,9 @@
[project]
name = "summary"
version = "1.36.1"
version = "1.35.0"
requires-python = ">=3.13"
dependencies = [
"certifi>=2026.10.19",
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
"pydantic>=2.5.0",
+4 -6
View File
@@ -126,11 +126,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.10.19"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
@@ -1484,12 +1484,11 @@ wheels = [
[[package]]
name = "summary"
version = "1.36.1"
version = "1.35.0"
source = { editable = "." }
dependencies = [
{ name = "boto3" },
{ name = "celery" },
{ name = "certifi" },
{ name = "dockerflow" },
{ name = "fastapi", extra = ["standard"] },
{ name = "langfuse" },
@@ -1514,7 +1513,6 @@ dev = [
requires-dist = [
{ name = "boto3", specifier = "==1.43.56" },
{ name = "celery", specifier = "==5.6.3" },
{ name = "certifi", specifier = ">=2026.10.19" },
{ name = "dockerflow", specifier = "==2026.3.4" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.105.0" },
{ name = "langfuse", specifier = "==4.14.1" },