Compare commits

..

17 Commits

Author SHA1 Message Date
lebaudantoine 15133f9d6b 🧑‍💻(helm) use YAML anchors to simplify Helm values for summary
Reduce duplication by introducing YAML anchors for configurations
shared across multiple services.

Most settings were nearly identical across the three summary
services, making them easier to maintain and update.
2026-03-25 13:43:19 +01:00
lebaudantoine bea1f18ab8 🗑️(helm) remove unused dev Helm values
The dev values are no longer in use and have not been used for over a year.
We primarily rely on the dev-keycloak values, and occasionally
the dev-dinum ones for testing on the Dinum-labeled frontend.

As a result, the unused dev values should be removed to reduce clutter
and simplify maintenance.
2026-03-25 13:29:57 +01:00
lebaudantoine d5a614d2b5 🐛(backend) fix regression in update-participant endpoint
Serialization hardening introduced a breaking change between the
frontend and backend. Adjust the Pydantic model to restore
compatibility.

Reinstate support for can_subscribe_metric, which is passed by
default from the frontend.
2026-03-25 12:20:45 +01:00
lebaudantoine 108db2e3e5 📌(backend) pin brevo-python to v2.x.x. in renovate
The SDK introduced breaking changes in newer versions. Lock the
dependency to v2.x.x to maintain compatibility.
2026-03-25 11:18:58 +01:00
renovate[bot] 73496406e8 ⬆️(dependencies) update python dependencies 2026-03-25 11:18:57 +01:00
lebaudantoine c5c96369c8 🔊(backend) remove email addresses from invitation failure logs
Email addresses are PII and should not appear in technical or
error logs.

Sanitize logging to avoid exposing sensitive user data when
invitation sending fails.
2026-03-25 09:52:43 +01:00
lebaudantoine e9f90e95b1 🔒️(backend) fix email disclosure in room invitation endpoint
Prevent invited participants from seeing each other's email
addresses when sending room invitations.

Ensure invitations are sent with proper isolation to avoid
mass PII disclosure.

This mitigates risks of email harvesting, spam, and phishing
through the platform.
2026-03-25 09:52:43 +01:00
lebaudantoine f57fbf2d35 🔖(minor) bump release to 1.12.0 2026-03-24 23:37:41 +01:00
Martin Weinelt 920f4558fc 🐛(backend): fix module inclusion with uv-build
After migrating to uv-build only the module matching the project name was
included in sdist/wheel packages. Without a src layout additional modules
need to be tracked manually to ship them in built packages.
2026-03-24 16:14:31 +01:00
lebaudantoine 9df901b9d6 📝(backend) clarify trailing slash requirement in API Swagger doc
Specify that POST routes require a trailing slash to avoid
confusion and incorrect usage from API consumers.
2026-03-24 15:31:58 +01:00
dependabot[bot] c09c440631 ⬆️️️(frontend) bump dompurify from 3.3.1 to 3.3.2 in /src/frontend
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.1...3.3.2)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 18:23:18 +01:00
dependabot[bot] cd7ce77074 ⬆️️️(frontend) bump hono from 4.12.2 to 4.12.7 in /src/frontend
Bumps [hono](https://github.com/honojs/hono) from 4.12.2 to 4.12.7.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.2...v4.12.7)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 18:08:38 +01:00
dependabot[bot] 6d3c26419d ⬆️️️(frontend) bump undici from 6.23.0 to 6.24.1 in /src/frontend
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 17:59:51 +01:00
dependabot[bot] 9dbc38984e ⬆️ Bump flatted from 3.3.1 to 3.4.2 in /src/frontend
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.1 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.1...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 17:50:57 +01:00
Michel-Marie MAUDET 1bd5a294e4 🐛(frontend) fix device selection not applying during conference
Await was missing, leading to inconsistent behavior between select component.
Michel-Marie fixed it.
2026-03-23 16:46:52 +01:00
Michel-Marie MAUDET 4d98ed4977 ♻️(backend) make SESSION_ENGINE configurable via environment variable
Make SESSION_ENGINE configurable through environment variable,
following the same values.Value() pattern already used for
SESSION_COOKIE_AGE. This enables OIDC backchannel-logout by
allowing users to set SESSION_ENGINE to db backend.

Closes #1037
2026-03-23 13:39:36 +01:00
Cyril bf32c073c6 💄(frontend) show OS-specific shortcut in participant tile hint
Participant tile hint uses formatShortcutLabel so Mac users see ⌘ not Ctrl.
2026-03-23 09:13:53 +01:00
88 changed files with 2073 additions and 1627 deletions
+33 -33
View File
@@ -48,12 +48,12 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
# -
# name: Run trivy scan
# uses: numerique-gouv/action-trivy-cache@main
# with:
# docker-build-args: '--target backend-production -f Dockerfile'
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
@@ -93,12 +93,12 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
# -
# name: Run trivy scan
# uses: numerique-gouv/action-trivy-cache@main
# with:
# docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
@@ -139,12 +139,12 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
# -
# name: Run trivy scan
# uses: numerique-gouv/action-trivy-cache@main
# with:
# docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
@@ -185,13 +185,13 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
# -
# name: Run trivy scan
# uses: numerique-gouv/action-trivy-cache@main
# continue-on-error: true
# with:
# docker-build-args: '-f src/summary/Dockerfile --target production'
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
continue-on-error: true
with:
docker-build-args: '-f src/summary/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
docker-context: './src/summary'
-
name: Build and push
@@ -233,14 +233,14 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
# -
# name: Run trivy scan
# uses: numerique-gouv/action-trivy-cache@main
# continue-on-error: true
# with:
# docker-build-args: '-f src/agents/Dockerfile --target production'
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
# docker-context: './src/agents'
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
continue-on-error: true
with:
docker-build-args: '-f src/agents/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
docker-context: './src/agents'
-
name: Build and push
uses: docker/build-push-action@v6
+40
View File
@@ -297,6 +297,46 @@ jobs:
- name: Run tests
run: uv run pytest -n 2
test-summary:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/summary
env:
APP_API_TOKEN: "test-api-token"
AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage"
AWS_S3_ENDPOINT_URL: "minio:9000"
AWS_S3_ACCESS_KEY_ID: "meet"
AWS_S3_SECRET_ACCESS_KEY: "password"
WHISPERX_BASE_URL: "https://configure-your-url.com"
WHISPERX_ASR_MODEL: "large-v2"
WHISPERX_API_KEY: "test-whisperx-secret"
WHISPERX_DEFAULT_LANGUAGE: "fr"
LLM_BASE_URL: "https://configure-your-url.com"
LLM_API_KEY: "test-llm-secret"
LLM_MODEL: "test-llm-model"
WEBHOOK_API_TOKEN: "test-webhook-secret"
WEBHOOK_URL: "https://configure-your-url.com"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: "pip"
- name: Install development dependencies
run: pip install --user .[dev]
- name: Run summary tests
run: ~/.local/bin/pytest
lint-front:
runs-on: ubuntu-latest
permissions:
+42 -2
View File
@@ -8,6 +8,41 @@ and this project adheres to
## [Unreleased]
### Changed
- ⬆️(dependencies) update python dependencies
### Fixed
- 🔒️(backend) fix email disclosure in room invitation endpoint #1200
- 🐛(backend) fix regression in update-participant endpoint #1204
## [1.12.0] - 2026-03-24
### Changed
- ♻️(backend) configurable SESSION_ENGINE #1038 #1154
- ♿️(frontend) fix sidepanel accessibility aria-label #1182
- ♿️(frontend) fix more tools heading hierarchy #1181
- ♿️(fronted) improve button descriptions for More tools actions #1184
- 💄(spinner) enforce spinner height #1183
- 💄(custom-background) add upload indicator with preview #1183
- ♿️(backend) improve logo accessibility in recording email notification #1092
- ♿️(summary) improve accessibility of transcription download link #1187
- 💄(frontend) show OS-specific shortcut in participant tile hint #1193
- ⬆️(frontend) bump flatted from 3.3.1 to 3.4.2 in /src/frontend #1188
- ⬆️(frontend) bump undici from 6.23.0 to 6.24.1 in /src/frontend
- ⬆️(frontend) bump hono from 4.12.2 to 4.12.7 in /src/frontend
- ⬆️(frontend) bump dompurify from 3.3.1 to 3.3.2 in /src/frontend
### Fixed
- 🐛(frontend) disable personal custom background while deleting #1183
- 🐛(frontend) auto-select new custom background when not logged in #1183
- 🐛(frontend) fix device selection not applying during conference #1156
## [1.11.0] - 2026-03-19
### Added
- ✨(helm) support celery with our Django backend #1124
@@ -15,20 +50,24 @@ and this project adheres to
- ✨(backend) add authenticated user rate throttling on request-entry #1129
- ✨(backend) expose `is_active` field for Application in Django admin #1133
- ✨(file-upload) disable by default & limit count by user #1141
- ✨(frontend) custom background #1067
### Changed
- ♿️(frontend) Caption text size setting for accessibility #1062
- ♿️(frontend) sync html lang attribute with i18n for screen readers #1111
- ♿️(frontend) improve MoreLink a11y and UX on home page #1112
- ♿(frontend) improve chat toast a11y for screen readers #1109
- ♿(frontend) improve ui and aria labels for help article links #1108
-(frontend) improve chat toast a11y for screen readers #1109
-(frontend) improve ui and aria labels for help article links #1108
- 🌐(frontend) improve German translation #1125
- 🔨(python-env) migrate meet main app to UV #1120
- ♻️(backend) align Application model field with `is_active` convention #1133
- 🔐(backend) avoids revealing the inactive status of an application #1135
- ⚡️(helm) reduce initialDelaySeconds and add periods seconds #1139
- 🔒️(backend) avoid information exposure through exception messages #1144
- ⬆️(dependencies) update PyJWT to v2.12.0 [SECURITY] #1151
- 📌(agents) unpin OpenSSL and related dependencies #1167
- ♿️(frontend) add caption font and background color customization #1122
### Fixed
@@ -36,6 +75,7 @@ and this project adheres to
- 🩹(backend) add page_size to pagination for room endpoints #1131
- 🐛(backend) refactor lobby throttling to use participant id #1129
- 🩹(backend) ignore non-recording uploads in storage webhook handler #1142
- 🐛(frontend) fix dimension mismatch in BackgroundCustomProcessor #1116
## [1.10.0] - 2026-03-05
+6
View File
@@ -191,6 +191,7 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
test: ## run project tests
@$(MAKE) test-back-parallel
@$(MAKE) test-summary
.PHONY: test
test-back: ## run back-end tests
@@ -203,6 +204,11 @@ test-back-parallel: ## run all back-end tests in parallel
bin/pytest -n auto $${args:-${1}}
.PHONY: test-back-parallel
test-summary: ## run summary tests
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
bin/pytest-summary $${args:-${1}}
.PHONY: test-summary
makemigrations: ## run django makemigrations for the Meet project.
@echo "$(BOLD)Running makemigrations$(RESET)"
@$(COMPOSE) up -d postgresql
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
_dc_run \
app-summary-dev \
python -m pytest "$@"
+3 -4
View File
@@ -61,11 +61,10 @@ services:
`docker compose up -d`
```
Your keycloak instance is now available on https://doc.yourdomain.tld
Your keycloak instance is now available on https://id.yourdomain.tld
> [!CAUTION]
> Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image. You can find available versions on [Keycloak registry](https://quay.io/repository/keycloak/keycloak?tab=tags).
```
## Creating an OIDC Client for Meet Application
@@ -76,7 +75,7 @@ Your keycloak instance is now available on https://doc.yourdomain.tld
3. Enter the name of the realm - `meet`.
4. Click "Create".
#### Step 2: Create a New Client
### Step 2: Create a New Client
1. Navigate to the "Clients" tab.
2. Click on the "Create client" button.
@@ -86,7 +85,7 @@ Your keycloak instance is now available on https://doc.yourdomain.tld
1. Set the "Web Origins" to the URL of your meet application - e.g. `https://meet.example.com`.
1. Click "Save".
#### Step 3: Get Client Credentials
### Step 3: Get Client Credentials
1. Go to the "Credentials" tab.
2. Copy the client ID (`meet` in this example) and the client secret.
+1
View File
@@ -190,6 +190,7 @@ paths:
'403':
$ref: '#/components/responses/ForbiddenError'
/rooms/:
post:
tags:
- Rooms
+1
View File
@@ -113,6 +113,7 @@ paths:
'403':
$ref: '#/components/responses/ForbiddenError'
/rooms/:
post:
tags:
- Rooms
+1 -1
View File
@@ -27,7 +27,7 @@ AWS_S3_DOMAIN_REPLACE=http://localhost:9000
AWS_S3_ENDPOINT_URL=http://minio:9000
AWS_S3_ACCESS_KEY_ID=meet
AWS_S3_SECRET_ACCESS_KEY=password
MEDIA_BASE_URL=http://localhost:8083
MEDIA_BASE_URL=http://localhost:3000
FILE_UPLOAD_ENABLED=True
# OIDC
+6
View File
@@ -36,6 +36,12 @@
"matchPackageNames": ["django"],
"allowedVersions": "<6.0.0"
},
{
"groupName": "allowed brevo versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["brevo-python"],
"allowedVersions": "<3.0.0"
},
{
"enabled": false,
"groupName": "ignored js dependencies",
-2
View File
@@ -4,8 +4,6 @@ FROM python:3.13-slim AS base
RUN apt-get update && apt-get install -y \
libglib2.0-0 \
libgobject-2.0-0 \
"openssl=3.5.4-1~deb13u2" \
"libssl3t64=3.5.4-1~deb13u2" \
&& rm -rf /var/lib/apt/lists/*
FROM base AS builder
+6 -6
View File
@@ -1,20 +1,20 @@
[project]
name = "agents"
version = "1.10.0"
version = "1.12.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.3.10",
"livekit-plugins-deepgram==1.3.10",
"livekit-plugins-silero==1.3.10",
"livekit-agents==1.4.5",
"livekit-plugins-deepgram==1.4.5",
"livekit-plugins-silero==1.4.5",
"livekit-plugins-kyutai-lasuite==0.0.6",
"python-dotenv==1.2.1",
"python-dotenv==1.2.2",
"protobuf==6.33.5"
]
[project.optional-dependencies]
dev = [
"ruff==0.14.4",
"ruff==0.15.6",
]
[build-system]
+4 -11
View File
@@ -303,6 +303,9 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
)
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
class ParticipantPermission(BaseModel):
"""Mirror the LiveKit ParticipantPermission protobuf.
@@ -313,9 +316,7 @@ class ParticipantPermission(BaseModel):
can_subscribe: bool | None = None
can_publish: bool | None = None
can_publish_data: bool | None = None
can_publish_sources: list[int] = Field(
default_factory=list
) # TrackSource enum values
can_publish_sources: list[TrackSource] = Field(default_factory=list)
hidden: bool | None = None
recorder: bool | None = None
can_update_metadata: bool | None = None
@@ -366,14 +367,6 @@ class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
f"Setting the following participant permissions is not allowed: "
f"{', '.join(suspicious_fields)}."
)
if permission.can_subscribe_metrics is not None:
raise serializers.ValidationError(
{
"permission": {
"can_subscribe_metrics": "This permission is not implemented."
}
}
)
return permission
-6
View File
@@ -73,9 +73,3 @@ class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle):
"""Throttle Anonymous user requesting room generation callback"""
scope = "creation_callback"
class SessionExchangeAnonRateThrottle(MonitoredAnonRateThrottle):
"""Throttle anonymous requests to the session exchange endpoint."""
scope = "session_exchange"
-61
View File
@@ -1,61 +0,0 @@
"""API endpoint for exchanging a one-time code for a session ID."""
import logging
from django.conf import settings
from django.core.cache import cache
from rest_framework import serializers, status
from rest_framework.decorators import api_view, permission_classes, throttle_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from core.api.throttling import SessionExchangeAnonRateThrottle
from .views import EXCHANGE_CODE_PREFIX
logger = logging.getLogger(__name__)
class SessionExchangeSerializer(serializers.Serializer):
"""Validates the exchange code request."""
code = serializers.CharField(max_length=64, min_length=16)
@api_view(["POST"])
@permission_classes([AllowAny])
@throttle_classes([SessionExchangeAnonRateThrottle])
def session_exchange(
request,
): # NOSONAR (S3752) POST-only, AllowAny is intentional: single-use code with 30s TTL and rate limiting
"""Exchange a one-time code for a session ID.
The code was generated during the OIDC callback and stored in cache
with a short TTL. This endpoint retrieves the session ID, deletes the
code (single-use), and returns the session ID to the native app.
"""
serializer = SessionExchangeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
cache_key = f"{EXCHANGE_CODE_PREFIX}{code}"
session_key = cache.get(cache_key)
if session_key is None:
logger.warning(
"Session exchange failed: invalid or expired code from %s",
request.META.get("REMOTE_ADDR"),
)
return Response(
{"detail": "Invalid or expired code."},
status=status.HTTP_400_BAD_REQUEST,
)
# Delete immediately — single use
cache.delete(cache_key)
cookie_name = getattr(settings, "SESSION_COOKIE_NAME", "sessionid")
logger.info("Session exchange successful from %s", request.META.get("REMOTE_ADDR"))
return Response({cookie_name: session_key})
-103
View File
@@ -1,103 +0,0 @@
"""Custom OIDC authentication views for native app support.
When a native app (iOS, Android, Desktop) initiates OIDC login, it sets
`returnTo` to a custom URL scheme (e.g. `visio://auth-callback`). After
the OIDC flow completes, Django sets the session cookie and redirects to
that URL. However, native apps cannot read browser cookies — they need
the session ID passed explicitly.
Instead of exposing the session ID directly in the redirect URL, this
module generates a short-lived, single-use exchange code. The native app
then exchanges this code for the session ID via a dedicated API endpoint.
"""
import uuid
from urllib.parse import urlencode, urlparse
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponseRedirect
from lasuite.oidc_login.views import (
OIDCAuthenticationCallbackView as BaseCallbackView,
OIDCAuthenticationRequestView as BaseRequestView,
)
# Cache key prefix and TTL for exchange codes
EXCHANGE_CODE_PREFIX = "auth_exchange:"
EXCHANGE_CODE_TTL = 30 # seconds
class NativeAppRedirect(HttpResponseRedirect):
"""HttpResponseRedirect subclass that allows native app custom URL schemes.
Django's HttpResponseRedirect only allows http, https, and ftp schemes.
Native apps use custom schemes (e.g. visio://) for deep links, which
Django rejects with DisallowedRedirect. This subclass extends
allowed_schemes with the configured native app schemes.
"""
allowed_schemes = HttpResponseRedirect.allowed_schemes + list(
getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", [])
)
class OIDCAuthenticationRequestView(BaseRequestView):
"""Custom authenticate view that preserves native app returnTo in session.
mozilla-django-oidc's get_next_url() rejects custom URL schemes
(e.g. visio://auth-callback) because url_has_allowed_host_and_scheme()
only allows http/https. We intercept the returnTo parameter and store
it directly in the session for whitelisted schemes, bypassing the
safety check (which is not relevant for native app deep links).
"""
def get(self, request):
redirect_field = getattr(settings, "OIDC_REDIRECT_FIELD_NAME", "returnTo")
return_to = request.GET.get(redirect_field, "")
parsed = urlparse(return_to)
allowed_schemes = getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", [])
response = super().get(request)
# Override oidc_login_next AFTER super() which set it to None
# via get_next_url() rejecting the custom scheme.
if parsed.scheme in allowed_schemes:
request.session["oidc_login_next"] = return_to
request.session.save()
return response
class OIDCAuthenticationCallbackView(BaseCallbackView):
"""Callback view that generates an exchange code for native app deep links."""
def login_success(self):
"""After successful login, append exchange code for whitelisted scheme redirects."""
# Temporarily remove native redirect from session to prevent
# super().login_success() from raising DisallowedRedirect when
# it tries HttpResponseRedirect with a custom scheme.
native_redirect = self.request.session.pop("oidc_login_next", None)
allowed_schemes = getattr(settings, "NATIVE_APP_REDIRECT_SCHEMES", [])
parsed = urlparse(native_redirect or "")
if native_redirect and parsed.scheme in allowed_schemes:
# Let super() redirect to the default URL (homepage)
super().login_success()
# Generate a short-lived, single-use exchange code
exchange_code = uuid.uuid4().hex
session_key = self.request.session.session_key
cache.set(
f"{EXCHANGE_CODE_PREFIX}{exchange_code}",
session_key,
EXCHANGE_CODE_TTL,
)
separator = "&" if parsed.query else "?"
new_url = (
f"{native_redirect}{separator}{urlencode({'code': exchange_code})}"
)
return NativeAppRedirect(new_url)
return super().login_success()
+13 -10
View File
@@ -4,7 +4,7 @@ import smtplib
from logging import getLogger
from django.conf import settings
from django.core.mail import send_mail
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _
@@ -45,15 +45,18 @@ class InvitationService:
)
) # Force translation
email = EmailMultiAlternatives(
subject=subject,
body=msg_plain,
from_email=settings.EMAIL_FROM,
to=[],
bcc=emails,
)
email.attach_alternative(msg_html, "text/html")
try:
send_mail(
subject,
msg_plain,
settings.EMAIL_FROM,
emails,
html_message=msg_html,
fail_silently=False,
)
email.send()
except smtplib.SMTPException as e:
logger.error("invitation to %s was not sent: %s", emails, e)
logger.error("invitations were not sent: %s", e)
raise InvitationError("Could not send invitation") from e
@@ -1,143 +0,0 @@
"""
Tests for the session exchange API endpoint and OIDC callback view.
"""
import uuid
from django.core.cache import cache
import pytest
from rest_framework.test import APIClient
from core.authentication.views import EXCHANGE_CODE_PREFIX, EXCHANGE_CODE_TTL
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def _clear_throttle_cache():
"""Clear cache before each test to reset throttle counters."""
cache.clear()
def test_session_exchange_valid_code():
"""A valid exchange code should return the session ID and be consumed."""
code = uuid.uuid4().hex
cache_key = f"{EXCHANGE_CODE_PREFIX}{code}"
cache.set(cache_key, "test-session-key-123", EXCHANGE_CODE_TTL)
client = APIClient()
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": code},
format="json",
)
assert response.status_code == 200
data = response.json()
assert "test-session-key-123" in data.values()
# Code should be consumed (single-use)
assert cache.get(cache_key) is None
def test_session_exchange_invalid_code():
"""An invalid/unknown code should return 400."""
client = APIClient()
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": uuid.uuid4().hex},
format="json",
)
assert response.status_code == 400
assert response.json()["detail"] == "Invalid or expired code."
def test_session_exchange_expired_code():
"""An expired code should return 400."""
code = uuid.uuid4().hex
cache_key = f"{EXCHANGE_CODE_PREFIX}{code}"
# Set with 0 TTL to simulate expiration
cache.set(cache_key, "expired-session", 0)
client = APIClient()
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": code},
format="json",
)
assert response.status_code == 400
def test_session_exchange_code_too_short():
"""A code that's too short should be rejected by validation."""
client = APIClient()
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": "short"},
format="json",
)
assert response.status_code == 400
def test_session_exchange_missing_code():
"""Missing code field should be rejected."""
client = APIClient()
response = client.post(
"/api/v1.0/auth/session-exchange/",
{},
format="json",
)
assert response.status_code == 400
def test_session_exchange_replay_attack():
"""Using the same code twice should fail the second time."""
code = uuid.uuid4().hex
cache.set(f"{EXCHANGE_CODE_PREFIX}{code}", "session-123", EXCHANGE_CODE_TTL)
client = APIClient()
# First use succeeds
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": code},
format="json",
)
assert response.status_code == 200
# Second use fails
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": code},
format="json",
)
assert response.status_code == 400
def test_session_exchange_returns_correct_cookie_name(settings):
"""The response key should match SESSION_COOKIE_NAME from settings."""
settings.SESSION_COOKIE_NAME = "meet_sessionid"
code = uuid.uuid4().hex
cache.set(f"{EXCHANGE_CODE_PREFIX}{code}", "my-session", EXCHANGE_CODE_TTL)
client = APIClient()
response = client.post(
"/api/v1.0/auth/session-exchange/",
{"code": code},
format="json",
)
assert response.status_code == 200
assert response.json() == {"meet_sessionid": "my-session"}
def test_session_exchange_get_not_allowed():
"""GET method should not be allowed on the exchange endpoint."""
client = APIClient()
response = client.get("/api/v1.0/auth/session-exchange/")
assert response.status_code == 405
@@ -240,10 +240,9 @@ def test_api_rooms_invite_error(mock_invite_to_room):
mock_invite_to_room.assert_called_once()
@mock.patch("core.services.invitation.send_mail")
def test_api_rooms_invite_success(mock_send_mail, settings):
@mock.patch("core.services.invitation.EmailMultiAlternatives")
def test_api_rooms_invite_success(mock_email_class, settings):
"""Test privileged users should successfully send invitation emails."""
settings.EMAIL_BRAND_NAME = "ACME"
settings.EMAIL_LOGO_IMG = "https://acme.com/logo"
settings.EMAIL_APP_BASE_URL = "https://acme.com"
@@ -255,7 +254,6 @@ def test_api_rooms_invite_success(mock_send_mail, settings):
user = UserFactory()
room.accesses.create(user=user, role=random.choice(["administrator", "owner"]))
client.force_login(user)
data = {"emails": ["fabien@yopmail.com", "gerald@yopmail.com"]}
@@ -269,26 +267,38 @@ def test_api_rooms_invite_success(mock_send_mail, settings):
assert response.status_code == 200
assert response.json() == {"status": "success", "message": "invitations sent"}
mock_send_mail.assert_called_once()
mock_email_class.assert_called_once()
subject, body, sender, recipients = mock_send_mail.call_args[0]
# Check constructor arguments
call_kwargs = mock_email_class.call_args[1] # EmailMultiAlternatives(**kwargs)
assert (
subject == f"Video call in progress: {user.email} is waiting for you to connect"
assert call_kwargs["subject"] == (
f"Video call in progress: {user.email} is waiting for you to connect"
)
assert call_kwargs["from_email"] == "notifications@acme.com"
assert call_kwargs["to"] == []
assert sorted(call_kwargs["bcc"]) == sorted(
["fabien@yopmail.com", "gerald@yopmail.com"]
)
# Verify email contains expected content
# Check plain text body
plain_body = call_kwargs["body"]
required_content = [
"ACME", # Brand name
"https://acme.com/logo", # Logo URL
f"https://acme.com/{room.slug}", # Room url
f"acme.com/{room.slug}", # Room link
"ACME",
"https://acme.com/logo",
f"https://acme.com/{room.slug}",
f"acme.com/{room.slug}",
]
for content in required_content:
assert content in body
assert content in plain_body
assert sender == "notifications@acme.com"
# Check HTML alternative was attached
mock_instance = mock_email_class.return_value
mock_instance.attach_alternative.assert_called_once()
html_body, mimetype = mock_instance.attach_alternative.call_args[0]
assert mimetype == "text/html"
for content in required_content:
assert content in html_body
# Verify all owners received the email (order-independent comparison)
assert sorted(recipients) == sorted(["fabien@yopmail.com", "gerald@yopmail.com"])
# Check send was called
mock_instance.send.assert_called_once()
@@ -130,10 +130,11 @@ def test_update_participant_success(mock_livekit_client):
"can_publish": True,
"can_publish_data": True,
"can_publish_sources": [
1,
2,
], # [TrackSource.CAMERA, TrackSource.MICROPHONE]
"CAMERA",
"MICROPHONE",
],
"can_update_metadata": True,
"can_subscribe_metrics": True,
},
"name": "John Doe",
}
@@ -155,8 +156,14 @@ def test_update_participant_success(mock_livekit_client):
{"can_subscribe": True},
{"can_publish": True},
{"can_publish_data": True},
{"can_publish_sources": [1, 2]},
{
"can_publish_sources": [
"CAMERA",
"MICROPHONE",
]
},
{"can_update_metadata": True},
{"can_subscribe_metrics": False},
],
)
def test_update_participant_permission_fields_are_optional(
@@ -264,35 +271,6 @@ def test_update_participant_suspicious_permission_multiple(mock_suspicious):
)
@pytest.mark.parametrize("value", (False, True))
def test_update_participant_unimplemented_can_subscribe_metrics(value):
"""Test update participant raises 400 when can_subscribe_metrics is set."""
client = APIClient()
room = RoomFactory()
user = UserFactory()
UserResourceAccessFactory(
resource=room, user=user, role=random.choice(["administrator", "owner"])
)
client.force_authenticate(user=user)
payload = {
"participant_identity": str(uuid4()),
"permission": {
"can_subscribe": True,
"can_publish": True,
"can_publish_data": True,
"can_update_metadata": False,
"can_subscribe_metrics": value,
},
}
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
response = client.post(url, payload, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "can_subscribe_metrics" in str(response.data)
def test_update_participant_forbidden_without_access():
"""Test update participant returns 403 when user lacks room privileges."""
client = APIClient()
-4
View File
@@ -7,7 +7,6 @@ from lasuite.oidc_login.urls import urlpatterns as oidc_urls
from rest_framework.routers import DefaultRouter
from core.api import get_frontend_configuration, viewsets
from core.authentication.api import session_exchange
from core.external_api import viewsets as external_viewsets
# - Main endpoints
@@ -41,9 +40,6 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
path(
"auth/session-exchange/", session_exchange, name="session_exchange"
),
path("config/", get_frontend_configuration, name="config"),
]
),
+7 -16
View File
@@ -344,11 +344,6 @@ class Base(Configuration):
environ_name="CREATION_CALLBACK_THROTTLE_RATES",
environ_prefix=None,
),
"session_exchange": values.Value(
default="5/minute",
environ_name="SESSION_EXCHANGE_THROTTLE_RATES",
environ_prefix=None,
),
},
}
MONITORED_THROTTLE_FAILURE_CALLBACK = (
@@ -457,23 +452,19 @@ class Base(Configuration):
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}, environ_prefix=None)
# Session
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_ENGINE = values.Value(
default="django.contrib.sessions.backends.cache",
environ_name="SESSION_ENGINE",
environ_prefix=None,
)
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
)
# OIDC - Authorization Code Flow
OIDC_AUTHENTICATE_CLASS = "core.authentication.views.OIDCAuthenticationRequestView"
OIDC_CALLBACK_CLASS = "core.authentication.views.OIDCAuthenticationCallbackView"
# Custom URL schemes allowed for native app OIDC redirects.
# Only these schemes will receive an exchange code in the callback.
NATIVE_APP_REDIRECT_SCHEMES = values.ListValue(
default=["visio"],
environ_name="NATIVE_APP_REDIRECT_SCHEMES",
environ_prefix=None,
)
OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView"
OIDC_CREATE_USER = values.BooleanValue(
default=True, environ_name="OIDC_CREATE_USER", environ_prefix=None
)
+20 -15
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.10.0"
version = "1.12.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -24,16 +24,16 @@ keywords = ["Django", "Contacts", "Templates", "RBAC"]
license = "MIT"
requires-python = ">=3.13"
dependencies = [
"boto3==1.42.49",
"boto3==1.42.68",
"Brotli==1.2.0",
"brevo-python==1.2.0",
"celery[redis]==5.6.2",
"dj-database-url==3.1.0",
"dj-database-url==3.1.2",
"django-configurations==2.5.1",
"django-cors-headers==4.9.0",
"django-countries==8.2.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.24",
"django-lasuite[all]==0.0.25",
"django-parler==2.3",
"redis==5.2.1",
"django-redis==6.0.0",
@@ -43,21 +43,21 @@ dependencies = [
"django==5.2.12",
"djangorestframework==3.16.1",
"drf_spectacular==0.29.0",
"dockerflow==2026.1.26",
"dockerflow==2026.3.4",
"easy_thumbnails==2.10.1",
"factory_boy==3.3.3",
"gunicorn==25.1.0",
"jsonschema==4.26.0",
"markdown==3.10.2",
"nested-multipart-parser==1.6.0",
"psycopg[binary]==3.3.2",
"pydantic==2.12.4",
"PyJWT==2.11.0",
"psycopg[binary]==3.3.3",
"pydantic==2.12.5",
"PyJWT==2.12.1",
"python-frontmatter==1.1.0",
"python-magic==0.4.27",
"requests==2.32.5",
"sentry-sdk==2.53.0",
"whitenoise==6.11.0",
"sentry-sdk==2.54.0",
"whitenoise==6.12.0",
"mozilla-django-oidc==5.0.2",
"livekit-api==1.1.0",
"aiohttp==3.13.3",
@@ -72,11 +72,11 @@ dependencies = [
[dependency-groups]
dev = [
"django-extensions==4.1",
"drf-spectacular-sidecar==2026.1.1",
"drf-spectacular-sidecar==2026.3.1",
"freezegun==1.5.5",
"ipdb==0.13.13",
"ipython==9.10.0",
"pyfakefs==6.1.1",
"ipython==9.11.0",
"pyfakefs==6.1.5",
"pylint-django==2.7.0",
"pylint<4.0.0",
"pytest-cov==7.0.0",
@@ -84,12 +84,17 @@ dev = [
"pytest==9.0.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.25.8",
"ruff==0.15.1",
"responses==0.26.0",
"ruff==0.15.6",
"types-requests==2.32.4.20260107",
]
[tool.uv.build-backend]
module-name = [
"core",
"demo",
"meet"
]
module-root = ""
source-exclude = [
"**/tests/**",
+99 -99
View File
@@ -159,30 +159,30 @@ wheels = [
[[package]]
name = "boto3"
version = "1.42.49"
version = "1.42.68"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/91/105aa17e0f3a566d33e2d8a3b32a70f553b1ad500d9756c6dd63991d8354/boto3-1.42.49.tar.gz", hash = "sha256:9cd252f640567b86e92b0a8ffdd4ade9a3018ee357c724bff6a21b8c8a41be0c", size = 112877, upload-time = "2026-02-13T20:29:57.062Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/ae/60c642aa5413e560b671da825329f510b29a77274ed0f580bde77562294d/boto3-1.42.68.tar.gz", hash = "sha256:3f349f967ab38c23425626d130962bcb363e75f042734fe856ea8c5a00eef03c", size = 112761, upload-time = "2026-03-13T19:32:17.137Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b1/1fa30cd7b26617d59efbe3a4f3660a5b8b397a4623bf1e67016c4cb6dd0e/boto3-1.42.49-py3-none-any.whl", hash = "sha256:99e1df4361c3f6ff6ade65803c043ea96314826134962dd3b385433b309eb819", size = 140606, upload-time = "2026-02-13T20:29:55.366Z" },
{ url = "https://files.pythonhosted.org/packages/fb/f6/dc6e993479dbb597d68223fbf61cb026511737696b15bd7d2a33e9b2c24f/boto3-1.42.68-py3-none-any.whl", hash = "sha256:dbff353eb7dc93cbddd7926ed24793e0174c04adbe88860dfa639568442e4962", size = 140556, upload-time = "2026-03-13T19:32:14.951Z" },
]
[[package]]
name = "botocore"
version = "1.42.64"
version = "1.42.73"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/3c/ac4bc939da695d2c648bf28f7b204ab741e4504e81749ccf943403cc07ca/botocore-1.42.64.tar.gz", hash = "sha256:4ee2aece227b9171ace8b749af694a77ab984fceab1639f2626bd0d6fb1aa69d", size = 14967869, upload-time = "2026-03-09T19:51:46.213Z" }
sdist = { url = "https://files.pythonhosted.org/packages/28/23/0c88ca116ef63b1ae77c901cd5d2095d22a8dbde9e80df74545db4a061b4/botocore-1.42.73.tar.gz", hash = "sha256:575858641e4949aaf2af1ced145b8524529edf006d075877af6b82ff96ad854c", size = 15008008, upload-time = "2026-03-20T19:39:40.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/0f/a0feb9a93da8f583217432dce71ce1940d6d8aa5884bad340872a504ba3f/botocore-1.42.64-py3-none-any.whl", hash = "sha256:f77c5cb76ed30576ed0bc73b591265d03dddffff02a9208d3ee0c790f43d3cd2", size = 14641339, upload-time = "2026-03-09T19:51:41.244Z" },
{ url = "https://files.pythonhosted.org/packages/8e/65/971f3d55015f4d133a6ff3ad74cd39f4b8dd8f53f7775a3c2ad378ea5145/botocore-1.42.73-py3-none-any.whl", hash = "sha256:7b62e2a12f7a1b08eb7360eecd23bb16fe3b7ab7f5617cf91b25476c6f86a0fe", size = 14681861, upload-time = "2026-03-20T19:39:35.341Z" },
]
[[package]]
@@ -545,14 +545,14 @@ wheels = [
[[package]]
name = "dj-database-url"
version = "3.1.0"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/c6/88676a7333fb7c668e626b55f8bfc8527dd863973eb1c40412b95d27747d/dj_database_url-3.1.0.tar.gz", hash = "sha256:d80218426b83f9302c8d27d4fccf52de5cf0cab179f0645fb2839f37605d1353", size = 7924, upload-time = "2026-01-04T09:18:32.693Z" }
sdist = { url = "https://files.pythonhosted.org/packages/03/f6/00b625e9d371b980aa261011d0dc906a16444cb688f94215e0dc86996eb5/dj_database_url-3.1.2.tar.gz", hash = "sha256:63c20e4bbaa51690dfd4c8d189521f6bf6bc9da9fcdb23d95d2ee8ee87f9ec62", size = 11490, upload-time = "2026-02-19T15:30:23.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e84f7472ab0bdacc3fd09556eb4dd40d88246941d465cc103b36a8dabcd8/dj_database_url-3.1.0-py3-none-any.whl", hash = "sha256:155a56fbbecbaaf1348ccd73bf29138b4c9988363ba08261a0f0145e392e638c", size = 8849, upload-time = "2026-01-04T09:18:43.77Z" },
{ url = "https://files.pythonhosted.org/packages/cf/a9/57c66006373381f1d3e5bd94216f1d371228a89f443d3030e010f73dd198/dj_database_url-3.1.2-py3-none-any.whl", hash = "sha256:544e015fee3efa5127a1eb1cca465f4ace578265b3671fe61d0ed7dbafb5ec8a", size = 8953, upload-time = "2026-02-19T15:30:39.37Z" },
]
[[package]]
@@ -633,7 +633,7 @@ wheels = [
[[package]]
name = "django-lasuite"
version = "0.0.24"
version = "0.0.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
@@ -644,9 +644,9 @@ dependencies = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/dc/4002ba20b964237a609628d53b950c5aecf6387119058831e0ee33176f59/django_lasuite-0.0.24.tar.gz", hash = "sha256:3231b0178a2187405c8faae447225c5dd069263a9a09e73e2a36427be4bf2388", size = 34293, upload-time = "2026-02-11T12:25:42.558Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/bc/5826a3e5ada5dad1ff6af9a2e1eb598a0dc2cb9c4d2dbf011f1bbb310c9e/django_lasuite-0.0.25.tar.gz", hash = "sha256:ee44783942e6ead74a732f6d7280c5fca961b66581350ce01c0589c3e80684cf", size = 34819, upload-time = "2026-03-10T13:40:28.705Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/90/9170b8f443ab5d34b32d7cd386ce3b38ca35baae966da4a1d77b4f4ab3d2/django_lasuite-0.0.24-py3-none-any.whl", hash = "sha256:c37e0b3606a23fd4094ca435fa4edf7a64d325be64b0310fa6577f75d515d856", size = 52620, upload-time = "2026-02-11T12:25:40.626Z" },
{ url = "https://files.pythonhosted.org/packages/61/2f/819ebbc9d3a4b8233cef1e9b0b607724e4acf527988393b04a4036b9c235/django_lasuite-0.0.25-py3-none-any.whl", hash = "sha256:4c10f625005cd41d05e8d34269f1a3b58fea8be5296528bd58596227f50d9884", size = 54034, upload-time = "2026-03-10T13:40:26.903Z" },
]
[package.optional-dependencies]
@@ -737,11 +737,11 @@ wheels = [
[[package]]
name = "dockerflow"
version = "2026.1.26"
version = "2026.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/08/7abe8458ce6fa82e694ba9b792b4158596fd681782976699d3f15642a23a/dockerflow-2026.1.26.tar.gz", hash = "sha256:c45667ec078944a468b9a634c866587b4cf31559d0e6f369fbadd21edce66b38", size = 68042, upload-time = "2026-01-26T15:30:06.606Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/03/935f8399094611981b37634a7ee078484b3995f744135339e51385638f6c/dockerflow-2026.3.4.tar.gz", hash = "sha256:6cc7fcab2a078d06aeca8b02e22fd7f6766dca322fb30aed6910684dbc08690c", size = 68701, upload-time = "2026-03-04T09:01:28.178Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/88/26cec1e8ce09b4022233f232962fba72650c22941d7a609d10b9eec9f748/dockerflow-2026.1.26-py2.py3-none-any.whl", hash = "sha256:26f02d8842672921b2a49f1e019fac54ed2ee69dc673ed80f7b237b67394c2be", size = 35992, upload-time = "2026-01-26T15:30:05.525Z" },
{ url = "https://files.pythonhosted.org/packages/86/b9/2d7bf30eb0c24e60e61e4f6ae7574e123d3fa51b39a612e146c94158446a/dockerflow-2026.3.4-py2.py3-none-any.whl", hash = "sha256:38fe0ad30a4aedf68e589919695d502fc6f584a761b56a91f56e80cbadfbf61a", size = 36424, upload-time = "2026-03-04T09:01:26.631Z" },
]
[[package]]
@@ -763,14 +763,14 @@ wheels = [
[[package]]
name = "drf-spectacular-sidecar"
version = "2026.1.1"
version = "2026.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/81/c7b0e3ccbd5a039c4f4fcfecf88391a666ca1406a953886e2f39295b1c90/drf_spectacular_sidecar-2026.1.1.tar.gz", hash = "sha256:6f7c173a8ddbbbdafc7a27e028614b65f07a89ca90f996a432d57460463b56be", size = 2468060, upload-time = "2026-01-01T11:27:12.682Z" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/42/2f8c1b2846399d47094ec414bc0d6a7cce7ba95fd6545a97285eee89f7f1/drf_spectacular_sidecar-2026.3.1.tar.gz", hash = "sha256:5b7fedad66e3851f2f442480792c08115d79217959d01645b93d3d2258938be1", size = 2461501, upload-time = "2026-03-01T11:31:19.708Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/96/38725edda526f3e9e597f531beeec94b0ef433d9494f06a13b7636eecb6e/drf_spectacular_sidecar-2026.1.1-py3-none-any.whl", hash = "sha256:af8df62f1b594ec280351336d837eaf2402ab25a6bc2a1fad7aee9935821070f", size = 2489520, upload-time = "2026-01-01T11:27:11.056Z" },
{ url = "https://files.pythonhosted.org/packages/c1/28/2d5e64d101ebc5180674fcaf7b5a35e398e2f8d9688b2e8d52b0e1394e7d/drf_spectacular_sidecar-2026.3.1-py3-none-any.whl", hash = "sha256:864edb83e022e13e3941c325c3cc0c954c843fa2e1d0bc95e81887664b2d3dad", size = 2481725, upload-time = "2026-03-01T11:31:18.469Z" },
]
[[package]]
@@ -976,7 +976,7 @@ wheels = [
[[package]]
name = "ipython"
version = "9.10.0"
version = "9.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -990,9 +990,9 @@ dependencies = [
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" }
sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" },
{ url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" },
]
[[package]]
@@ -1157,7 +1157,7 @@ wheels = [
[[package]]
name = "meet"
version = "1.10.0"
version = "1.12.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
@@ -1222,24 +1222,24 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = "==3.13.3" },
{ name = "boto3", specifier = "==1.42.49" },
{ name = "boto3", specifier = "==1.42.68" },
{ name = "brevo-python", specifier = "==1.2.0" },
{ name = "brotli", specifier = "==1.2.0" },
{ name = "celery", extras = ["redis"], specifier = "==5.6.2" },
{ name = "dj-database-url", specifier = "==3.1.0" },
{ name = "dj-database-url", specifier = "==3.1.2" },
{ name = "django", specifier = "==5.2.12" },
{ name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==8.2.0" },
{ name = "django-filter", specifier = "==25.2" },
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.24" },
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.25" },
{ name = "django-parler", specifier = "==2.3" },
{ name = "django-pydantic-field", specifier = "==0.5.4" },
{ name = "django-redis", specifier = "==6.0.0" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ name = "django-timezone-field", specifier = ">=5.1" },
{ name = "djangorestframework", specifier = "==3.16.1" },
{ name = "dockerflow", specifier = "==2026.1.26" },
{ name = "dockerflow", specifier = "==2026.3.4" },
{ name = "drf-spectacular", specifier = "==0.29.0" },
{ name = "easy-thumbnails", specifier = "==2.10.1" },
{ name = "factory-boy", specifier = "==3.3.3" },
@@ -1249,25 +1249,25 @@ requires-dist = [
{ name = "markdown", specifier = "==3.10.2" },
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.2" },
{ name = "pydantic", specifier = "==2.12.4" },
{ name = "pyjwt", specifier = "==2.11.0" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.3" },
{ name = "pydantic", specifier = "==2.12.5" },
{ name = "pyjwt", specifier = "==2.12.1" },
{ name = "python-frontmatter", specifier = "==1.1.0" },
{ name = "python-magic", specifier = "==0.4.27" },
{ name = "redis", specifier = "==5.2.1" },
{ name = "requests", specifier = "==2.32.5" },
{ name = "sentry-sdk", specifier = "==2.53.0" },
{ name = "whitenoise", specifier = "==6.11.0" },
{ name = "sentry-sdk", specifier = "==2.54.0" },
{ name = "whitenoise", specifier = "==6.12.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "django-extensions", specifier = "==4.1" },
{ name = "drf-spectacular-sidecar", specifier = "==2026.1.1" },
{ name = "drf-spectacular-sidecar", specifier = "==2026.3.1" },
{ name = "freezegun", specifier = "==1.5.5" },
{ name = "ipdb", specifier = "==0.13.13" },
{ name = "ipython", specifier = "==9.10.0" },
{ name = "pyfakefs", specifier = "==6.1.1" },
{ name = "ipython", specifier = "==9.11.0" },
{ name = "pyfakefs", specifier = "==6.1.5" },
{ name = "pylint", specifier = "<4.0.0" },
{ name = "pylint-django", specifier = "==2.7.0" },
{ name = "pytest", specifier = "==9.0.2" },
@@ -1275,8 +1275,8 @@ dev = [
{ name = "pytest-django", specifier = "==4.12.0" },
{ name = "pytest-icdiff", specifier = "==0.9" },
{ name = "pytest-xdist", specifier = "==3.8.0" },
{ name = "responses", specifier = "==0.25.8" },
{ name = "ruff", specifier = "==0.15.1" },
{ name = "responses", specifier = "==0.26.0" },
{ name = "ruff", specifier = "==0.15.6" },
{ name = "types-requests", specifier = "==2.32.4.20260107" },
]
@@ -1595,14 +1595,14 @@ wheels = [
[[package]]
name = "psycopg"
version = "3.3.2"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
]
[package.optional-dependencies]
@@ -1612,31 +1612,31 @@ binary = [
[[package]]
name = "psycopg-binary"
version = "3.3.2"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" },
{ url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" },
{ url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" },
{ url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" },
{ url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" },
{ url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" },
{ url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" },
{ url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" },
{ url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" },
{ url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" },
{ url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
{ url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
{ url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
{ url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
{ url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
{ url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
{ url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
{ url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
{ url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
{ url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" },
{ url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" },
{ url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" },
{ url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" },
{ url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" },
{ url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" },
{ url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" },
{ url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" },
{ url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" },
{ url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" },
{ url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" },
{ url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" },
{ url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" },
{ url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" },
{ url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" },
{ url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" },
{ url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" },
{ url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" },
]
[[package]]
@@ -1668,7 +1668,7 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.12.4"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -1676,9 +1676,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
@@ -1736,11 +1736,11 @@ wheels = [
[[package]]
name = "pyfakefs"
version = "6.1.1"
version = "6.1.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/84/843a50cfde48d6004cbf540b38b82a39737273cc76b51da6b456fea75de0/pyfakefs-6.1.1.tar.gz", hash = "sha256:1c166fdba85f3f288f89b2a410e05712914d1f7ea77b920e960fe0cbbea1294d", size = 226032, upload-time = "2026-02-09T19:44:19.539Z" }
sdist = { url = "https://files.pythonhosted.org/packages/61/87/fe3f1fbbf18480b57e6b195d4d416e01d8e55bf88743982ccb3a8845c2a2/pyfakefs-6.1.5.tar.gz", hash = "sha256:d70fb1c622a1d5c729462d31bb58eead99c1830986bc049bf8975c480a49d19b", size = 226814, upload-time = "2026-03-15T19:05:18.025Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/e2/1d74c7663aa38e405c25a30c26e8a1b92c06f8947118b9617042f4290ade/pyfakefs-6.1.1-py3-none-any.whl", hash = "sha256:e2c30fd0b7736ba03785bbc20b86de2312623cf7a11a7de334042ab0de23d46f", size = 239630, upload-time = "2026-02-09T19:44:18.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/d8/a60fb94ab2b5a9bf0e38d6e93667904d4e844d8b7e9cfb5ef0304d68da6b/pyfakefs-6.1.5-py3-none-any.whl", hash = "sha256:2c482f36bad7d1f7516d58d17818fb0dbe397a3372ec5274869386a9b7cd4883", size = 239885, upload-time = "2026-03-15T19:05:16.488Z" },
]
[[package]]
@@ -1754,11 +1754,11 @@ wheels = [
[[package]]
name = "pyjwt"
version = "2.11.0"
version = "2.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
]
[[package]]
@@ -1992,16 +1992,16 @@ wheels = [
[[package]]
name = "responses"
version = "0.25.8"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" },
{ url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" },
]
[[package]]
@@ -2072,27 +2072,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.1"
version = "0.15.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
{ url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
{ url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
{ url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
{ url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
{ url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
{ url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
{ url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
{ url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
{ url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
{ url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
]
[[package]]
@@ -2109,15 +2109,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
version = "2.53.0"
version = "2.54.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" },
{ url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" },
]
[[package]]
@@ -2271,11 +2271,11 @@ wheels = [
[[package]]
name = "whitenoise"
version = "6.11.0"
version = "6.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/95/8c81ec6b6ebcbf8aca2de7603070ccf37dbb873b03f20708e0f7c1664bc6/whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f", size = 26432, upload-time = "2025-09-18T09:16:10.995Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/e9/4366332f9295fe0647d7d3251ce18f5615fbcb12d02c79a26f8dba9221b3/whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258", size = 20197, upload-time = "2025-09-18T09:16:09.754Z" },
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
]
[[package]]
+19 -15
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.10.0",
"version": "1.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.10.0",
"version": "1.12.0",
"dependencies": {
"@fontsource-variable/material-symbols-outlined": "5.2.34",
"@fontsource/material-icons-outlined": "5.2.6",
@@ -6269,10 +6269,13 @@
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -7158,10 +7161,11 @@
}
},
"node_modules/flatted": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
"node_modules/for-each": {
"version": "0.3.3",
@@ -7628,9 +7632,9 @@
"integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg=="
},
"node_modules/hono": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz",
"integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==",
"version": "4.12.8",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz",
"integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11107,9 +11111,9 @@
}
},
"node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.10.0",
"version": "1.12.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

+13 -1
View File
@@ -15,7 +15,19 @@ export const fetchApi = async <T = Record<string, unknown>>(
...options?.headers,
},
})
const result = await response.json()
let result: T
if (response.status === 204) {
result = undefined as T
} else {
const contentType = response.headers.get('content-type') ?? ''
if (!contentType.includes('application/json')) {
result = undefined as T
} else {
result = (await response.json()) as T
}
}
if (!response.ok) {
throw new ApiError(response.status, result)
}
+1
View File
@@ -5,4 +5,5 @@ export const keys = {
requestEntry: 'requestEntry',
waitingParticipants: 'waitingParticipants',
roomCreationCallback: 'roomCreationCallback',
files: 'files',
}
+7
View File
@@ -30,6 +30,13 @@ export interface ApiConfig {
expiration_days?: number
max_duration?: number
}
background_image: {
upload_is_enabled: boolean
max_size: number
max_count_by_user: number
allowed_extensions: string[]
allowed_mimetypes: string[]
}
subtitle: {
enabled: boolean
}
@@ -0,0 +1,93 @@
import { fetchApi } from '@/api/fetchApi'
import { useMutation } from '@tanstack/react-query'
import { ApiFileItem } from '@/features/files/api/types.ts'
import { keys } from '@/api/queryKeys.ts'
import { queryClient } from '@/api/queryClient.ts'
/**
* Upload a file, using XHR so we can report on progress through a handler.
*
* @param url The URL to PUT the file to.
* @param file The file to upload.
* @param progressHandler A handler that receives progress updates as a single integer `0 <= x <= 100`.
*/
export const uploadFile = (
url: string,
file: File,
progressHandler: (progress: number) => void
) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('PUT', url)
xhr.setRequestHeader('X-amz-acl', 'private')
xhr.setRequestHeader('Content-Type', file.type)
xhr.addEventListener('error', reject)
xhr.addEventListener('abort', reject)
xhr.addEventListener('readystatechange', () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Make sure to always set the progress to 100% when the upload is done.
// Because 'progress' event listener is not called when the file size is 0.
progressHandler(100)
return resolve(true)
}
reject(new Error(`Failed to perform the upload on ${url}.`))
}
})
xhr.upload.addEventListener('progress', (progressEvent) => {
if (progressEvent.lengthComputable) {
progressHandler(
Math.floor((progressEvent.loaded / progressEvent.total) * 100)
)
}
})
xhr.send(file)
})
/**
* Asynchronously creates a new file and uploads it to the server.
*
* @param {object} params - The parameters for the file creation and upload process.
* @param {File} params.file - The file object to be uploaded.
* @param {function} params.onProgress - A callback function that receives the upload progress as a number (0 to 100).
* @returns {Promise<ApiFileItem>} A promise that resolves when the file has been successfully uploaded and the server process is completed.
*/
export const createFile = async ({
file,
onProgress,
}: {
file: File
onProgress: (progress: number) => void
}): Promise<ApiFileItem> => {
const res = await fetchApi<ApiFileItem>(`/files/`, {
method: 'POST',
body: JSON.stringify({ filename: file.name, type: 'background_image' }),
})
if (res.upload_state !== 'pending') {
throw new Error('State should be pending right after creation')
}
const policy = res.policy
await uploadFile(policy, file, onProgress)
const createdFile = await fetchApi<ApiFileItem>(
`/files/${res.id}/upload-ended/`,
{
method: 'POST',
}
)
// We invalidate the files query to make sure the new file is immediately available.
await queryClient.invalidateQueries({
queryKey: [keys.files],
})
return createdFile
}
export const useCreateFile = () => {
return useMutation({
mutationFn: createFile,
})
}
@@ -0,0 +1,33 @@
import { fetchApi } from '@/api/fetchApi'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys.ts'
/**
* Deletes a file specified by its unique identifier.
*
* @param {Object} params - The parameters required for deleting the file.
* @param {string} params.fileId - The unique identifier of the file to be deleted.
* @returns {Promise<void>} A promise that resolves when the file is successfully deleted.
*/
export const deleteFile = async ({
fileId,
}: {
fileId: string
}): Promise<void> => {
await fetchApi<void>(`/files/${fileId}/`, {
method: 'DELETE',
})
}
export const useDeleteFile = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: deleteFile,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: [keys.files],
})
},
})
}
@@ -0,0 +1,70 @@
import { fetchApi } from '@/api/fetchApi'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import {
ApiFileItem,
ApiFileType,
ApiFileUploadState,
} from '@/features/files/api/types.ts'
import { useUser } from '@/features/auth'
import { useConfig } from '@/api/useConfig.ts'
type ListFilesResponse = {
count: number
next: string | null
previous: string | null
results: ApiFileItem[]
}
type ListFilesFilters = {
is_creator_me?: boolean
type?: ApiFileType
upload_state?: ApiFileUploadState
is_deleted?: boolean
}
export type ListFilesParams = {
filters?: ListFilesFilters
pagination: {
page: number
pageSize: number
}
}
export const listMyFiles = async ({
filters = {},
pagination: { page, pageSize },
}: ListFilesParams): Promise<ListFilesResponse> => {
const query = new URLSearchParams()
query.append('page', page.toString())
query.append('page_size', pageSize.toString())
if (filters?.is_creator_me ?? true) {
query.append('is_creator_me', 'true')
}
if (filters?.type) {
query.append('type', filters.type)
}
if (filters?.upload_state) {
query.append('upload_state', filters.upload_state)
}
if (typeof filters?.is_deleted === 'boolean') {
query.append('is_deleted', filters.is_deleted ? 'true' : 'false')
}
return fetchApi<ListFilesResponse>(`/files?${query.toString()}`, {
method: 'GET',
})
}
export const useListMyFiles = (params: Parameters<typeof listMyFiles>[0]) => {
const { isLoggedIn } = useUser()
const { data: appConfig } = useConfig()
return useQuery({
queryKey: [keys.files, params],
queryFn: () => listMyFiles(params),
refetchOnMount: 'always',
placeholderData: keepPreviousData,
enabled:
isLoggedIn && appConfig?.background_image?.upload_is_enabled === true,
})
}
@@ -0,0 +1,34 @@
export type ApiFileCreator = {
id: string // UUID
full_name: string | null
short_name: string | null
}
export type ApiFileType = 'background_image'
export type ApiFileUploadState = 'pending' | 'ready'
export type ApiFileItem = {
id: string // UUID
created_at: string // ISO datetime string
updated_at: string // ISO datetime string
title: string
type: ApiFileType
creator: ApiFileCreator
deleted_at: string | null
hard_deleted_at: string | null
filename: string
upload_state: ApiFileUploadState
mimetype: string // e.g. "image/png"
size: number // file size in bytes
description: string | null
} & (
| {
upload_state: 'ready'
url: string
}
| {
upload_state: 'pending'
policy: string
url: null
}
)
@@ -215,8 +215,8 @@ export const Conference = ({
audio={userConfig.audioEnabled}
video={
userConfig.videoEnabled && {
processor: BackgroundProcessorFactory.deserializeProcessor(
userConfig.processorSerialized
processor: BackgroundProcessorFactory.fromProcessorConfig(
userConfig.processorConfig
),
}
}
@@ -4,15 +4,15 @@ import { css } from '@/styled-system/css'
import { Screen } from '@/layout/Screen'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
createLocalVideoTrack,
createLocalAudioTrack,
createLocalVideoTrack,
LocalAudioTrack,
LocalVideoTrack,
Track,
} from 'livekit-client'
import { H } from '@/primitives/H'
import { Field } from '@/primitives/Field'
import { Button, Dialog, Text, Form } from '@/primitives'
import { Button, Dialog, Form, Text } from '@/primitives'
import { VStack } from '@/styled-system/jsx'
import { Heading } from 'react-aria-components'
import { RiImageCircleAiFill } from '@remixicon/react'
@@ -44,8 +44,7 @@ const onError = (e: Error) => console.error('ERROR', e)
const Effects = ({
videoTrack,
onSubmit,
}: Pick<EffectsConfigurationProps, 'videoTrack' | 'onSubmit'>) => {
}: Pick<EffectsConfigurationProps, 'videoTrack'>) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join.effects' })
const [isDialogOpen, setIsDialogOpen] = useState(false)
const openDialog = () => setIsDialogOpen(true)
@@ -81,7 +80,7 @@ const Effects = ({
>
{t('subTitle')}
</Text>
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
<EffectsConfiguration videoTrack={videoTrack} />
</Dialog>
<Button
variant="whiteCircle"
@@ -111,7 +110,7 @@ export const Join = ({
audioDeviceId,
audioOutputDeviceId,
videoDeviceId,
processorSerialized,
processorConfig,
username,
},
saveAudioInputEnabled,
@@ -120,7 +119,6 @@ export const Join = ({
saveAudioInputDeviceId,
saveVideoInputDeviceId,
saveUsername,
saveProcessorSerialized,
} = usePersistentUserChoices()
const initialUserChoices = useRef<LocalUserChoices | null>(null)
@@ -132,7 +130,7 @@ export const Join = ({
audioDeviceId,
audioOutputDeviceId,
videoDeviceId,
processorSerialized,
processorConfig,
username,
}
}
@@ -146,8 +144,8 @@ export const Join = ({
video: !!initialUserChoices.current &&
initialUserChoices.current?.videoEnabled && {
deviceId: initialUserChoices.current.videoDeviceId,
processor: BackgroundProcessorFactory.deserializeProcessor(
initialUserChoices.current.processorSerialized
processor: BackgroundProcessorFactory.fromProcessorConfig(
initialUserChoices.current.processorConfig
),
},
},
@@ -186,9 +184,7 @@ export const Join = ({
const track = await createLocalVideoTrack({
deviceId: { exact: videoDeviceId },
processor:
BackgroundProcessorFactory.deserializeProcessor(
processorSerialized
),
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
})
setDynamicVideoTrack(track)
} catch (error) {
@@ -207,7 +203,7 @@ export const Join = ({
}, [
videoEnabled,
videoDeviceId,
processorSerialized,
processorConfig,
previewVideoTrack,
dynamicVideoTrack,
])
@@ -690,12 +686,7 @@ export const Join = ({
zIndex: '1',
})}
>
<Effects
videoTrack={videoTrack}
onSubmit={(processor) =>
saveProcessorSerialized(processor?.serialize())
}
/>
<Effects videoTrack={videoTrack} />
</div>
</div>
</div>
@@ -31,6 +31,8 @@ import { FullScreenShareWarning } from './FullScreenShareWarning'
import { ParticipantName } from './ParticipantName'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { useTranslation } from 'react-i18next'
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
import { KeyboardShortcutHint } from './KeyboardShortcutHint'
export function TrackRefContextIfNeeded(
@@ -237,7 +239,13 @@ export const ParticipantTile: (
)}
</ParticipantContextIfNeeded>
</TrackRefContextIfNeeded>
<KeyboardShortcutHint>{t('toolbarHint')}</KeyboardShortcutHint>
<KeyboardShortcutHint>
{t('toolbarHint', {
shortcut: formatShortcutLabel(
getShortcutDescriptorById('open-shortcuts')?.shortcut
),
})}
</KeyboardShortcutHint>
</div>
)
})
@@ -149,11 +149,12 @@ export const SidePanel = () => {
activeSubPanelId,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const title = t(`heading.${activeSubPanelId || activePanelId}`)
return (
<StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)}
ariaLabel={t('ariaLabel')}
title={title}
ariaLabel={t('ariaLabel', { title })}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
@@ -65,7 +65,7 @@ const ToolButton = ({
<div>
<Text
margin={false}
as="h3"
as="h2"
className={css({
display: 'flex',
gap: 0.25,
@@ -11,11 +11,7 @@ import {
TIMEOUT_TICK,
timerWorkerScript,
} from './TimerWorker'
import {
BackgroundProcessorInterface,
BackgroundOptions,
ProcessorType,
} from '.'
import { BackgroundProcessorInterface, ProcessorConfig, ProcessorType } from '.'
const PROCESSING_WIDTH = 256
const PROCESSING_HEIGHT = 144
@@ -33,7 +29,7 @@ const DEFAULT_BLUR = '10'
* MediaStreamTrackProcessor.
*/
export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
options: BackgroundOptions
options: ProcessorConfig
name: string
processedTrack?: MediaStreamTrack | undefined
@@ -64,15 +60,10 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
type: ProcessorType
virtualBackgroundImage?: HTMLImageElement
constructor(opts: BackgroundOptions) {
constructor(opts: ProcessorConfig) {
this.name = 'blur'
this.options = opts
if (this.options.blurRadius) {
this.type = ProcessorType.BLUR
} else {
this.type = ProcessorType.VIRTUAL
}
this.type = opts.type
}
static get isSupported() {
@@ -107,6 +98,12 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
}
_initVirtualBackgroundImage() {
if (this.options.type !== 'virtual') {
throw new Error(
'Virtual background is only supported for virtual background'
)
}
const needsUpdate =
this.options.imagePath &&
this.virtualBackgroundImage &&
@@ -118,7 +115,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
}
}
async update(opts: BackgroundOptions): Promise<void> {
async update(opts: ProcessorConfig): Promise<void> {
this.options = opts
this._initVirtualBackgroundImage()
}
@@ -187,7 +184,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
0,
0,
PROCESSING_WIDTH,
PROCESSING_WIDTH
PROCESSING_HEIGHT
)
}
@@ -212,6 +209,9 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
* TODO: future improvement with WebGL.
*/
async blur() {
if (this.options.type !== 'blur') {
throw new Error('Blurring is only supported for blur background')
}
const mask = this.imageSegmenterResult!.categoryMask!.getAsUint8Array()
for (let i = 0; i < mask.length; ++i) {
this.segmentationMask!.data[i * 4 + 3] = 255 - mask[i]
@@ -293,7 +293,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
await this.sizeSource()
await this.segment()
if (this.options.blurRadius) {
if (this.options.type === 'blur') {
await this.blur()
} else {
await this.drawVirtualBackground()
@@ -354,15 +354,4 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
this.timerWorker?.terminate()
this.imageSegmenter?.close()
}
clone() {
return new BackgroundCustomProcessor(this.options)
}
serialize() {
return {
type: this.type,
options: this.options,
}
}
}
@@ -332,15 +332,4 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
this.timerWorker?.terminate()
this.faceLandmarker?.close()
}
clone() {
return new FaceLandmarksProcessor(this.options)
}
serialize() {
return {
type: this.type,
options: this.options,
}
}
}
@@ -1,28 +1,23 @@
import { ProcessorOptions, Track } from 'livekit-client'
import {
BackgroundBlur,
BackgroundTransformer,
ProcessorWrapper,
VirtualBackground,
} from '@livekit/track-processors'
import {
BackgroundOptions,
BackgroundProcessorInterface,
ProcessorType,
} from '.'
import { ProcessorConfig, BackgroundProcessorInterface, ProcessorType } from '.'
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
processor: ProcessorWrapper<BackgroundOptions>
opts: BackgroundOptions
processor: ProcessorWrapper<{ imagePath?: string; blurRadius?: number }>
opts: ProcessorConfig
processorType: ProcessorType
constructor(opts: BackgroundOptions) {
constructor(opts: ProcessorConfig) {
this.opts = opts
if (opts.imagePath) {
if (opts.type === 'virtual') {
this.processorType = ProcessorType.VIRTUAL
this.processor = VirtualBackground(opts.imagePath)
} else if (opts.blurRadius !== undefined) {
} else if (opts.type === 'blur') {
this.processorType = ProcessorType.BLUR
this.processor = BackgroundBlur(opts.blurRadius)
} else {
@@ -44,24 +39,23 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
return this.processor.destroy()
}
async update(opts: BackgroundOptions): Promise<void> {
const newProcessorType = opts.imagePath
? ProcessorType.VIRTUAL
: ProcessorType.BLUR
async update(opts: ProcessorConfig): Promise<void> {
this.opts = opts
const newProcessorType =
opts.type === 'virtual' ? ProcessorType.VIRTUAL : ProcessorType.BLUR
let processedOpts = opts
if (newProcessorType !== this.processorType) {
this.processorType = newProcessorType
if (newProcessorType === ProcessorType.VIRTUAL) {
this.processor.name = 'virtual-background'
processedOpts = { ...opts, blurRadius: undefined }
} else {
this.processor.name = 'background-blur'
processedOpts = { ...opts, imagePath: undefined }
}
}
await this.processor.updateTransformerOptions(processedOpts)
this.opts = processedOpts
await this.processor.updateTransformerOptions(
opts as { imagePath?: string; blurRadius?: number }
)
}
get name() {
@@ -73,17 +67,6 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
}
get options() {
return (this.processor.transformer as BackgroundTransformer).options
}
clone() {
return new UnifiedBackgroundTrackProcessor(this.options || this.opts)
}
serialize() {
return {
type: this.processorType,
options: this.options,
}
return this.opts
}
}
@@ -2,23 +2,7 @@ import { ProcessorWrapper } from '@livekit/track-processors'
import { Track, TrackProcessor } from 'livekit-client'
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
export type BackgroundOptions = {
blurRadius?: number
imagePath?: string
}
export interface ProcessorSerialized {
type: ProcessorType
options: BackgroundOptions
}
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
update(opts: BackgroundOptions): Promise<void>
options: BackgroundOptions
clone(): BackgroundProcessorInterface
serialize(): ProcessorSerialized
}
import { FaceLandmarksOptions } from './FaceLandmarksProcessor'
export enum ProcessorType {
BLUR = 'blur',
@@ -26,6 +10,16 @@ export enum ProcessorType {
FACE_LANDMARKS = 'faceLandmarks',
}
export type ProcessorConfig =
| { type: ProcessorType.BLUR; blurRadius: number }
| { type: ProcessorType.VIRTUAL; imagePath: string; fileId?: string }
| ({ type: ProcessorType.FACE_LANDMARKS } & FaceLandmarksOptions)
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
update(opts: ProcessorConfig): Promise<void>
options: ProcessorConfig
}
export class BackgroundProcessorFactory {
static hasModernApiSupport() {
return ProcessorWrapper.hasModernApiSupport
@@ -36,28 +30,27 @@ export class BackgroundProcessorFactory {
}
static getProcessor(
type: ProcessorType,
opts: BackgroundOptions
config: ProcessorConfig
): BackgroundProcessorInterface | undefined {
const isBlur = type === ProcessorType.BLUR
const isVirtual = type === ProcessorType.VIRTUAL
const isBlur = config.type === ProcessorType.BLUR
const isVirtual = config.type === ProcessorType.VIRTUAL
if (!isBlur && !isVirtual) return undefined
if (ProcessorWrapper.isSupported) {
return new UnifiedBackgroundTrackProcessor(opts)
return new UnifiedBackgroundTrackProcessor(config)
}
if (BackgroundCustomProcessor.isSupported) {
return new BackgroundCustomProcessor(opts)
return new BackgroundCustomProcessor(config)
}
return undefined
}
static deserializeProcessor(data?: ProcessorSerialized) {
if (data?.type) {
return BackgroundProcessorFactory.getProcessor(data?.type, data?.options)
static fromProcessorConfig(data?: ProcessorConfig) {
if (data) {
return BackgroundProcessorFactory.getProcessor(data)
}
return undefined
}
@@ -69,10 +69,10 @@ const SelectDevicePermissions = <T extends string | number>({
iconComponent={iconComponent}
placeholder={items.length === 0 ? t('loading') : t('select')}
selectedKey={selectedKey}
onSelectionChange={(key) => {
onSelectionChange={async (key) => {
if (key === selectedKey) return
await setActiveMediaDevice(key as string)
onSubmit?.(key as string)
setActiveMediaDevice(key as string)
}}
{...props}
/>
@@ -82,8 +82,8 @@ export const VideoDeviceControl = ({
*
* See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121
*/
const processor = BackgroundProcessorFactory.deserializeProcessor(
userChoices.processorSerialized
const processor = BackgroundProcessorFactory.fromProcessorConfig(
userChoices.processorConfig
)
const toggle = trackProps.toggle as (
@@ -2,14 +2,12 @@ import { useLocalParticipant } from '@livekit/components-react'
import { LocalVideoTrack } from 'livekit-client'
import { css } from '@/styled-system/css'
import { EffectsConfiguration } from './EffectsConfiguration'
import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices'
import { useCanPublishTrack } from '@/features/rooms/livekit/hooks/useCanPublishTrack'
import { TrackSource } from '@livekit/protocol'
export const Effects = () => {
const { cameraTrack } = useLocalParticipant()
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
const { saveProcessorSerialized } = usePersistentUserChoices()
const canPublishCamera = useCanPublishTrack(TrackSource.CAMERA)
@@ -24,9 +22,6 @@ export const Effects = () => {
isDisabled={!canPublishCamera}
videoTrack={localCameraTrack}
layout="vertical"
onSubmit={(processor) =>
saveProcessorSerialized(processor?.serialize())
}
/>
</div>
)
File diff suppressed because it is too large Load Diff
@@ -27,7 +27,7 @@ export const FunnyEffects = ({
showFrench: false,
}
}
return processor.serialize().options
return { ...processor.options }
}
const options = getOptions()
@@ -1,7 +1,7 @@
import { useSnapshot } from 'valtio'
import { userChoicesStore } from '@/stores/userChoices'
import type { VideoResolution } from '@/stores/userChoices'
import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur'
import { ProcessorConfig } from '@/features/rooms/livekit/components/blur'
import type { VideoQuality } from 'livekit-client'
export function usePersistentUserChoices() {
@@ -36,10 +36,8 @@ export function usePersistentUserChoices() {
saveNoiseReductionEnabled: (enabled: boolean) => {
userChoicesStore.noiseReductionEnabled = enabled
},
saveProcessorSerialized: (
processorSerialized: ProcessorSerialized | undefined
) => {
userChoicesStore.processorSerialized = processorSerialized
saveProcessorConfig: (processorConfig: ProcessorConfig | undefined) => {
userChoicesStore.processorConfig = processorConfig
},
}
}
@@ -31,7 +31,7 @@ export const VideoTab = ({ id }: VideoTabProps) => {
const {
userChoices: {
videoDeviceId,
processorSerialized,
processorConfig,
videoPublishResolution,
videoSubscribeQuality,
},
@@ -78,7 +78,7 @@ export const VideoTab = ({ id }: VideoTabProps) => {
resolution: VideoPresets[key].resolution,
deviceId: { exact: videoDeviceId },
processor:
BackgroundProcessorFactory.deserializeProcessor(processorSerialized),
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
})
}
}
@@ -6,8 +6,11 @@ import { useSnapshot } from 'valtio'
import {
accessibilityStore,
type CaptionTextSize,
type CaptionColor,
CAPTION_TEXT_SIZE_OPTIONS,
CAPTION_COLOR_OPTIONS,
} from '@/stores/accessibility'
import { useAreSubtitlesAvailable } from '../hooks/useAreSubtitlesAvailable'
export const CaptionsSettings = () => {
const { t } = useTranslation('settings', {
@@ -24,6 +27,27 @@ export const CaptionsSettings = () => {
[t]
)
const captionFontColorItems = useMemo(
() =>
CAPTION_COLOR_OPTIONS.map((color) => ({
value: color,
label: t(`fontColor.options.${color}`),
})),
[t]
)
const captionBackgroundColorItems = useMemo(
() =>
CAPTION_COLOR_OPTIONS.map((color) => ({
value: color,
label: t(`backgroundColor.options.${color}`),
})),
[t]
)
const areSubtitlesAvailable = useAreSubtitlesAvailable()
if (!areSubtitlesAvailable) return null
return (
<li>
<H
@@ -34,16 +58,44 @@ export const CaptionsSettings = () => {
>
{t('heading')}
</H>
<Field
type="select"
label={t('textSize.label')}
items={captionTextSizeItems}
selectedKey={snap.captionTextSize}
onSelectionChange={(key) => {
accessibilityStore.captionTextSize = key as CaptionTextSize
}}
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
})}
>
<Field
type="select"
label={t('textSize.label')}
items={captionTextSizeItems}
selectedKey={snap.captionTextSize}
onSelectionChange={(key) => {
accessibilityStore.captionTextSize = key as CaptionTextSize
}}
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
<Field
type="select"
label={t('fontColor.label')}
items={captionFontColorItems}
selectedKey={snap.captionFontColor}
onSelectionChange={(key) => {
accessibilityStore.captionFontColor = key as CaptionColor
}}
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
<Field
type="select"
label={t('backgroundColor.label')}
items={captionBackgroundColorItems}
selectedKey={snap.captionBackgroundColor}
onSelectionChange={(key) => {
accessibilityStore.captionBackgroundColor = key as CaptionColor
}}
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
</div>
</li>
)
}
@@ -12,6 +12,8 @@ import { useSnapshot } from 'valtio'
import {
accessibilityStore,
CAPTION_TEXT_SIZE_OPTIONS,
CAPTION_FONT_COLOR_VALUES,
CAPTION_BACKGROUND_COLOR_VALUES,
type CaptionTextSize,
} from '@/stores/accessibility'
@@ -92,10 +94,14 @@ const useTranscriptionState = () => {
}
const Transcription = ({ row }: { row: TranscriptionRow }) => {
const { captionTextSize } = useSnapshot(accessibilityStore)
const { captionTextSize, captionFontColor, captionBackgroundColor } =
useSnapshot(accessibilityStore)
const participantColor = getParticipantColor(row.participant)
const participantName = getParticipantName(row.participant)
const { fontSize, lineHeight } = CAPTION_FONT_SIZES[captionTextSize]
const fontColor = CAPTION_FONT_COLOR_VALUES[captionFontColor]
const backgroundColor =
CAPTION_BACKGROUND_COLOR_VALUES[captionBackgroundColor]
const getDisplayText = (row: TranscriptionRow): string => {
return row.segments
@@ -128,9 +134,9 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => {
/>
<div
className={css({
color: 'white',
width: '100%',
})}
style={{ color: fontColor }}
>
<Text variant="h3" margin={false}>
{participantName}
@@ -138,8 +144,10 @@ const Transcription = ({ row }: { row: TranscriptionRow }) => {
<p
className={css({
fontWeight: '400',
borderRadius: '4px',
padding: '0.125rem 0.25rem',
})}
style={{ fontSize, lineHeight }}
style={{ fontSize, lineHeight, backgroundColor }}
>
{displayText}
</p>
+37 -14
View File
@@ -273,17 +273,40 @@
"title": "Virtueller Hintergrund",
"selectedLabel": "Hintergrund angewendet:",
"apply": "Ersetze deinen Hintergrund:",
"descriptions": {
"0": "Gerilltes Holzmöbel",
"1": "Besprechungsraum",
"2": "Loft mit schwarzer Glaswand",
"3": "Esszimmer",
"4": "Holzregale",
"5": "Holztreppe",
"6": "Graue Bibliothek",
"7": "Kaffeetheke"
"personal": {
"title": "Meine Hintergründe",
"selectFileTooltip": "Wähle ein Bild aus, das als persönlicher Hintergrund verwendet werden soll",
"notLoggedInWarning": "Du bist nicht angemeldet, der persönliche Hintergrund wird nicht von einem Meeting zum anderen gespeichert.",
"warningUploadDisabled": "Persönliche Hintergründe werden derzeit nicht von einem Meeting zum anderen gespeichert.",
"uploadLimitReached": "Du kannst keine weiteren persönlichen Hintergründe hinzufügen.",
"uploadInProgress": "Datei wird hochgeladen…",
"errors": {
"close": "Schließen",
"file_too_large": {
"title": "Datei zu groß",
"description": "Die Datei ist zu groß. Bitte wähle eine Datei mit weniger als {{maxSize, number}} MB."
},
"invalid_file_type": {
"title": "Ungültiger Dateityp",
"description": "Der Dateityp wird nicht unterstützt. Bitte wähle eine {{allowedExtension}}-Datei."
}
}
},
"presets": {
"title": "Vorschläge",
"descriptions": {
"0": "Gerilltes Holzmöbel",
"1": "Besprechungsraum",
"2": "Loft mit schwarzer Glaswand",
"3": "Esszimmer",
"4": "Holzregale",
"5": "Holztreppe",
"6": "Graue Bibliothek",
"7": "Kaffeetheke"
}
}
},
"faceLandmarks": {
"title": "Visuelle Effekte",
"glasses": {
@@ -297,7 +320,7 @@
}
},
"sidePanel": {
"ariaLabel": "Seitenleiste",
"ariaLabel": "Seitenleiste - {{title}}",
"backToTools": "Zurück zu den Meeting-Tools",
"heading": {
"participants": "Teilnehmende",
@@ -332,11 +355,11 @@
"tools": {
"transcript": {
"title": "Transkribieren",
"body": "Das Gespräch aufzeichnen."
"body": "Wandelt Meetings in Text um."
},
"screenRecording": {
"title": "Aufzeichnen",
"body": "Das Meeting aufzeichnen."
"title": "Aufnehmen",
"body": "Speichert Meetings als Video."
}
}
},
@@ -597,7 +620,7 @@
},
"participantTileFocus": {
"containerLabel": "Optionen für {{name}}",
"toolbarHint": "Ctrl+Shift+/: Direkt auf die Tastenkürzel zugreifen.",
"toolbarHint": "{{shortcut}}: Direkt auf die Tastenkürzel zugreifen.",
"pin": {
"enable": "Anheften",
"disable": "Lösen"
+28
View File
@@ -126,6 +126,34 @@
"medium": "Mittel",
"large": "Groß"
}
},
"fontColor": {
"label": "Schriftfarbe",
"options": {
"default": "Standard",
"white": "Weiß",
"black": "Schwarz",
"blue": "Blau",
"green": "Grün",
"red": "Rot",
"yellow": "Gelb",
"cyan": "Cyan",
"magenta": "Magenta"
}
},
"backgroundColor": {
"label": "Hintergrundfarbe",
"options": {
"default": "Standard",
"white": "Weiß",
"black": "Schwarz",
"blue": "Blau",
"green": "Grün",
"red": "Rot",
"yellow": "Gelb",
"cyan": "Cyan",
"magenta": "Magenta"
}
}
}
},
+36 -14
View File
@@ -270,18 +270,40 @@
}
},
"virtual": {
"title": "Virtual background",
"title": "Virtual backgrounds",
"selectedLabel": "Background applied:",
"apply": "Replace your background:",
"descriptions": {
"0": "Fluted wooden furniture",
"1": "Meeting room",
"2": "Loft with black glass partition",
"3": "Dining room",
"4": "Wooden shelves",
"5": "Wooden staircase",
"6": "Gray library",
"7": "Coffee counter"
"personal": {
"title": "My backgrounds",
"selectFileTooltip": "Select an image file to use as a personal background",
"notLoggedInWarning": "You are not logged-in, personal backgrounds won't be saved from one meeting to the other.",
"warningUploadDisabled": "Personal backgrounds are currently not saved from one meeting to the other.",
"uploadLimitReached": "You cannot upload more personal backgrounds.",
"uploadInProgress": "Image is being uploaded…",
"errors": {
"close": "Close",
"file_too_large": {
"title": "File too large",
"description": "The file is too large. Please choose a file smaller than {{maxSize, number}} MB."
},
"invalid_file_type": {
"title": "Invalid file type",
"description": "The file type is not supported. Please choose a {{allowedExtension, list(type: 'disjunction')}} file."
}
}
},
"presets": {
"title": "Suggestions",
"descriptions": {
"0": "Fluted wooden furniture",
"1": "Meeting room",
"2": "Loft with black glass partition",
"3": "Dining room",
"4": "Wooden shelves",
"5": "Wooden staircase",
"6": "Gray library",
"7": "Coffee counter"
}
}
},
"faceLandmarks": {
@@ -297,7 +319,7 @@
}
},
"sidePanel": {
"ariaLabel": "Sidepanel",
"ariaLabel": "Sidepanel - {{title}}",
"backToTools": "Back to meeting tools",
"heading": {
"participants": "Participants",
@@ -332,11 +354,11 @@
"tools": {
"transcript": {
"title": "Transcribe",
"body": "Record the conversation."
"body": "Turn meetings into text."
},
"screenRecording": {
"title": "Record",
"body": "Record the meeting."
"body": "Save meetings as video."
}
}
},
@@ -597,7 +619,7 @@
},
"participantTileFocus": {
"containerLabel": "Options for {{name}}",
"toolbarHint": "Ctrl+Shift+/: access shortcuts directly.",
"toolbarHint": "{{shortcut}}: access shortcuts directly.",
"pin": {
"enable": "Pin",
"disable": "Unpin"
+28
View File
@@ -126,6 +126,34 @@
"medium": "Medium",
"large": "Large"
}
},
"fontColor": {
"label": "Font color",
"options": {
"default": "Default",
"white": "White",
"black": "Black",
"blue": "Blue",
"green": "Green",
"red": "Red",
"yellow": "Yellow",
"cyan": "Cyan",
"magenta": "Magenta"
}
},
"backgroundColor": {
"label": "Background color",
"options": {
"default": "Default",
"white": "White",
"black": "Black",
"blue": "Blue",
"green": "Green",
"red": "Red",
"yellow": "Yellow",
"cyan": "Cyan",
"magenta": "Magenta"
}
}
}
},
+36 -14
View File
@@ -270,18 +270,40 @@
}
},
"virtual": {
"title": "Arrière-plan virtuel",
"title": "Arrière-plans virtuels",
"selectedLabel": "Arrière-plan appliqué :",
"apply": "Remplacer votre arrière plan :",
"descriptions": {
"0": "Meuble cannelé en bois",
"1": "Salle de réunion",
"2": "Loft avec verrière noire",
"3": "Salle à manger",
"4": "Étagères en bois",
"5": "Escalier en bois",
"6": "Bibliothèque grise",
"7": "Comptoir de café"
"personal": {
"title": "Mes arrière-plans",
"selectFileTooltip": "Sélectionnez une image à utiliser comme arrière-plan",
"notLoggedInWarning": "Vous n'êtes pas connecté, l'arrière-plan personnel ne sera pas sauvegardé d'une réunion à l'autre.",
"warningUploadDisabled": "Les arrière-plans personnels ne sont actuellement pas sauvegardés d'une réunion à l'autre.",
"uploadLimitReached": "Vous ne pouvez pas ajouter plus d'arrière-plans personnels.",
"uploadInProgress": "Image en cours d'envoi…",
"errors": {
"close": "Fermer",
"file_too_large": {
"title": "Fichier trop volumineux",
"description": "Le fichier est trop volumineux. Veuillez choisir un fichier de moins de {{maxSize, number}} Mo."
},
"invalid_file_type": {
"title": "Type de fichier non valide",
"description": "Le type de fichier n'est pas pris en charge. Veuillez choisir un fichier {{allowedExtension}}."
}
}
},
"presets": {
"title": "Suggestions",
"descriptions": {
"0": "Meuble cannelé en bois",
"1": "Salle de réunion",
"2": "Loft avec verrière noire",
"3": "Salle à manger",
"4": "Étagères en bois",
"5": "Escalier en bois",
"6": "Bibliothèque grise",
"7": "Comptoir de café"
}
}
},
"faceLandmarks": {
@@ -297,7 +319,7 @@
}
},
"sidePanel": {
"ariaLabel": "Panneau latéral",
"ariaLabel": "Panneau latéral - {{title}}",
"backToTools": "Retour aux outils de réunion",
"heading": {
"participants": "Participants",
@@ -332,11 +354,11 @@
"tools": {
"transcript": {
"title": "Transcrire",
"body": "Enregistrer la conversation."
"body": "Transcrire la réunion."
},
"screenRecording": {
"title": "Enregistrer",
"body": "Enregistrer la réunion."
"body": "Enregistrer la réunion en vidéo."
}
}
},
@@ -597,7 +619,7 @@
},
"participantTileFocus": {
"containerLabel": "Options pour {{name}}",
"toolbarHint": "Ctrl+Shift+/ : accéder directement aux raccourcis.",
"toolbarHint": "{{shortcut}} : accéder directement aux raccourcis.",
"pin": {
"enable": "Épingler",
"disable": "Annuler l'épinglage"
+28
View File
@@ -126,6 +126,34 @@
"medium": "Moyen",
"large": "Grand"
}
},
"fontColor": {
"label": "Couleur du texte",
"options": {
"default": "Par défaut",
"white": "Blanc",
"black": "Noir",
"blue": "Bleu",
"green": "Vert",
"red": "Rouge",
"yellow": "Jaune",
"cyan": "Cyan",
"magenta": "Magenta"
}
},
"backgroundColor": {
"label": "Couleur de fond",
"options": {
"default": "Par défaut",
"white": "Blanc",
"black": "Noir",
"blue": "Bleu",
"green": "Vert",
"red": "Rouge",
"yellow": "Jaune",
"cyan": "Cyan",
"magenta": "Magenta"
}
}
}
},
+35 -13
View File
@@ -273,15 +273,37 @@
"title": "Virtuele achtergrond",
"selectedLabel": "Achtergrond toegepast:",
"apply": "Vervang je achtergrond:",
"descriptions": {
"0": "Geprofileerd houten meubel",
"1": "Vergaderruimte",
"2": "Loft met zwarte glaswand",
"3": "Eetkamer",
"4": "Houten planken",
"5": "Houten trap",
"6": "Grijze bibliotheek",
"7": "Koffiebar"
"personal": {
"title": "Mijn achtergronden",
"selectFileTooltip": "Selecteer een afbeeldingsbestand om te gebruiken als persoonlijke achtergrond",
"notLoggedInWarning": "U bent niet ingelogd, persoonlijke achtergronden worden niet opgeslagen van de ene vergadering naar de andere.",
"warningUploadDisabled": "Persoonlijke achtergronden worden momenteel niet opgeslagen van de ene vergadering naar de andere.",
"uploadLimitReached": "U kunt geen persoonlijke achtergronden meer uploaden.",
"uploadInProgress": "Afbeelding wordt geüpload…",
"errors": {
"close": "Sluiten",
"file_too_large": {
"title": "Bestand te groot",
"description": "Het bestand is te groot. Kies een bestand kleiner dan {{maxSize, number}} MB."
},
"invalid_file_type": {
"title": "Ongeldig bestandstype",
"description": "Het bestandstype wordt niet ondersteund. Kies een {{allowedExtension, list(type: 'disjunction')}} bestand."
}
}
},
"presets": {
"title": "Suggesties",
"descriptions": {
"0": "Geprofileerd houten meubel",
"1": "Vergaderruimte",
"2": "Loft met zwarte glaswand",
"3": "Eetkamer",
"4": "Houten planken",
"5": "Houten trap",
"6": "Grijze bibliotheek",
"7": "Koffiebar"
}
}
},
"faceLandmarks": {
@@ -297,7 +319,7 @@
}
},
"sidePanel": {
"ariaLabel": "Zijbalk",
"ariaLabel": "Zijbalk - {{title}}",
"backToTools": "Terug naar vergadertools",
"heading": {
"participants": "Deelnemers",
@@ -332,11 +354,11 @@
"tools": {
"transcript": {
"title": "Transcriberen",
"body": "Het gesprek opnemen."
"body": "Zet vergaderingen om in tekst."
},
"screenRecording": {
"title": "Opnemen",
"body": "De vergadering opnemen."
"body": "Sla vergaderingen op als video."
}
}
},
@@ -597,7 +619,7 @@
},
"participantTileFocus": {
"containerLabel": "Opties voor {{name}}",
"toolbarHint": "Ctrl+Shift+/: direct toegang tot de sneltoetsen.",
"toolbarHint": "{{shortcut}}: direct toegang tot de sneltoetsen.",
"pin": {
"enable": "Pinnen",
"disable": "Losmaken"
+28
View File
@@ -126,6 +126,34 @@
"medium": "Gemiddeld",
"large": "Groot"
}
},
"fontColor": {
"label": "Tekstkleur",
"options": {
"default": "Standaard",
"white": "Wit",
"black": "Zwart",
"blue": "Blauw",
"green": "Groen",
"red": "Rood",
"yellow": "Geel",
"cyan": "Cyaan",
"magenta": "Magenta"
}
},
"backgroundColor": {
"label": "Achtergrondkleur",
"options": {
"default": "Standaard",
"white": "Wit",
"black": "Zwart",
"blue": "Blauw",
"green": "Groen",
"red": "Rood",
"yellow": "Geel",
"cyan": "Cyaan",
"magenta": "Magenta"
}
}
}
},
+5 -1
View File
@@ -20,7 +20,11 @@ export const Spinner = ({
const r = 14 - strokeWidth
const c = 2 * r * Math.PI
return (
<ProgressBar aria-label="Loading..." value={30}>
<ProgressBar
aria-label="Loading..."
value={30}
style={{ height: size, width: size }}
>
{({ percentage }) => (
<div
className={css({
+4
View File
@@ -36,6 +36,10 @@ export const text = cva({
textStyle: 'body',
fontWeight: 'bold',
},
bodyXsMedium: {
textStyle: 'body',
fontWeight: 'medium',
},
body: {
textStyle: 'body',
},
+66 -2
View File
@@ -10,14 +10,65 @@ export const CAPTION_TEXT_SIZE_OPTIONS: CaptionTextSize[] = [
'large',
]
export type CaptionColor =
| 'default'
| 'white'
| 'black'
| 'blue'
| 'green'
| 'red'
| 'yellow'
| 'cyan'
| 'magenta'
export const CAPTION_COLOR_OPTIONS: CaptionColor[] = [
'default',
'white',
'black',
'blue',
'green',
'red',
'yellow',
'cyan',
'magenta',
]
export const CAPTION_FONT_COLOR_VALUES: Record<CaptionColor, string> = {
default: '#FFFFFF',
white: '#FFFFFF',
black: '#000000',
blue: '#0000FF',
green: '#00FF00',
red: '#FF0000',
yellow: '#FFFF00',
cyan: '#00FFFF',
magenta: '#FF00FF',
}
export const CAPTION_BACKGROUND_COLOR_VALUES: Record<CaptionColor, string> = {
default: 'rgba(0, 0, 0, 0.75)',
black: 'rgba(0, 0, 0, 0.75)',
white: 'rgba(255, 255, 255, 0.75)',
blue: 'rgba(0, 0, 255, 0.75)',
green: 'rgba(0, 255, 0, 0.75)',
red: 'rgba(255, 0, 0, 0.75)',
yellow: 'rgba(255, 255, 0, 0.75)',
cyan: 'rgba(0, 255, 255, 0.75)',
magenta: 'rgba(255, 0, 255, 0.75)',
}
type AccessibilityState = {
announceReactions: boolean
captionTextSize: CaptionTextSize
captionFontColor: CaptionColor
captionBackgroundColor: CaptionColor
}
const DEFAULT_STATE: AccessibilityState = {
announceReactions: false,
captionTextSize: 'medium',
captionFontColor: 'default',
captionBackgroundColor: 'default',
}
function getAccessibilityState(): AccessibilityState {
@@ -25,10 +76,21 @@ function getAccessibilityState(): AccessibilityState {
const stored = localStorage.getItem(STORAGE_KEYS.ACCESSIBILITY)
if (stored) {
const parsed = JSON.parse(stored)
const validCaptionSizes = CAPTION_TEXT_SIZE_OPTIONS
const captionTextSize = validCaptionSizes.includes(parsed.captionTextSize)
const captionTextSize = CAPTION_TEXT_SIZE_OPTIONS.includes(
parsed.captionTextSize
)
? parsed.captionTextSize
: DEFAULT_STATE.captionTextSize
const captionFontColor = CAPTION_COLOR_OPTIONS.includes(
parsed.captionFontColor
)
? parsed.captionFontColor
: DEFAULT_STATE.captionFontColor
const captionBackgroundColor = CAPTION_COLOR_OPTIONS.includes(
parsed.captionBackgroundColor
)
? parsed.captionBackgroundColor
: DEFAULT_STATE.captionBackgroundColor
return {
...DEFAULT_STATE,
...parsed,
@@ -37,6 +99,8 @@ function getAccessibilityState(): AccessibilityState {
? parsed.announceReactions
: DEFAULT_STATE.announceReactions,
captionTextSize,
captionFontColor,
captionBackgroundColor,
}
}
+33 -4
View File
@@ -1,16 +1,19 @@
import { proxy, subscribe } from 'valtio'
import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur'
import {
ProcessorConfig,
ProcessorType,
} from '@/features/rooms/livekit/components/blur'
import {
loadUserChoices,
saveUserChoices,
LocalUserChoices as LocalUserChoicesLK,
saveUserChoices,
} from '@livekit/components-core'
import { VideoQuality } from 'livekit-client'
export type VideoResolution = 'h720' | 'h360' | 'h180'
export type LocalUserChoices = LocalUserChoicesLK & {
processorSerialized?: ProcessorSerialized
processorConfig?: ProcessorConfig
noiseReductionEnabled?: boolean
audioOutputDeviceId?: string
videoPublishResolution?: VideoResolution
@@ -28,7 +31,33 @@ function getUserChoicesState(): LocalUserChoices {
}
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
subscribe(userChoicesStore, () => {
saveUserChoices(userChoicesStore, false)
})
// we run some logic on store loading to check if the processor config is still valid
if (userChoicesStore.processorConfig?.type === ProcessorType.VIRTUAL) {
if (userChoicesStore.processorConfig.imagePath.startsWith('blob:')) {
// this happens when a not authenticated user had changed their background image
// we restore clear the processor config to avoid displaying a black screen.
userChoicesStore.processorConfig = undefined
} else if (userChoicesStore.processorConfig.fileId) {
// Checking if the image is still available / accessible
await fetch(userChoicesStore.processorConfig.imagePath, {
// We bypass the cache to ensure we have access
cache: 'reload',
})
.then((response) => {
// if we cannot fetch the image (likely a 401 from the backend because
// the user is not logged in anymore, etc.),
// we clear the processor config to avoid displaying a black screen.
// This can happen when the user logs out for instance, etc.
if (!response.ok) {
userChoicesStore.processorConfig = undefined
}
})
.catch(() => {
userChoicesStore.processorConfig = undefined
})
}
}
+11
View File
@@ -95,3 +95,14 @@ html:has(.lk-video-conference) {
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
.hoverGroup .hoverGroupChild {
opacity: 0;
pointer-events: none;
}
.hoverGroup:hover .hoverGroupChild,
.hoverGroup:focus-within .hoverGroupChild {
opacity: 1;
pointer-events: auto;
}
+8
View File
@@ -14,6 +14,14 @@ export default defineConfig(({ mode }) => {
port: parseInt(env.VITE_PORT) || 3000,
host: env.VITE_HOST ?? 'localhost',
allowedHosts: ['.nip.io'],
// In a local dev setup, we proxy the media server ourselves to avoid CORS issues
proxy: {
'/media': {
target: 'http://localhost:8083',
changeOrigin: true,
secure: false
}
}
},
}
})
@@ -1,3 +1,29 @@
_summaryEnvVars: &summaryEnvVars
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000/
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
_summaryImage: &summaryImage
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
image:
repository: localhost:5001/meet-backend
pullPolicy: Always
@@ -142,66 +168,15 @@ posthog:
summary:
replicas: 1
image: *summaryImage
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "uvicorn"
- "summary.main:app"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--reload"
<<: *summaryEnvVars
celeryTranscribe:
replicas: 1
image: *summaryImage
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
<<: *summaryEnvVars
command:
- "celery"
- "-A"
@@ -213,31 +188,9 @@ celeryTranscribe:
celerySummarize:
replicas: 1
image: *summaryImage
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
<<: *summaryEnvVars
command:
- "celery"
- "-A"
@@ -1,3 +1,29 @@
_summaryEnvVars: &summaryEnvVars
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000/
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
_summaryImage: &summaryImage
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
image:
repository: localhost:5001/meet-backend
pullPolicy: Always
@@ -160,69 +186,15 @@ posthog:
summary:
replicas: 1
image: *summaryImage
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000/
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "uvicorn"
- "summary.main:app"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--reload"
<<: *summaryEnvVars
celeryTranscribe:
replicas: 1
image: *summaryImage
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000/
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
<<: *summaryEnvVars
command:
- "celery"
- "-A"
@@ -235,32 +207,9 @@ celeryTranscribe:
celerySummarize:
replicas: 1
image: *summaryImage
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000/
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
<<: *summaryEnvVars
command:
- "celery"
- "-A"
@@ -1,43 +0,0 @@
replicaCount: 1
terminationGracePeriodSeconds: 18000
egress:
log_level: debug
ws_url: ws://livekit-livekit-server:80
insecure: true
enable_chrome_sandbox: true
{{- with .Values.livekit.keys }}
{{- range $key, $value := . }}
api_key: {{ $key }}
api_secret: {{ $value }}
{{- end }}
{{- end }}
redis:
address: redis-master:6379
password: pass
s3:
access_key: meet
secret: password
region: local
bucket: meet-media-storage
endpoint: http://minio:9000
force_path_style: true
loadBalancer:
type: nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
- hosts:
- livekit-egress.127.0.0.1.nip.io
secretName: livekit-egress-dinum-cert
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
nodeSelector: {}
resources: {}
@@ -1,49 +0,0 @@
replicaCount: 1
terminationGracePeriodSeconds: 18000
image:
repository: localhost:5001/meet-livekit
pullPolicy: Always
tag: "latest"
livekit:
log_level: debug
rtc:
use_external_ip: false
port_range_start: 50000
port_range_end: 60000
tcp_port: 7881
redis:
address: redis-master:6379
password: pass
keys:
turn:
enabled: true
udp_port: 443
domain: livekit.127.0.0.1.nip.io
loadBalancerAnnotations: {}
webhook:
api_key:
urls:
- https://meet.127.0.0.1.nip.io/api/v1.0/rooms/webhooks-livekit/
loadBalancer:
type: nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
- hosts:
- livekit.127.0.0.1.nip.io
secretName: livekit-dinum-cert
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 60
nodeSelector: {}
resources: {}
-290
View File
@@ -1,290 +0,0 @@
secrets:
- name: oidcLogin
itemId: a25effec-eaea-4ce1-9ed8-3a3cc1c734db
field: username
podVariable: OIDC_RP_CLIENT_ID
clusterSecretStore: bitwarden-login-meet
- name: oidcPass
itemId: a25effec-eaea-4ce1-9ed8-3a3cc1c734db
field: password
podVariable: OIDC_RP_CLIENT_SECRET
clusterSecretStore: bitwarden-login-meet
- name: brevoApiKey
itemId: 99107889-6124-4436-97cc-a5193f28443f
field: password
podVariable: BREVO_API_KEY
clusterSecretStore: bitwarden-login-meet
image:
repository: localhost:5001/meet-backend
pullPolicy: Always
tag: "latest"
backend:
replicas: 1
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: https://meet.127.0.0.1.nip.io,http://meet.127.0.0.1.nip.io
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: meet.127.0.0.1.nip.io
DJANGO_SECRET_KEY: {{ .Values.djangoSecretKey }}
DJANGO_SETTINGS_MODULE: meet.settings
DJANGO_SILENCED_SYSTEM_CHECKS: security.W004, security.W008
DJANGO_SUPERUSER_PASSWORD: admin
DJANGO_EMAIL_HOST: "mailcatcher"
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_USE_SSL: False
DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique"
DJANGO_EMAIL_SUPPORT_EMAIL: "test@yopmail.com"
DJANGO_EMAIL_LOGO_IMG: https://meet.127.0.0.1.nip.io/assets/logo-suite-numerique.png
DJANGO_EMAIL_DOMAIN: meet.127.0.0.1.nip.io
DJANGO_EMAIL_APP_BASE_URL: https://meet.127.0.0.1.nip.io
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
OIDC_RP_CLIENT_ID:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_ID
OIDC_RP_CLIENT_SECRET:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email given_name usual_name"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.127.0.0.1.nip.io
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://meet.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
DB_HOST: postgres
DB_NAME: meet
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
{{- range $key, $value := . }}
LIVEKIT_API_SECRET: {{ $value }}
LIVEKIT_API_KEY: {{ $key }}
{{- end }}
{{- end }}
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX: "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_REGION_NAME: local
MEDIA_BASE_URL: https://meet.127.0.0.1.nip.io
RECORDING_ENABLE: True
RECORDING_STORAGE_EVENT_ENABLE: True
RECORDING_STORAGE_EVENT_TOKEN: password
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
SUMMARY_SERVICE_API_TOKEN: password
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
SIGNUP_NEW_USER_TO_MARKETING_EMAIL: True
BREVO_API_KEY:
secretKeyRef:
name: backend
key: BREVO_API_KEY
BREVO_API_CONTACT_LIST_IDS: 8
ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
migrate:
command:
- "/bin/sh"
- "-c"
- |
python manage.py migrate --no-input
restartPolicy: Never
command:
- "gunicorn"
- "-c"
- "/usr/local/etc/gunicorn/meet.py"
- "meet.wsgi:application"
- "--reload"
createsuperuser:
command:
- "/bin/sh"
- "-c"
- |
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
extraVolumes:
- name: certs
configMap:
name: certifi
items:
- key: cacert.pem
path: cacert.pem
frontend:
envVars:
VITE_APP_TITLE: "LaSuite Meet"
VITE_PORT: 8080
VITE_HOST: 0.0.0.0
VITE_API_BASE_URL: https://meet.127.0.0.1.nip.io/
replicas: 1
image:
repository: localhost:5001/meet-frontend
pullPolicy: Always
tag: "latest"
ingress:
enabled: true
host: meet.127.0.0.1.nip.io
ingressAdmin:
enabled: true
host: meet.127.0.0.1.nip.io
ingressWebhook:
enabled: true
host: meet.127.0.0.1.nip.io
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
summary:
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "uvicorn"
- "summary.main:app"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--reload"
celeryTranscribe:
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "celery"
- "-A"
- "summary.core.celery_worker"
- "worker"
- "--pool=solo"
- "--loglevel=info"
- "-Q transcribe-queue"
celerySummarize:
replicas: 1
envVars:
APP_NAME: summary-microservice
APP_API_TOKEN: password
AWS_STORAGE_BUCKET_NAME: meet-media-storage
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password
AWS_S3_SECURE_ACCESS: False
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
image:
repository: localhost:5001/meet-summary
pullPolicy: Always
tag: "latest"
command:
- "celery"
- "-A"
- "summary.core.celery_worker"
- "worker"
- "--pool=solo"
- "--loglevel=info"
- "-Q summarize-queue"
ingressMedia:
enabled: true
host: meet.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
serviceMedia:
host: minio.meet.svc.cluster.local
port: 9000
-10
View File
@@ -1,10 +0,0 @@
djangoSecretKey: 7K9mQ2xR8pL3vN6tY1sW4jH5cE0zF9bM2qA7uI3oP6rT1wErt12te12
livekit:
keys:
devkey: secret
webhook:
api_key: devkey
livekitApi:
key: devkey
secret: secret
-4
View File
@@ -7,10 +7,6 @@ environments:
values:
- version: 0.0.1
- env.d/dev-dinum/values.secrets.yaml
dev:
values:
- version: 0.0.1
- env.d/dev/values.secrets.yaml
---
repositories:
+1 -1
View File
@@ -9,7 +9,7 @@
align="center"
src="{{logo_img}}"
width="320px"
alt="{%trans 'Logo email' %}"
alt="{%trans 'Logo email' %} {{brandname}}"
/>
</mj-column>
</mj-section>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.10.0",
"version": "1.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.10.0",
"version": "1.12.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.10.0",
"version": "1.12.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.10.0",
"version": "1.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.10.0",
"version": "1.12.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.10.0",
"version": "1.12.0",
"author": "",
"license": "ISC",
"description": "",
+17 -8
View File
@@ -1,28 +1,36 @@
[project]
name = "summary"
version = "1.10.0"
version = "1.12.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"celery==5.5.3",
"celery==5.6.2",
"redis==5.2.1",
"minio==7.2.18",
"minio==7.2.20",
"mutagen==1.47.0",
"openai==2.7.1",
"posthog==6.9.1",
"openai==2.28.0",
"posthog==7.9.12",
"requests==2.32.5",
"sentry-sdk[fastapi, celery]==2.43.0",
"langfuse==3.10.6"
"sentry-sdk[fastapi, celery]==2.54.0",
"langfuse==4.0.0"
]
[project.optional-dependencies]
dev = [
"ruff==0.14.4",
"ruff==0.15.6",
"pytest==9.0.2",
"responses>=0.25.8",
]
[tool.pytest.ini_options]
markers = [
"api: Test the API",
]
testpaths = ["tests"]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
@@ -49,6 +57,7 @@ select = [
"T20", # flake8-print
"W", # pycodestyle warning
]
ignore= ["PLR2004"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = [
+1 -2
View File
@@ -23,8 +23,7 @@ Einige Punkte, die wir Ihnen empfehlen zu überprüfen:
""",
download_header_template=(
"\n*Laden Sie Ihre Aufnahme herunter, "
"indem Sie [diesem Link folgen]({download_link})*\n"
"\n*[Laden Sie hier Ihre Aufnahme herunter (externer Link)]({download_link})*\n"
),
hallucination_replacement_text="[Text konnte nicht transkribiert werden]",
document_default_title="Transkription",
+1 -1
View File
@@ -23,7 +23,7 @@ A few things we recommend you check:
""",
download_header_template=(
"\n*Download your recording by [following this link]({download_link})*\n"
"\n*[Download your recording (external link)]({download_link})*\n"
),
hallucination_replacement_text="[Unable to transcribe text]",
document_default_title="Transcription",
+1 -1
View File
@@ -23,7 +23,7 @@ Quelques points que nous vous conseillons de vérifier :
""",
download_header_template=(
"\n*Télécharger votre enregistrement en [suivant ce lien]({download_link})*\n"
"\n*[Télécharger votre enregistrement (lien externe)]({download_link})*\n"
),
hallucination_replacement_text="[Texte impossible à transcrire]",
document_default_title="Transcription",
+1 -1
View File
@@ -23,7 +23,7 @@ Een paar punten die wij u aanraden te controleren:
""",
download_header_template=(
"\n*Download uw opname door [deze link te volgen]({download_link})*\n"
"\n*[Download hier je opname (externe link)]({download_link})*\n"
),
hallucination_replacement_text="[Tekst kon niet worden getranscribeerd]",
document_default_title="Transcriptie",
+1
View File
@@ -0,0 +1 @@
"""Tests for the summary service."""
+1
View File
@@ -0,0 +1 @@
"""Tests for the API summary service."""
+21
View File
@@ -0,0 +1,21 @@
"""Integration tests for the health check endpoints."""
class TestHeartbeat:
"""Tests for the /__heartbeat__ endpoint."""
def test_returns_200(self, client):
"""The heartbeat endpoint responds with 200 OK without a token."""
response = client.get("/__heartbeat__")
assert response.status_code == 200
class TestLBHeartbeat:
"""Tests for the /__lbheartbeat__ endpoint."""
def test_returns_200(self, client):
"""The load-balancer heartbeat endpoint responds with 200 OK without a token."""
response = client.get("/__lbheartbeat__")
assert response.status_code == 200
+88
View File
@@ -0,0 +1,88 @@
"""Integration tests for the API tasks endpoints."""
# tests/unit/test_api_tasks.py
from unittest.mock import MagicMock, patch
class TestTasks:
"""Tests for the /tasks endpoint."""
@patch(
"summary.api.route.tasks.process_audio_transcribe_summarize_v2.apply_async",
return_value=MagicMock(id="task-id-abc"),
)
@patch("summary.api.route.tasks.time.time", return_value=1735725600.0)
def test_create_task_returns_task_id(self, mock_time, mock_apply_async, client):
"""POST /tasks/ with valid payload returns id and dispatches Celery task."""
response = client.post(
"api/v1/tasks/",
headers={"Authorization": "Bearer test-api-token"},
json={
"owner_id": "owner-123",
"filename": "recording.mp4",
"email": "user@example.com",
"sub": "sub-123",
"room": "room-abc",
"recording_date": "2026-01-01",
"recording_time": "10:00:00",
"language": None,
"download_link": "http://example.com/file.mp4",
},
)
assert response.status_code == 200
assert response.json() == {"id": "task-id-abc", "message": "Task created"}
args = mock_apply_async.call_args.kwargs["args"]
assert args == [
"owner-123", # owner_id
"recording.mp4", # filename
"user@example.com", # email
"sub-123", # sub
1735725600.0, # frozen time
"room-abc", # room
"2026-01-01", # recording_date
"10:00:00", # recording_time
None, # language
"http://example.com/file.mp4", # download_link
None, # context_language
]
def test_create_task_invalid_language(self, client):
"""POST /tasks/ with an unsupported language returns 422."""
payload = {"language": "klingon"}
response = client.post(
"/api/v1/tasks/",
headers={"Authorization": "Bearer test-api-token"},
json=payload,
)
assert response.status_code == 422
@patch(
"summary.api.route.tasks.AsyncResult",
return_value=MagicMock(status="PENDING"),
)
def test_get_task_status_pending(self, mock_result, client):
"""GET /tasks/{id} returns PENDING status when the task has not started yet."""
response = client.get(
"/api/v1/tasks/task-id-abc",
headers={"Authorization": "Bearer test-api-token"},
)
assert response.status_code == 200
assert response.json() == {"id": "task-id-abc", "status": "PENDING"}
@patch(
"summary.api.route.tasks.AsyncResult",
return_value=MagicMock(status="SUCCESS"),
)
def test_get_task_status_success(self, mock_result, client):
"""GET /tasks/{id} returns SUCCESS status when the task has completed."""
response = client.get(
"/api/v1/tasks/task-id-abc",
headers={"Authorization": "Bearer test-api-token"},
)
assert response.status_code == 200
assert response.json()["status"] == "SUCCESS"
+24
View File
@@ -0,0 +1,24 @@
"""Integration test configuration. Provides shared fixtures."""
import pytest
from fastapi.testclient import TestClient
from pydantic import SecretStr
from summary.core.config import Settings, get_settings
from summary.main import app
def get_settings_override():
"""Return settings for tests."""
return Settings(
app_api_token=SecretStr("test-api-token"),
)
@pytest.fixture()
def client():
"""Provide a FastAPI TestClient for tests."""
client = TestClient(app)
app.dependency_overrides[get_settings] = get_settings_override
return client