Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acd7c51758 | |||
| 7e286fcb4b |
@@ -19,8 +19,6 @@ env:
|
||||
DOCKER_USER: 1001:127
|
||||
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
|
||||
DOCKER_CONTAINER_REGISTRY_NAMESPACE: lasuite
|
||||
IS_MULTI_PLATFORM_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
BUILD_PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
|
||||
|
||||
jobs:
|
||||
build-and-push-backend:
|
||||
@@ -33,7 +31,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
@@ -63,7 +60,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -79,7 +76,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
@@ -110,7 +106,7 @@ jobs:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -126,7 +122,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
@@ -157,7 +152,7 @@ jobs:
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -173,7 +168,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
@@ -206,7 +200,7 @@ jobs:
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -222,7 +216,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
@@ -255,7 +248,7 @@ jobs:
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -305,7 +305,6 @@ jobs:
|
||||
working-directory: src/summary
|
||||
|
||||
env:
|
||||
V1_TENANT_ID: 'test-tenant'
|
||||
AUTHORIZED_TENANTS: '[{"id": "test-tenant", "api_key": "test-api-token", "webhook_url": "https://example.com/webhook", "webhook_api_key": "test-webhook-api-key"}]'
|
||||
AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL: "minio:9000"
|
||||
|
||||
@@ -83,6 +83,3 @@ docker/livekit/out
|
||||
|
||||
# LiveKit CA configuration
|
||||
docker/livekit/rootCA.pem
|
||||
|
||||
# Frontend rollup-plugin-visualizer
|
||||
/src/frontend/rollup-plugin-visualizer/*
|
||||
|
||||
@@ -8,51 +8,6 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.20.0] - 2026-06-12
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(addon) improve Outlook add-on: i18n support, feedback link, smarter link
|
||||
- ⬆️(frontend) upgrade react-i18next from 15.1.1 to 17.0.8
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix noise reduction left-channel-only audio
|
||||
|
||||
## [1.19.0] - 2026-06-04
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) add file specific admin #1387
|
||||
|
||||
### Changed
|
||||
|
||||
- 🐛(agents) fix bug when closing metadata-collector
|
||||
- ⬆️(dependencies) update python dependencies
|
||||
- ⬆️(frontend) update js dependencies
|
||||
- ♻️(agents) replace deprecated room options API
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔇(summary) make ffmpeg quiet #1404
|
||||
- 🔒️(backend) prevent accessing files if they are not ready #1395
|
||||
- ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
|
||||
|
||||
## [1.18.0] - 2026-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- 🔧(backend) backport logging configuration from docs
|
||||
- 🧑💻(backend) add management command to merge duplicate users
|
||||
- 👷(helm) add Kubernetes job for duplicate user merge command
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) prevent duplicate pending users on concurrent requests
|
||||
- 🔒️(backend) prevent file change post checks #1377
|
||||
|
||||
## [1.17.0] - 2026-05-31
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(fullstack) allow participants to mute others based on room configuration
|
||||
@@ -60,21 +15,12 @@ and this project adheres to
|
||||
- ✨(frontend) make reaction toolbar responsive on small viewports
|
||||
- ✨(frontend) enable reactions on mobile devices
|
||||
- ✨(frontend) introduce picture-in-picture meeting
|
||||
- ✨(backend) add core.recording.event.parsers.S3Parser
|
||||
- ✨(summary) extended support for all video / audio files #1358
|
||||
|
||||
### Changed
|
||||
### Changed
|
||||
|
||||
- ♻️(fullstack) simplify source serialization
|
||||
- ✨(backend) expose room configuration to all API consumers
|
||||
- 🩹(frontend) improve reaction toolbar centering with dynamic positioning
|
||||
- 🚀 (paas) remove buildpack requirements.txt to use the new uv.lock #1349
|
||||
- ✨(backend) allow room configuration and access level via external api #1260
|
||||
- ♻️(backend) prefix Swagger routes with /api
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🩹(backend) fix swagger and redoc documentation URLs
|
||||
|
||||
## [1.16.0] - 2026-05-13
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ docker_build(
|
||||
live_update=[
|
||||
sync('../src/backend', '/app'),
|
||||
run(
|
||||
'uv sync --locked --no-dev',
|
||||
trigger=['../src/backend/uv.lock', '../src/backend/pyproject.toml']
|
||||
'pip install -r /app/requirements.txt',
|
||||
trigger=['./api/requirements.txt']
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -110,20 +110,11 @@ k8s_resource('meet-celery-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summarize', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summary-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-transcribe-default', resource_deps=['redis'])
|
||||
k8s_resource('meet-backend-migrate', resource_deps=['meet-backend'])
|
||||
k8s_resource('livekit-livekit-server', resource_deps=['redis'])
|
||||
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
|
||||
k8s_resource('keycloak', resource_deps=['kc-postgresql'])
|
||||
# Trigger once on launch
|
||||
k8s_resource(
|
||||
'meet-backend-createsuperuser',
|
||||
resource_deps=['meet-backend-migrate'],
|
||||
trigger_mode=TRIGGER_MODE_MANUAL,
|
||||
)
|
||||
k8s_resource(
|
||||
'meet-backend-migrate',
|
||||
resource_deps=['meet-backend'],
|
||||
trigger_mode=TRIGGER_MODE_MANUAL,
|
||||
)
|
||||
k8s_resource('meet-backend-createsuperuser', resource_deps=['meet-backend-migrate'])
|
||||
|
||||
migration = '''
|
||||
set -eu
|
||||
|
||||
@@ -47,3 +47,4 @@ mv src/backend/* ./
|
||||
mv deploy/paas/* ./
|
||||
|
||||
echo "3.13" > .python-version
|
||||
echo "." > requirements.txt
|
||||
|
||||
@@ -60,8 +60,6 @@ USER root
|
||||
|
||||
# Security patches for known CVEs
|
||||
RUN apk update && apk upgrade \
|
||||
libcrypto3>=3.5.7-r0 \
|
||||
libssl3>=3.5.7-r0 \
|
||||
musl \
|
||||
musl-utils \
|
||||
zlib>=1.3.2-r0 \
|
||||
|
||||
@@ -48,12 +48,9 @@ server {
|
||||
|
||||
set $nonce $request_id;
|
||||
|
||||
set $csp "default-src 'self'; upgrade-insecure-requests; ";
|
||||
set $csp "upgrade-insecure-requests; ";
|
||||
set $csp "${csp}frame-ancestors ${ms_domains}; ";
|
||||
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
|
||||
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
|
||||
set $csp "${csp}img-src 'self' data:; ";
|
||||
set $csp "${csp}font-src 'self' data:; ";
|
||||
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
|
||||
set $csp "${csp}frame-src 'none'; ";
|
||||
set $csp "${csp}object-src 'none'; ";
|
||||
|
||||
@@ -185,7 +185,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -199,6 +198,10 @@ paths:
|
||||
description: |
|
||||
Creates a new room with secure defaults for external API usage.
|
||||
|
||||
**Restrictions:**
|
||||
- Rooms are always created with `trusted` access (no public rooms via API)
|
||||
- Room access_level can be updated from the webapp interface.
|
||||
|
||||
**Defaults:**
|
||||
- Delegated user is set as owner
|
||||
- Room slug auto-generated for uniqueness
|
||||
@@ -215,17 +218,8 @@ paths:
|
||||
$ref: '#/components/schemas/RoomCreate'
|
||||
examples:
|
||||
emptyBody:
|
||||
summary: No parameters (use all defaults)
|
||||
value: { }
|
||||
withAccessLevel:
|
||||
summary: Specify access level
|
||||
value:
|
||||
access_level: "trusted"
|
||||
withConfiguration:
|
||||
summary: Provide room configuration
|
||||
value:
|
||||
configuration:
|
||||
everyone_can_mute: true
|
||||
summary: No parameters (default)
|
||||
value: {}
|
||||
responses:
|
||||
'201':
|
||||
description: Room created successfully
|
||||
@@ -275,7 +269,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -351,56 +344,8 @@ components:
|
||||
|
||||
RoomCreate:
|
||||
type: object
|
||||
description: |
|
||||
Optional fields for room creation. All fields have secure defaults if omitted.
|
||||
properties:
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
RoomConfiguration:
|
||||
type: object
|
||||
description: |
|
||||
Optional room behaviour settings. Unknown fields are rejected.
|
||||
All fields are optional and default to `null` (server-side defaults apply).
|
||||
properties:
|
||||
can_publish_sources:
|
||||
type: array
|
||||
nullable: true
|
||||
description: |
|
||||
Restricts which media tracks participants are allowed to publish.
|
||||
If `null`, all sources are permitted.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- camera
|
||||
- microphone
|
||||
- screen_share
|
||||
- screen_share_audio
|
||||
example: [ "camera", "microphone" ]
|
||||
everyone_can_mute:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: |
|
||||
Whether any participant can mute others, or only the room owner/moderator.
|
||||
If `null`, the server default applies.
|
||||
example: true
|
||||
additionalProperties: false
|
||||
|
||||
RoomAccessLevel:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- trusted
|
||||
- restricted
|
||||
description: |
|
||||
Controls who can join the room without going through the lobby.
|
||||
|
||||
- `public`: Anyone with the room link can join directly, no authentication required.
|
||||
- `trusted`: Authenticated users join directly. Unauthenticated users wait in the lobby for approval.
|
||||
- `restricted`: Only participants explicitly trusted by the owner bypass the lobby. Everyone else waits for approval regardless of authentication.
|
||||
example: "trusted"
|
||||
description: Empty object - all room properties are auto-generated
|
||||
properties: {}
|
||||
|
||||
Room:
|
||||
type: object
|
||||
@@ -417,7 +362,10 @@ components:
|
||||
description: URL-friendly room identifier (auto-generated)
|
||||
example: "aze-eere-zer"
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Room access level (always 'trusted' for API-created rooms)
|
||||
example: "trusted"
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
@@ -445,8 +393,6 @@ components:
|
||||
type: string
|
||||
description: Default country code
|
||||
example: "US"
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
OAuthError:
|
||||
type: object
|
||||
|
||||
@@ -48,7 +48,7 @@ paths:
|
||||
summary: List rooms
|
||||
description: |
|
||||
Returns a list of rooms accessible to the authenticated user.
|
||||
Only rooms where the user has access will be returned.
|
||||
Only rooms where the delegated user has access will be returned.
|
||||
operationId: listRooms
|
||||
security:
|
||||
- BearerAuth: [rooms:list]
|
||||
@@ -108,7 +108,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: { }
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -121,9 +120,13 @@ paths:
|
||||
summary: Create a room
|
||||
description: |
|
||||
Creates a new room with secure defaults for external API usage.
|
||||
|
||||
|
||||
**Restrictions:**
|
||||
- Rooms are always created with `trusted` access (no public rooms via API)
|
||||
- Room access_level can be updated from the webapp interface.
|
||||
|
||||
**Defaults:**
|
||||
- user is set as owner
|
||||
- Delegated user is set as owner
|
||||
- Room slug auto-generated for uniqueness
|
||||
- Telephony PIN auto-generated when enabled
|
||||
- Creation tracked with application client_id for auditing
|
||||
@@ -138,17 +141,8 @@ paths:
|
||||
$ref: '#/components/schemas/RoomCreate'
|
||||
examples:
|
||||
emptyBody:
|
||||
summary: No parameters (use all defaults)
|
||||
value: { }
|
||||
withAccessLevel:
|
||||
summary: Specify access level
|
||||
value:
|
||||
access_level: "trusted"
|
||||
withConfiguration:
|
||||
summary: Provide room configuration
|
||||
value:
|
||||
configuration:
|
||||
everyone_can_mute: true
|
||||
summary: No parameters (default)
|
||||
value: {}
|
||||
responses:
|
||||
'201':
|
||||
description: Room created successfully
|
||||
@@ -198,7 +192,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: { }
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -217,58 +210,65 @@ components:
|
||||
Include in requests as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
TokenRequest:
|
||||
type: object
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
- grant_type
|
||||
- scope
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
description: Application client identifier
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
client_secret:
|
||||
type: string
|
||||
format: password
|
||||
writeOnly: true
|
||||
description: Application secret key
|
||||
example: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
grant_type:
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
description: OAuth2 grant type (must be 'client_credentials')
|
||||
example: "client_credentials"
|
||||
scope:
|
||||
type: string
|
||||
format: email
|
||||
description: |
|
||||
Email address of the user to delegate.
|
||||
The application will act on behalf of this user.
|
||||
Note: This parameter is named 'scope' to align with OAuth2 conventions,
|
||||
but accepts an email address to identify the user. This design allows
|
||||
for future extensibility.
|
||||
example: "user@example.com"
|
||||
|
||||
TokenResponse:
|
||||
type: object
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: JWT access token
|
||||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtZWV0LWFwaSIsImF1ZCI6Im1lZXQtY2xpZW50cyIsImlhdCI6MTcwOTQ5MTIwMCwiZXhwIjoxNzA5NDk0ODAwLCJjbGllbnRfaWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJzY29wZSI6InJvb21zOmxpc3Qgcm9vbXM6cmV0cmlldmUgcm9vbXM6Y3JlYXRlIiwidXNlcl9pZCI6IjdiOGQ5YzQwLTNhMmItNGVkZi04NzFjLTJmM2Q0ZTVmNmE3YiIsImRlbGVnYXRlZCI6dHJ1ZX0.signature"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type (always 'Bearer')
|
||||
example: "Bearer"
|
||||
expires_in:
|
||||
type: integer
|
||||
description: Token lifetime in seconds
|
||||
example: 3600
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of granted permission scopes
|
||||
example: "rooms:list rooms:retrieve rooms:create"
|
||||
|
||||
RoomCreate:
|
||||
type: object
|
||||
description: |
|
||||
Optional fields for room creation. All fields have secure defaults if omitted.
|
||||
properties:
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
RoomConfiguration:
|
||||
type: object
|
||||
description: |
|
||||
Optional room behaviour settings. Unknown fields are rejected.
|
||||
All fields are optional and default to `null` (server-side defaults apply).
|
||||
properties:
|
||||
can_publish_sources:
|
||||
type: array
|
||||
nullable: true
|
||||
description: |
|
||||
Restricts which media tracks participants are allowed to publish.
|
||||
If `null`, all sources are permitted.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- camera
|
||||
- microphone
|
||||
- screen_share
|
||||
- screen_share_audio
|
||||
example: [ "camera", "microphone" ]
|
||||
everyone_can_mute:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: |
|
||||
Whether any participant can mute others, or only the room owner/moderator.
|
||||
If `null`, the server default applies.
|
||||
example: true
|
||||
additionalProperties: false
|
||||
|
||||
RoomAccessLevel:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- trusted
|
||||
- restricted
|
||||
description: |
|
||||
Controls who can join the room without going through the lobby.
|
||||
|
||||
- `public`: Anyone with the room link can join directly, no authentication required.
|
||||
- `trusted`: Authenticated users join directly. Unauthenticated users wait in the lobby for approval.
|
||||
- `restricted`: Only participants explicitly trusted by the owner bypass the lobby. Everyone else waits for approval regardless of authentication.
|
||||
example: "trusted"
|
||||
description: Empty object - all room properties are auto-generated
|
||||
properties: {}
|
||||
|
||||
Room:
|
||||
type: object
|
||||
@@ -285,7 +285,10 @@ components:
|
||||
description: URL-friendly room identifier (auto-generated)
|
||||
example: "aze-eere-zer"
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Room access level (always 'trusted' for API-created rooms)
|
||||
example: "trusted"
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
@@ -313,8 +316,6 @@ components:
|
||||
type: string
|
||||
description: Default country code
|
||||
example: "US"
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
OAuthError:
|
||||
type: object
|
||||
|
||||
@@ -64,8 +64,9 @@ ALLOW_UNREGISTERED_ROOMS=False
|
||||
RECORDING_ENABLE=True
|
||||
RECORDING_STORAGE_EVENT_ENABLE=True
|
||||
RECORDING_STORAGE_EVENT_TOKEN=password
|
||||
SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v1/tasks/
|
||||
SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v2/async-jobs/transcribe/
|
||||
SUMMARY_SERVICE_API_TOKEN=password
|
||||
SUMMARY_SERVICE_WEBHOOK_API_TOKEN=webhook-password
|
||||
RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording
|
||||
|
||||
# Recording encoding (LiveKit Egress advanced options).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
|
||||
<Id>a025f0f6-757a-4790-97f3-99c66c4a5795</Id>
|
||||
<Version>0.0.2.0</Version>
|
||||
<Version>0.0.1.0</Version>
|
||||
<ProviderName>__APP_NAME__</ProviderName>
|
||||
<DefaultLocale>fr-FR</DefaultLocale>
|
||||
<DisplayName DefaultValue="__APP_NAME__"/>
|
||||
@@ -87,9 +87,9 @@
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
<bt:Image size="16" resid="Add.16x16"/>
|
||||
<bt:Image size="32" resid="Add.32x32"/>
|
||||
<bt:Image size="80" resid="Add.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromMail</FunctionName>
|
||||
@@ -126,9 +126,9 @@
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
<bt:Image size="16" resid="Add.16x16"/>
|
||||
<bt:Image size="32" resid="Add.32x32"/>
|
||||
<bt:Image size="80" resid="Add.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
|
||||
@@ -175,34 +175,15 @@
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<!-- Default (French) -->
|
||||
<bt:String id="GroupLabel" DefaultValue="__APP_NAME__"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__">
|
||||
<bt:Override Locale="en-US" Value="Add a __APP_NAME__ link"/>
|
||||
<bt:Override Locale="de-DE" Value="__APP_NAME__-Link hinzufügen"/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
|
||||
<bt:Override Locale="en-US" Value="Open settings"/>
|
||||
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
|
||||
</bt:String>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
|
||||
<bt:Override Locale="en-US" Value="Settings"/>
|
||||
<bt:Override Locale="de-DE" Value="Einstellungen"/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__"/>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres"/>
|
||||
</bt:ShortStrings>
|
||||
<bt:LongStrings>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement.">
|
||||
<bt:Override Locale="de-DE" Value="Generiert einen __APP_NAME__-Besprechungslink und fügt ihn in den Termin ein."/>
|
||||
<bt:Override Locale="en-US" Value="Generates a __APP_NAME__ meeting link and inserts it into the item."/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
|
||||
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
|
||||
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
|
||||
</bt:String>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
|
||||
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
|
||||
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__."/>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement."/>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__."/>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
|
||||
@@ -9,35 +9,33 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "3.49.0",
|
||||
"i18next": "26.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
"core-js": "^3.36.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@types/office-js": "1.0.582",
|
||||
"@types/office-runtime": "1.0.36",
|
||||
"acorn": "8.16.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"eslint-plugin-office-addins": "4.0.6",
|
||||
"file-loader": "6.2.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-inject-attributes-plugin": "1.0.6",
|
||||
"html-webpack-plugin": "5.6.6",
|
||||
"office-addin-cli": "2.0.6",
|
||||
"office-addin-debugging": "6.0.6",
|
||||
"office-addin-dev-certs": "2.0.6",
|
||||
"office-addin-lint": "3.0.6",
|
||||
"office-addin-manifest": "2.1.2",
|
||||
"office-addin-prettier-config": "2.0.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"process": "0.11.10",
|
||||
"source-map-loader": "5.0.0",
|
||||
"webpack": "5.105.4",
|
||||
"webpack-cli": "5.1.4",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@types/office-js": "^1.0.377",
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-inject-attributes-plugin": "^1.0.6",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "5.2.4"
|
||||
}
|
||||
},
|
||||
@@ -2113,7 +2111,9 @@
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -6863,9 +6863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.49.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
|
||||
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
|
||||
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -9363,43 +9363,6 @@
|
||||
"node": ">=10.18"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
|
||||
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com/i18next"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -15250,7 +15213,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -26,35 +26,33 @@
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.49.0",
|
||||
"i18next": "26.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
"core-js": "^3.36.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@types/office-js": "1.0.582",
|
||||
"@types/office-runtime": "1.0.36",
|
||||
"acorn": "8.16.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"eslint-plugin-office-addins": "4.0.6",
|
||||
"file-loader": "6.2.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-inject-attributes-plugin": "1.0.6",
|
||||
"html-webpack-plugin": "5.6.6",
|
||||
"office-addin-cli": "2.0.6",
|
||||
"office-addin-debugging": "6.0.6",
|
||||
"office-addin-dev-certs": "2.0.6",
|
||||
"office-addin-lint": "3.0.6",
|
||||
"office-addin-manifest": "2.1.2",
|
||||
"office-addin-prettier-config": "2.0.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"process": "0.11.10",
|
||||
"source-map-loader": "5.0.0",
|
||||
"webpack": "5.105.4",
|
||||
"webpack-cli": "5.1.4",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@types/office-js": "^1.0.377",
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-inject-attributes-plugin": "^1.0.6",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "5.2.4"
|
||||
},
|
||||
"prettier": "office-addin-prettier-config",
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
/* global Office */
|
||||
const { APP_NAME } = require("../common/index");
|
||||
const { createRoom, initSession } = require("../common/api");
|
||||
const { startPolling } = require("../common/polling");
|
||||
const { saveSession, loadSession } = require("../common/session");
|
||||
const { openTransitDialog } = require("../common/transitDialog");
|
||||
const { buildMeetingMessage } = require("../common/messageBuilder");
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { initI18n, t } = require("../common/i18n");
|
||||
const { isMeetingAlreadyAdded } = require("../common/meetingDetector");
|
||||
|
||||
Office.onReady(async function (info) {
|
||||
|
||||
await initI18n()
|
||||
|
||||
Office.onReady(function (info) {
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
}
|
||||
@@ -28,52 +22,41 @@ function notify(message) {
|
||||
}
|
||||
|
||||
function insertMeetingLink(event, session) {
|
||||
const item = Office.context.mailbox.item;
|
||||
|
||||
isMeetingAlreadyAdded(item)
|
||||
.then((alreadyAdded) => {
|
||||
if (alreadyAdded) {
|
||||
notify(t("meeting.already_added", { app_name: APP_NAME }));
|
||||
event.completed();
|
||||
return;
|
||||
}
|
||||
return _doInsertMeetingLink(event, session);
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(t("meeting.error.details", { message: err.message }));
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
function _doInsertMeetingLink(event, session) {
|
||||
createRoom(session)
|
||||
.then((data) => {
|
||||
const isWeb = Office.context.diagnostics.platform === "OfficeOnline";
|
||||
const { url, text } = buildMeetingMessage(data, isWeb);
|
||||
const { url, message } = buildMeetingMessage(data);
|
||||
const item = Office.context.mailbox.item;
|
||||
const coercionType = isWeb ? Office.CoercionType.Html : Office.CoercionType.Text;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(t("meeting.error.details", { message: setResult.error.message }));
|
||||
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
|
||||
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(`Erreur de lecture : ${getResult.error.message}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
notify(t("meeting.link_inserted"));
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
item.location.setAsync(url, (locationResult) => {
|
||||
if (locationResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(t("meeting.error.details", { message: locationResult.error.message }));
|
||||
} else {
|
||||
notify(t("meeting.link_inserted"));
|
||||
const newBody = getResult.value + message;
|
||||
item.body.setAsync(newBody, { coercionType: Office.CoercionType.Html }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(`Erreur d'insertion : ${setResult.error.message}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
notify("Lien de réunion inséré !");
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
item.location.setAsync(url, (locationResult) => {
|
||||
if (locationResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(`Erreur de localisation : ${locationResult.error.message}`);
|
||||
} else {
|
||||
notify("Lien de réunion inséré !");
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -96,11 +79,11 @@ function connect(event) {
|
||||
});
|
||||
},
|
||||
onTimeout: () => {
|
||||
notify(t("meeting.error.auth"));
|
||||
notify("Connexion expirée, veuillez réessayer.");
|
||||
event.completed();
|
||||
},
|
||||
onError: (err) => {
|
||||
notify(t("meeting.error.retry"));
|
||||
notify("Une erreur est survenue, veuillez ré-essayer");
|
||||
event.completed();
|
||||
},
|
||||
});
|
||||
@@ -116,7 +99,7 @@ function connect(event) {
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(t("meeting.error.details", { message: err.message }));
|
||||
notify(`Erreur : ${err.message}`);
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
const { APP_NAME } = require("../common");
|
||||
|
||||
const i18nextModule = require("i18next");
|
||||
const i18next = i18nextModule.default || i18nextModule;
|
||||
|
||||
const fr = require("../locales/fr/translation.json");
|
||||
const en = require("../locales/en/translation.json");
|
||||
const de = require("../locales/de/translation.json");
|
||||
|
||||
async function initI18n() {
|
||||
const lng = typeof Office !== "undefined" ? Office.context.displayLanguage : navigator.language;
|
||||
|
||||
await i18next.init({
|
||||
lng,
|
||||
fallbackLng: "fr",
|
||||
interpolation: { escapeValue: false },
|
||||
resources: {
|
||||
fr: { translation: fr },
|
||||
en: { translation: en },
|
||||
de: { translation: de },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function t(key, vars) {
|
||||
return i18next.t(key, vars);
|
||||
}
|
||||
|
||||
function translateUI() {
|
||||
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
||||
const key = el.getAttribute("data-i18n");
|
||||
el.textContent = t(key, { app_name: APP_NAME });
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-attr]").forEach((el) => {
|
||||
const pairs = el.getAttribute("data-i18n-attr").split(",");
|
||||
pairs.forEach((pair) => {
|
||||
const [attr, key] = pair.split(":");
|
||||
el.setAttribute(attr, t(key, { app_name: APP_NAME }));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-aria]").forEach((el) => {
|
||||
el.setAttribute("aria-label", t(el.getAttribute("data-i18n-aria")));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initI18n, t, translateUI };
|
||||
@@ -1,11 +1,7 @@
|
||||
const BASE_URL = window.__APP_CONFIG__?.BASE_URL || "https://meet.127.0.0.1.nip.io";
|
||||
const APP_NAME = window.__APP_CONFIG__?.APP_NAME || "LaSuite Meet";
|
||||
const ENABLE_SOURCE_TRACKING = window.__APP_CONFIG__?.ENABLE_SOURCE_TRACKING === "true";
|
||||
const FEEDBACK_FORM = window.__APP_CONFIG__?.FEEDBACK_FORM || null;
|
||||
|
||||
module.exports = {
|
||||
BASE_URL,
|
||||
APP_NAME,
|
||||
ENABLE_SOURCE_TRACKING,
|
||||
FEEDBACK_FORM
|
||||
};
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
const { BASE_URL } = require("./index");
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to true if a meeting link is already present
|
||||
*/
|
||||
function isMeetingAlreadyAdded(item) {
|
||||
return Promise.all([_checkBody(item), _checkLocation(item)]).then(
|
||||
([inBody, inLocation]) => inBody || inLocation
|
||||
);
|
||||
}
|
||||
|
||||
function _checkBody(item) {
|
||||
return new Promise((resolve) => {
|
||||
item.body.getAsync(Office.CoercionType.Text, (result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
resolve(_containsMeetingUrl(result.value));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _checkLocation(item) {
|
||||
// Location only exists on appointments
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
item.location.getAsync((result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
resolve(_containsMeetingUrl(result.value));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _containsMeetingUrl(text) {
|
||||
if (!text) return false;
|
||||
return text.includes(BASE_URL);
|
||||
}
|
||||
|
||||
function removeMeetingLink(item) {
|
||||
return Promise.all([_removeFromBody(item), _removeFromLocation(item)]);
|
||||
}
|
||||
|
||||
function _removeFromBody(item) {
|
||||
return new Promise((resolve) => {
|
||||
item.body.getAsync(Office.CoercionType.Html, (result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const cleaned = _cleanBody(result.value || "");
|
||||
if (cleaned === null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
item.body.setAsync(cleaned, { coercionType: Office.CoercionType.Html }, () => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _removeFromLocation(item) {
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
item.location.getAsync((result) => {
|
||||
if (
|
||||
result.status === Office.AsyncResultStatus.Succeeded &&
|
||||
_containsMeetingUrl(result.value)
|
||||
) {
|
||||
item.location.setAsync("", () => resolve());
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const SEPARATOR = /─{10,}/;
|
||||
|
||||
/**
|
||||
* Returns cleaned HTML, or null if no meeting block was found.
|
||||
*/
|
||||
function _cleanBody(html) {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
|
||||
const hits = [];
|
||||
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
||||
while (walker.nextNode()) {
|
||||
if (SEPARATOR.test(walker.currentNode.nodeValue)) hits.push(walker.currentNode);
|
||||
}
|
||||
if (hits.length < 2) return null;
|
||||
|
||||
const range = doc.createRange();
|
||||
range.setStartBefore(hits[0]);
|
||||
range.setEndAfter(hits[hits.length - 1]);
|
||||
range.deleteContents();
|
||||
|
||||
return doc.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
module.exports = { isMeetingAlreadyAdded, removeMeetingLink };
|
||||
@@ -1,5 +1,4 @@
|
||||
const { APP_NAME, ENABLE_SOURCE_TRACKING } = require("./index");
|
||||
const { t } = require("./i18n");
|
||||
const { APP_NAME } = require("./index");
|
||||
|
||||
function _formatPin(pin) {
|
||||
if (!pin) return "";
|
||||
@@ -21,59 +20,33 @@ function _formatPhone(phone) {
|
||||
return clean;
|
||||
}
|
||||
|
||||
function _appendTrackingParams(url) {
|
||||
if (!ENABLE_SOURCE_TRACKING) return url;
|
||||
const u = new URL(url);
|
||||
u.searchParams.set("from", "outlook-addin");
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
|
||||
// todo - escape html / link
|
||||
function buildMeetingMessage(data, isWeb) {
|
||||
function buildMeetingMessage(data) {
|
||||
if (!data?.url) {
|
||||
throw new Error("buildMeetingMessage: missing url in data");
|
||||
}
|
||||
|
||||
const url = _appendTrackingParams(data.url);
|
||||
const url = data.url;
|
||||
const phone = _formatPhone(data.telephony?.phone_number);
|
||||
const pin = _formatPin(data.telephony?.pin_code);
|
||||
|
||||
let textLines = "";
|
||||
let phoneLines = [];
|
||||
const telephonyBlock =
|
||||
phone && pin
|
||||
? `
|
||||
|
||||
const join = t("meeting_message.join", { app_name: APP_NAME });
|
||||
const phoneOnly = t("meeting_message.phone_only");
|
||||
const phoneFr = t("meeting_message.phone_fr", { phone });
|
||||
const pinCode = t("meeting_message.pin_code", { pin });
|
||||
Ou appelez (audio uniquement)
|
||||
(FR) ${phone}
|
||||
Code : ${pin}`
|
||||
: "";
|
||||
|
||||
if (isWeb) {
|
||||
phoneLines = phone && pin ? [`<br><br>${phoneOnly}`, `<br>${phoneFr}`, `<br>${pinCode}`] : [];
|
||||
const message = `<pre style="font-family:inherit; font-size:inherit; border:none; background:none; margin:16px 0;">
|
||||
────────────────────────────────────────
|
||||
Rejoindre la réunion ${APP_NAME}
|
||||
|
||||
textLines = [
|
||||
"<br><br>────────────────────────────────────────",
|
||||
`<br>${join}`,
|
||||
`<br><br><a href="${url}" target="_blank">${url}</a>`,
|
||||
...phoneLines,
|
||||
"<br>────────────────────────────────────────<br>",
|
||||
];
|
||||
<a href="${url}">${url}</a>${telephonyBlock}
|
||||
────────────────────────────────────────</pre>`;
|
||||
|
||||
} else {
|
||||
|
||||
phoneLines = phone && pin ? [`\n\n${phoneOnly}`, `\n${phoneFr}`, `\n${pinCode}`] : [];
|
||||
|
||||
textLines = [
|
||||
"\n\n────────────────────────────────────────",
|
||||
`\n${join}`,
|
||||
`\n\n${url}`,
|
||||
...phoneLines,
|
||||
"\n────────────────────────────────────────\n",
|
||||
];
|
||||
}
|
||||
|
||||
const text = textLines.join("");
|
||||
|
||||
return { url, text };
|
||||
return { url, message };
|
||||
}
|
||||
|
||||
module.exports = { buildMeetingMessage };
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"app": {
|
||||
"sideload": "Laden Sie das Add-In.",
|
||||
"loading": "Wird geladen..."
|
||||
},
|
||||
"unauth": {
|
||||
"intro": "Fügen Sie Ihren Outlook-Terminen ganz einfach einen {{app_name}}-Besprechungslink hinzu.",
|
||||
"proconnect_btn": "Aanmelden met ProConnect",
|
||||
"proconnect_link": "Wat is ProConnect?",
|
||||
"proconnect_link_title": "Wat is ProConnect? - nieuw venster"
|
||||
},
|
||||
"success": {
|
||||
"close_window": "Falls sich dieses Fenster nicht automatisch schließt, schließen Sie es bitte manuell."
|
||||
},
|
||||
"auth": {
|
||||
"disconnect": "Abmelden"
|
||||
},
|
||||
"meeting": {
|
||||
"already_added": "Es wurde bereits ein {{app_name}}-Meeting hinzugefügt.",
|
||||
"link_inserted": "Besprechungslink erfolgreich eingefügt",
|
||||
"generating": "Wird erstellt...",
|
||||
"add_meeting": "{{app_name}}-Besprechung hinzufügen",
|
||||
"remove_meeting": "{{app_name}}-Besprechung entfernen",
|
||||
"removing": "Wird entfernt...",
|
||||
"error": {
|
||||
"auth": "Ihre Sitzung ist abgelaufen. Bitte versuchen Sie es erneut.",
|
||||
"retry": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"details": "Fehler: {{message}}"
|
||||
}
|
||||
},
|
||||
"meeting_message": {
|
||||
"join": "An der {{app_name}}-Besprechung teilnehmen",
|
||||
"phone_only": "Oder per Telefon teilnehmen (nur Audio)",
|
||||
"phone_fr": "(FR) {{phone}}",
|
||||
"pin_code": "Code {{pin}}"
|
||||
},
|
||||
"footer": {
|
||||
"feedback": "Teilen Sie uns Ihr Feedback mit"
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"app": {
|
||||
"sideload": "Please load the add-in.",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"unauth": {
|
||||
"intro": "Easily add a {{app_name}} meeting link to your Outlook events.",
|
||||
"proconnect_btn": "Sign in with ProConnect",
|
||||
"proconnect_link": "What is ProConnect?",
|
||||
"proconnect_link_title": "What is ProConnect? - new window"
|
||||
},
|
||||
"success": {
|
||||
"close_window": "If this window does not close automatically, please close it manually."
|
||||
},
|
||||
"auth": {
|
||||
"disconnect": "Sign out"
|
||||
},
|
||||
"meeting": {
|
||||
"already_added": "A {{app_name}} meeting has already been added.",
|
||||
"link_inserted": "Meeting link inserted successfully",
|
||||
"generating": "Generating...",
|
||||
"add_meeting": "Add a {{app_name}} meeting",
|
||||
"remove_meeting": "Remove the {{app_name}} meeting",
|
||||
"removing": "Removing...",
|
||||
"error": {
|
||||
"auth": "Your session has expired. Please try again.",
|
||||
"retry": "An error occurred. Please try again.",
|
||||
"details": "Error: {{message}}"
|
||||
}
|
||||
},
|
||||
"meeting_message": {
|
||||
"join": "Join the {{app_name}} meeting",
|
||||
"phone_only": "Or call in (audio only)",
|
||||
"phone_fr": "(FR) {{phone}}",
|
||||
"pin_code": "Code {{pin}}"
|
||||
},
|
||||
"footer": {
|
||||
"feedback": "Share your feedback"
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"app": {
|
||||
"sideload": "Veuillez charger le complément.",
|
||||
"loading": "Chargement..."
|
||||
},
|
||||
"unauth": {
|
||||
"intro": "Ajoutez facilement un lien de réunion {{app_name}} à vos événements Outlook.",
|
||||
"proconnect_btn": "S'identifier avec ProConnect",
|
||||
"proconnect_link": "Qu'est-ce que ProConnect ?",
|
||||
"proconnect_link_title": "Qu'est-ce que ProConnect ? - nouvelle fenêtre"
|
||||
},
|
||||
"success": {
|
||||
"close_window": "Si cette fenêtre ne se ferme pas toute seule, veuillez la fermer manuellement."
|
||||
},
|
||||
"auth": {
|
||||
"disconnect": "Se déconnecter"
|
||||
},
|
||||
"meeting": {
|
||||
"already_added": "Une réunion {{app_name}} a déjà été ajoutée.",
|
||||
"link_inserted": "Lien de réunion inséré avec succès",
|
||||
"generating": "Génération...",
|
||||
"add_meeting": "Ajouter une réunion {{app_name}}",
|
||||
"remove_meeting": "Supprimer la réunion {{app_name}}",
|
||||
"removing": "Suppression en cours...",
|
||||
"error": {
|
||||
"auth": "Connexion expirée, veuillez réessayer.",
|
||||
"retry": "Une erreur est survenue, veuillez ré-essayer",
|
||||
"details": "Erreur : {{message}}"
|
||||
}
|
||||
},
|
||||
"meeting_message": {
|
||||
"join": "Rejoindre la réunion {{app_name}}",
|
||||
"phone_only": "Ou appelez (audio uniquement)",
|
||||
"phone_fr": "(FR) {{phone}}",
|
||||
"pin_code": "Code {{pin}}"
|
||||
},
|
||||
"footer": {
|
||||
"feedback": "Partagez-nous vos retours"
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,10 @@
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
<div class="spinner-container"
|
||||
role="progressbar"
|
||||
data-i18n-aria="app.loading"
|
||||
aria-label="Chargement..."
|
||||
>
|
||||
<svg class="spinner-svg"
|
||||
viewBox="0 0 28 28"
|
||||
@@ -40,6 +40,5 @@
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<p id="close-msg" style="display: none; text-align: center; font-size: 13px; color: #666; margin-top: 16px;" data-i18n="success.close_window"></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,35 +1,20 @@
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { exchangeSession } = require("../common/api");
|
||||
const { consume } = require("../common/transitToken");
|
||||
const { initI18n, translateUI } = require("../common/i18n");
|
||||
|
||||
(async () => {
|
||||
await initI18n();
|
||||
applyAppName();
|
||||
|
||||
applyAppName();
|
||||
translateUI();
|
||||
const transitToken = consume();
|
||||
|
||||
const transitToken = consume();
|
||||
|
||||
if (!transitToken) {
|
||||
console.error("Transit token not found in sessionStorage");
|
||||
window.close();
|
||||
} else {
|
||||
exchangeSession(transitToken)
|
||||
.then(() => {
|
||||
document.querySelector(".spinner-container").style.display = "none";
|
||||
document.querySelector("#close-msg").style.display = "block";
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`Error occured: ${e}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// NOTE: doesn't work with the desktop client — the browser considers
|
||||
// this window wasn't opened by this script (it was opened externally),
|
||||
// so it blocks window.close() for security reasons. The "#close-msg"
|
||||
// shown above is the fallback for that case.g
|
||||
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
})();
|
||||
if (!transitToken) {
|
||||
console.error("Transit token not found in sessionStorage");
|
||||
window.close();
|
||||
} else {
|
||||
exchangeSession(transitToken)
|
||||
.catch((e) => {
|
||||
console.error(`Error occured: ${e}`);
|
||||
})
|
||||
.finally(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,39 +115,15 @@ button {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* ── Danger button (remove meeting) ── */
|
||||
|
||||
#btn-remove {
|
||||
background-color: #CA3632; /* error.400 */
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#btn-remove:hover {
|
||||
background-color: #EE6A66; /* error.600 */
|
||||
}
|
||||
|
||||
#btn-remove:active {
|
||||
background-color: #F28D8A; /* error.700 */
|
||||
color: #F6AFAD; /* error.200 */
|
||||
}
|
||||
|
||||
#btn-remove:disabled {
|
||||
background-color: #F6AFAD; /* error.800 */
|
||||
color: #FAD2D1; /* error.900 */
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Version ── */
|
||||
|
||||
#version-tag {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #6b7280;
|
||||
@@ -155,29 +131,9 @@ button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#feedback-link {
|
||||
font-size: 11px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #6b7280;
|
||||
text-decoration: underline;
|
||||
pointer-events: all; /* override parent's pointer-events: none */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#feedback-link:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
#footer-right {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background: #EEF1F4;
|
||||
color: #2845C1;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -10,56 +10,47 @@
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
|
||||
<div id="app-body">
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="view-loading">
|
||||
<p class="intro-text" data-i18n="app.loading"></p>
|
||||
<p class="intro-text">Chargement...</p>
|
||||
</div>
|
||||
|
||||
<!-- Unauthenticated -->
|
||||
<div id="view-unauth" style="display:none;">
|
||||
<p class="intro-text">
|
||||
<span data-i18n="unauth.intro"></span>
|
||||
<span>Ajoutez facilement un lien de réunion <span data-app-name></span> à vos événements Outlook.</span>
|
||||
</p>
|
||||
<hr class="divider" />
|
||||
<button class="proconnect-button" id="btn-connect">
|
||||
<span class="proconnect-sr-only" data-i18n="unauth.proconnect_btn"></span>
|
||||
<span class="proconnect-sr-only">S'identifier avec ProConnect</span>
|
||||
</button>
|
||||
<p>
|
||||
<a href="https://www.proconnect.gouv.fr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-i18n-attr="title:unauth.proconnect_link_title"
|
||||
data-i18n="unauth.proconnect_link"
|
||||
></a>
|
||||
<a
|
||||
href="https://www.proconnect.gouv.fr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Qu’est-ce que ProConnect ? - nouvelle fenêtre"
|
||||
>
|
||||
Qu’est-ce que ProConnect ?
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Authenticated -->
|
||||
<div id="view-auth" style="display:none;">
|
||||
<div id="btn-container">
|
||||
<!-- shown when no meeting is present -->
|
||||
<button id="btn-generate" data-i18n="meeting.add_meeting"></button>
|
||||
<!-- shown when a meeting is already present -->
|
||||
<button id="btn-remove" style="display:none;" data-i18n="meeting.remove_meeting"></button>
|
||||
<button id="btn-disconnect" data-i18n="auth.disconnect"></button>
|
||||
<button id="btn-generate">Ajouter une réunion <span data-app-name></span></button>
|
||||
<button id="btn-disconnect">Se déconnecter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer id="version-tag">
|
||||
<a id="feedback-link"
|
||||
style="display:none;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-i18n="footer.feedback"
|
||||
></a>
|
||||
<div id="footer-right">
|
||||
<span class="version-badge">beta</span>
|
||||
<span class="version-number">0.0.2</span>
|
||||
</div>
|
||||
<span class="version-badge">alpha</span>
|
||||
<span class="version-number">0.0.1</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,77 +1,22 @@
|
||||
/* global Office */
|
||||
const { APP_NAME, FEEDBACK_FORM } = require("../common");
|
||||
const { APP_NAME } = require("../common");
|
||||
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { initSession, createRoom } = require("../common/api");
|
||||
const { startPolling } = require("../common/polling");
|
||||
const { openTransitDialog } = require("../common/transitDialog");
|
||||
const { loadSession, saveSession, clearSession } = require("../common/session");
|
||||
|
||||
const { buildMeetingMessage } = require("../common/messageBuilder");
|
||||
const { initI18n, t, translateUI } = require("../common/i18n");
|
||||
const { isMeetingAlreadyAdded, removeMeetingLink } = require("../common/meetingDetector");
|
||||
|
||||
// ── Views ────────────────────────────────────────────────────
|
||||
|
||||
// todo - support loading view while polling
|
||||
// todo - support error view
|
||||
function showView(name) {
|
||||
document.getElementById("view-loading").style.display = "none";
|
||||
document.getElementById("view-unauth").style.display = "none";
|
||||
document.getElementById("view-auth").style.display = "none";
|
||||
document.getElementById(`view-${name}`).style.display = "block";
|
||||
|
||||
if (name === "auth") {
|
||||
_refreshMeetingButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Button state ─────────────────────────────────────────────
|
||||
|
||||
function _showAddButton() {
|
||||
document.getElementById("btn-generate").style.display = "block";
|
||||
document.getElementById("btn-remove").style.display = "none";
|
||||
}
|
||||
|
||||
function _showRemoveButton() {
|
||||
document.getElementById("btn-generate").style.display = "none";
|
||||
document.getElementById("btn-remove").style.display = "block";
|
||||
}
|
||||
|
||||
function _setButtonLoading() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = true;
|
||||
btn.textContent = t("meeting.generating");
|
||||
}
|
||||
|
||||
function _setButtonIdle() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = false;
|
||||
btn.textContent = t("meeting.add_meeting", { app_name: APP_NAME });
|
||||
}
|
||||
|
||||
function _setRemoveLoading() {
|
||||
const btn = document.getElementById("btn-remove");
|
||||
btn.disabled = true;
|
||||
btn.textContent = t("meeting.removing");
|
||||
}
|
||||
|
||||
function _setRemoveIdle() {
|
||||
const btn = document.getElementById("btn-remove");
|
||||
btn.disabled = false;
|
||||
btn.textContent = t("meeting.remove_meeting", { app_name: APP_NAME });
|
||||
}
|
||||
|
||||
function _refreshMeetingButtonState() {
|
||||
const item = Office.context.mailbox.item;
|
||||
if (!item) return;
|
||||
isMeetingAlreadyAdded(item).then((alreadyAdded) => {
|
||||
if (alreadyAdded) {
|
||||
_showRemoveButton();
|
||||
} else {
|
||||
_showAddButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────
|
||||
|
||||
function connect() {
|
||||
initSession()
|
||||
.then((data) => {
|
||||
@@ -102,11 +47,22 @@ function disconnect() {
|
||||
clearSession().finally(() => showView("unauth"));
|
||||
}
|
||||
|
||||
// ── Meeting ──────────────────────────────────────────────────
|
||||
function _setButtonLoading() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Génération...";
|
||||
}
|
||||
|
||||
function _setButtonIdle() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = false;
|
||||
btn.textContent = `Ajouter une réunion ${APP_NAME}`;
|
||||
}
|
||||
|
||||
function generateMeetingLink() {
|
||||
const session = loadSession();
|
||||
if (!session?.access_token) {
|
||||
console.error("Session introuvable. Veuillez vous reconnecter.");
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
@@ -115,28 +71,36 @@ function generateMeetingLink() {
|
||||
|
||||
createRoom(session)
|
||||
.then((data) => {
|
||||
const isWeb = Office.context.diagnostics.platform === "OfficeOnline";
|
||||
const { url, text } = buildMeetingMessage(data, isWeb);
|
||||
const { url, message } = buildMeetingMessage(data);
|
||||
const item = Office.context.mailbox.item;
|
||||
const coercionType = isWeb ? Office.CoercionType.Html : Office.CoercionType.Text;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(setResult.error);
|
||||
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
|
||||
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(getResult.error);
|
||||
return;
|
||||
}
|
||||
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
|
||||
item.location.setAsync(url, () => resolve());
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
|
||||
item.body.setAsync(
|
||||
getResult.value + message,
|
||||
{ coercionType: Office.CoercionType.Html },
|
||||
(setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(setResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── If calendar event, also set location ──────────────
|
||||
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
|
||||
item.location.setAsync(url, () => resolve());
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
_showRemoveButton();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
@@ -145,41 +109,7 @@ function generateMeetingLink() {
|
||||
});
|
||||
}
|
||||
|
||||
function removeMeetingLinkFromItem() {
|
||||
const session = loadSession();
|
||||
if (!session?.access_token) {
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
|
||||
_setRemoveLoading();
|
||||
|
||||
const item = Office.context.mailbox.item;
|
||||
|
||||
removeMeetingLink(item)
|
||||
.then(() => {
|
||||
_showAddButton();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
_setRemoveIdle();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────
|
||||
|
||||
Office.onReady(async (info) => {
|
||||
await initI18n();
|
||||
translateUI();
|
||||
|
||||
if (FEEDBACK_FORM) {
|
||||
const link = document.getElementById("feedback-link");
|
||||
link.href = FEEDBACK_FORM;
|
||||
link.style.display = "inline";
|
||||
}
|
||||
|
||||
Office.onReady((info) => {
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
document.getElementById("sideload-msg").style.display = "none";
|
||||
@@ -187,11 +117,10 @@ Office.onReady(async (info) => {
|
||||
document.getElementById("btn-connect").onclick = connect;
|
||||
document.getElementById("btn-disconnect").onclick = disconnect;
|
||||
document.getElementById("btn-generate").onclick = generateMeetingLink;
|
||||
document.getElementById("btn-remove").onclick = removeMeetingLinkFromItem;
|
||||
|
||||
const session = loadSession();
|
||||
if (session?.state === "authenticated" && session?.access_token) {
|
||||
showView("auth"); // this already calls _refreshMeetingButtonState internally
|
||||
showView("auth");
|
||||
} else {
|
||||
showView("unauth");
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
<div
|
||||
class="spinner-container"
|
||||
role="progressbar"
|
||||
data-i18n-aria="app.loading"
|
||||
aria-label="Chargement..."
|
||||
>
|
||||
<svg
|
||||
class="spinner-svg"
|
||||
|
||||
@@ -2,7 +2,6 @@ const { applyAppName } = require("../common/helpers");
|
||||
const { URLS } = require("../common/urls");
|
||||
const { save } = require("../common/transitToken");
|
||||
const { DIALOG_SIGNALS } = require("../common/transitDialog");
|
||||
const { initI18n, translateUI } = require("../common/i18n");
|
||||
|
||||
// Initiate the authentication flow, then return to the success page
|
||||
function getAuthenticateUrl() {
|
||||
@@ -11,11 +10,7 @@ function getAuthenticateUrl() {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
Office.onReady(async function (info) {
|
||||
|
||||
await initI18n();
|
||||
translateUI();
|
||||
|
||||
Office.onReady(function (info) {
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
}
|
||||
|
||||
@@ -19,14 +19,13 @@ from livekit.agents import (
|
||||
JobContext,
|
||||
JobProcess,
|
||||
JobRequest,
|
||||
RoomInputOptions,
|
||||
RoomIO,
|
||||
RoomOutputOptions,
|
||||
WorkerPermissions,
|
||||
cli,
|
||||
utils,
|
||||
)
|
||||
from livekit.agents import (
|
||||
room_io as lk_room_io,
|
||||
)
|
||||
from livekit.plugins import silero
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
@@ -303,11 +302,13 @@ class MetadataCollector:
|
||||
agent_session=session,
|
||||
room=self.ctx.room,
|
||||
participant=participant,
|
||||
options=lk_room_io.RoomOptions(
|
||||
audio_input=lk_room_io.AudioInputOptions(),
|
||||
text_input=False,
|
||||
audio_output=False,
|
||||
text_output=False,
|
||||
input_options=RoomInputOptions(
|
||||
audio_enabled=True,
|
||||
text_enabled=False,
|
||||
),
|
||||
output_options=RoomOutputOptions(
|
||||
audio_enabled=False,
|
||||
transcription_enabled=False,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -323,6 +324,7 @@ class MetadataCollector:
|
||||
async def _close_session(self, session: AgentSession) -> None:
|
||||
"""Close and cleanup VAD monitoring session."""
|
||||
try:
|
||||
await session.drain()
|
||||
await session.aclose()
|
||||
except Exception:
|
||||
logger.exception("Error closing session")
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.20.0"
|
||||
version = "1.16.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.5.13",
|
||||
"livekit-plugins-deepgram==1.5.13",
|
||||
"livekit-plugins-silero==1.5.13",
|
||||
"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.2",
|
||||
"protobuf==6.33.6",
|
||||
"minio==7.2.20"
|
||||
"protobuf==6.33.5",
|
||||
"minio==7.2.15"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff==0.15.14",
|
||||
"ruff==0.15.6",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
|
||||
[[package]]
|
||||
name = "agents"
|
||||
version = "1.20.0"
|
||||
version = "1.16.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "livekit-agents" },
|
||||
@@ -28,14 +28,14 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "livekit-agents", specifier = "==1.5.13" },
|
||||
{ name = "livekit-plugins-deepgram", specifier = "==1.5.13" },
|
||||
{ name = "livekit-agents", specifier = "==1.4.5" },
|
||||
{ name = "livekit-plugins-deepgram", specifier = "==1.4.5" },
|
||||
{ name = "livekit-plugins-kyutai-lasuite", specifier = "==0.0.6" },
|
||||
{ name = "livekit-plugins-silero", specifier = "==1.5.13" },
|
||||
{ name = "minio", specifier = "==7.2.20" },
|
||||
{ name = "protobuf", specifier = "==6.33.6" },
|
||||
{ name = "livekit-plugins-silero", specifier = "==1.4.5" },
|
||||
{ name = "minio", specifier = "==7.2.15" },
|
||||
{ name = "protobuf", specifier = "==6.33.5" },
|
||||
{ name = "python-dotenv", specifier = "==1.2.2" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.14" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.6" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
@@ -638,11 +638,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.18"
|
||||
version = "3.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -731,7 +731,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "livekit"
|
||||
version = "1.1.8"
|
||||
version = "1.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -739,18 +739,18 @@ dependencies = [
|
||||
{ name = "protobuf" },
|
||||
{ name = "types-protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/d7/1fde055300c1f8338feec88bac55270ea5c20cac66ed3655cc60fb14f2b6/livekit-1.1.8.tar.gz", hash = "sha256:862100f479dc06b10cd1442d0a687126ac71930d3e003925449bcb8c2531bd53", size = 335048, upload-time = "2026-05-13T17:30:36.446Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/9c/0a9b9e1f88226df372b9ccc58868ce9a1eb97c8bf5257463a643dda2a6da/livekit-1.1.2.tar.gz", hash = "sha256:ef826e94dd039767fcabc2f0d810b1b2335c9cf249f52320b6ab018b06d5ccd7", size = 320006, upload-time = "2026-02-17T01:18:46.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/7c/1f609b57b46988a7696d49a6d3d8c19056950b7dc88552323fbd39403494/livekit-1.1.8-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:e65732bd4e0b3625d8c32676cede90be45e06ab443b5c18127df6e89637d5269", size = 10027840, upload-time = "2026-05-13T17:30:24.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/1d/2af5a4fc85fe4a49e32432cbf37b1869cdb6e428b4858d0821703742cf01/livekit-1.1.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b025e5881ae63674aa0ef193ce42b4cd98258fe098707142888e6a79f448f47d", size = 8860811, upload-time = "2026-05-13T17:30:27.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/80/919c2f291083f253cf37aafcbd8e727dac3e29a0e60ec63b5249b93ad025/livekit-1.1.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:080a3799bcf7075328f2921868646296a18e4ab1c613df6b313d723f99b5557b", size = 9852340, upload-time = "2026-05-13T17:30:29.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/11/44bebbb16d79c9026200f83c8d9695ea6976a53f1430b0ea3b15a41c4bd4/livekit-1.1.8-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2746bbe611869994fbc42e3f8387f3a3d4a5f83e3fad8860c076742a4c0d943e", size = 11235966, upload-time = "2026-05-13T17:30:31.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/08/9dca90dd3f9c5147ec0b1168d3410db1cc944d546c1d123343f81880b261/livekit-1.1.8-py3-none-win_amd64.whl", hash = "sha256:3942843f0c0072a3d973f3e45b60b1565833785bbe7890d64f7e5555ff61b9e2", size = 10598792, upload-time = "2026-05-13T17:30:34.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/7b/1b2d448d8976b14794bfba3d003e1451c79afa77e6b9fa1593f19175ef90/livekit-1.1.2-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:78be23f3f6315354aaacee664eb19b793009bc06faa8184ad9c07cffbe8d7f74", size = 9844157, upload-time = "2026-02-17T01:18:34.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/fb/19f7fc4a7df3b1385ba2e32b907c5a502fe01c10e6ee2fb76629c068667d/livekit-1.1.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:656f4d9e4692f3d7ab2e51b0bbf4ec03b356a487b7ff220576dab496e60f99f3", size = 8651703, upload-time = "2026-02-17T01:18:36.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/59/ac6a3987bfd11687f86156627a8c7f6a0047a72877748facbd427e63b157/livekit-1.1.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a20e681d8a4929e27df69f818888ae649700c9d52a262abbec296c84937bc337", size = 14085891, upload-time = "2026-02-17T01:18:38.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/a5/680548ef7bf5034144ae01f1f742a6c49e4428942b3119ac6553207fea9b/livekit-1.1.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:0e4d8105a0c317b513a118b48340b5773239103cb6305768451ef99ac7567823", size = 11448902, upload-time = "2026-02-17T01:18:42.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/05/e484f8fc079c5de7690e58ed48f44e77436b8732c3050da742bdfca51a5c/livekit-1.1.2-py3-none-win_amd64.whl", hash = "sha256:dd4a436fa16de589353bfbabde91068ab64241afd05b04f21fb1f22bfe155dc0", size = 10394562, upload-time = "2026-02-17T01:18:44.589Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "livekit-agents"
|
||||
version = "1.5.13"
|
||||
version = "1.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -782,9 +782,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "watchfiles" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/91/059854851dc0c61b8fc429642243c80a2b31d70f9f23c01ae0b2898d45a5/livekit_agents-1.5.13.tar.gz", hash = "sha256:08c87be20e4bc977550cf9e5a9c5a9941b5cdad498b306a900cadf2a64f26ef9", size = 2506879, upload-time = "2026-05-25T05:58:58.712Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/06/1a23902ffea2742e60e30f957f3a96766be125892a2573ea1754051b3d8d/livekit_agents-1.4.5.tar.gz", hash = "sha256:19fdf45a2c4d02d86b4ad132b73bf3f58eadb75f79819e5015e6fadfb1263852", size = 2416100, upload-time = "2026-03-11T06:45:03.912Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/7f/9eb7191032036c2c1bff7c55cf73ce5350c49cc4048add919e0907284386/livekit_agents-1.5.13-py3-none-any.whl", hash = "sha256:7b365b0da02a0610c708ac5280a914519a29f8b7c02f0ab37f42ff2c617a9d60", size = 2606897, upload-time = "2026-05-25T05:58:55.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/ad/a7613ddd5cfb29e015913ae44b804505d41c1d02d19ffeaf2f7015d23664/livekit_agents-1.4.5-py3-none-any.whl", hash = "sha256:aff0b61dbd24b1a100cccca8e2a0056f414b861ce71ba8e4338bc5f89e149f7a", size = 2499369, upload-time = "2026-03-11T06:45:01.772Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -837,15 +837,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "livekit-plugins-deepgram"
|
||||
version = "1.5.13"
|
||||
version = "1.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "livekit-agents", extra = ["codecs"] },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/17/cb774f11d4290e98151f9756a4d7511926cca715f53b48d976bcc9b7d3fd/livekit_plugins_deepgram-1.5.13.tar.gz", hash = "sha256:4c6a127a064e8b5d38f4f5df8cc278e1b0b834684db7bb08a524e01744ac7623", size = 18335, upload-time = "2026-05-25T05:59:22.068Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/49/778424318c3bd5a1251449048024a46ad8f456a910a4f71c70b1e67eb050/livekit_plugins_deepgram-1.4.5.tar.gz", hash = "sha256:51762665b855360cdea586a59577c30fb17fa991689dd3fe1cb20fd43c089a2b", size = 16877, upload-time = "2026-03-11T06:45:40.838Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/50/2f907b99064673a9a8f22118ed9de4ba93400493ab7bfb8b37a4a2027a21/livekit_plugins_deepgram-1.5.13-py3-none-any.whl", hash = "sha256:2b11dc0c9c3d2aab070291ebe493dcae3a39b279ab4a10d114edbf1eecea0c9e", size = 23064, upload-time = "2026-05-25T05:59:20.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e5/7657aa8f23a5e029748fb7576103da3c2dac4aee9cb1dff0950a09fecad3/livekit_plugins_deepgram-1.4.5-py3-none-any.whl", hash = "sha256:f2bcfc139c4a454e1beafba715ad57296d88f17e5215ec07a6e16491434fe87d", size = 21910, upload-time = "2026-03-11T06:45:39.074Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -864,29 +864,29 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "livekit-plugins-silero"
|
||||
version = "1.5.13"
|
||||
version = "1.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "livekit-agents" },
|
||||
{ name = "numpy" },
|
||||
{ name = "onnxruntime" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/c0/fa8305788ab5993a045806754827064f39f05be9e747b0cb277071ff3c9f/livekit_plugins_silero-1.5.13.tar.gz", hash = "sha256:4cb9eb10b62c3eab274bf1f2928ad7b0483afc1719c2e90587056ffdd8cf0507", size = 1955918, upload-time = "2026-05-25T06:00:59.243Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/58/4cb8704199ab7ed1a065b8a853bddf5a0a84baade412935dd67dfed25659/livekit_plugins_silero-1.4.5.tar.gz", hash = "sha256:d6ffe6b2f57ba4bf8e877a41ba475256402a76bdc01fa4759d12bc26e30a04b8", size = 1955572, upload-time = "2026-03-11T06:46:45.003Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/af/0fdc4b6426f7b0e3ef8dc370d03f53a600b35ea299533abb704ecf052333/livekit_plugins_silero-1.5.13-py3-none-any.whl", hash = "sha256:3d631355b5830285b3b03ecdafd07e57ca1687c0fd1b23f8617865f61d4055ec", size = 3903909, upload-time = "2026-05-25T06:00:57.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/26/5da1adbc201f997b2d778b885c9b01ba7a8b3b36ab615c8e6e3abfd86693/livekit_plugins_silero-1.4.5-py3-none-any.whl", hash = "sha256:788fc5afc636358a841fd6c2ca33edb941045881b94ba5f04aa329ea92f68273", size = 3903690, upload-time = "2026-03-11T06:46:43.273Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "livekit-protocol"
|
||||
version = "1.1.13"
|
||||
version = "1.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
{ name = "types-protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ed/09cf0419967e1f46b5b84447d29dff58473b13308fafa44456d431d53883/livekit_protocol-1.1.13.tar.gz", hash = "sha256:38eff0dc23ff9d6b7d9d10220b74735a0ec784a4a5196ef02c10c6052f8f7f6c", size = 101474, upload-time = "2026-06-02T22:06:40.34Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/28/8a7ee5a4126476a0ce770a482274d51fc81ae0d9f578c07f07d6655acfc4/livekit_protocol-1.1.6.tar.gz", hash = "sha256:43648863440a6ce064f7f8b8b287dda8f5ac82b8da64738b20f42cad51e70f9b", size = 93925, upload-time = "2026-04-20T15:48:43.032Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/66/d16928af7f7641a4edda734c69cdf1af07b6c5f378d72691580ecf4fe6bd/livekit_protocol-1.1.13-py3-none-any.whl", hash = "sha256:8827860835ae576cd66948f5721c5949a285fe72856ebf67af8e42e368a994e7", size = 125127, upload-time = "2026-06-02T22:06:39.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/36/525cd9364036617aa97c597c93d7bddb6be311b86288cd54de77e4892544/livekit_protocol-1.1.6-py3-none-any.whl", hash = "sha256:286ac0bd555b6b75cd8a0c5d417572d7dab524129fcea43080202d170f957710", size = 115012, upload-time = "2026-04-20T15:48:41.312Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -912,7 +912,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "minio"
|
||||
version = "7.2.20"
|
||||
version = "7.2.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "argon2-cffi" },
|
||||
@@ -921,9 +921,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/68/86a1cef80396e6a35a6fc4fafee5d28578c1a137bddd3ca2aa86f9b26a22/minio-7.2.15.tar.gz", hash = "sha256:5247df5d4dca7bfa4c9b20093acd5ad43e82d8710ceb059d79c6eea970f49f79", size = 138040, upload-time = "2025-01-19T08:57:26.626Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6f/3690028e846fe432bfa5ba724a0dc37ec9c914965b7733e19d8ca2c4c48d/minio-7.2.15-py3-none-any.whl", hash = "sha256:c06ef7a43e5d67107067f77b6c07ebdd68733e5aa7eed03076472410ca19d876", size = 95075, upload-time = "2025-01-19T08:57:24.169Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1417,17 +1417,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.6"
|
||||
version = "6.33.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1644,27 +1644,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.14"
|
||||
version = "0.15.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
||||
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/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
||||
{ 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]]
|
||||
@@ -1772,11 +1772,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -3,55 +3,17 @@
|
||||
from django import forms
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.db import transaction
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.recording.event import notification
|
||||
|
||||
from . import models
|
||||
from .tasks.file import process_file_deletion
|
||||
from .utils import generate_download_file_url
|
||||
|
||||
|
||||
def hard_delete_file(file):
|
||||
"""Hard delete a file, soft deleting it first when needed."""
|
||||
if file.deleted_at is None:
|
||||
file.soft_delete()
|
||||
file.hard_delete()
|
||||
transaction.on_commit(lambda: process_file_deletion.delay(file.id))
|
||||
|
||||
|
||||
class FileInlineFormSet(forms.BaseInlineFormSet):
|
||||
"""Inline formset overriding delete behavior for files."""
|
||||
|
||||
def delete_existing(self, obj, commit=True):
|
||||
"""Hard delete files instead of calling model.delete()."""
|
||||
hard_delete_file(obj)
|
||||
|
||||
|
||||
class FileInline(admin.TabularInline):
|
||||
"""Inline class for the File model."""
|
||||
|
||||
model = models.File
|
||||
formset = FileInlineFormSet
|
||||
fk_name = "creator"
|
||||
extra = 0
|
||||
fields = ("id", "title", "type", "upload_state", "created_at")
|
||||
readonly_fields = ("id", "created_at", "upload_state", "type")
|
||||
show_change_link = True
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Hide hard deleted files in the inline."""
|
||||
return super().get_queryset(request).filter(hard_deleted_at__isnull=True)
|
||||
|
||||
|
||||
@admin.register(models.User)
|
||||
class UserAdmin(auth_admin.UserAdmin):
|
||||
"""Admin class for the User model"""
|
||||
|
||||
inlines = (FileInline,)
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
@@ -135,136 +97,6 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||
search_fields = ("id", "sub", "admin_email", "email", "full_name")
|
||||
|
||||
|
||||
@admin.register(models.File)
|
||||
class FileAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the File model."""
|
||||
|
||||
list_display = (
|
||||
"id",
|
||||
"title",
|
||||
"type",
|
||||
"creator",
|
||||
"upload_state",
|
||||
"deleted_at",
|
||||
"hard_deleted_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_filter = (
|
||||
"type",
|
||||
"upload_state",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"hard_deleted_at",
|
||||
)
|
||||
search_fields = (
|
||||
"id",
|
||||
"title",
|
||||
"filename",
|
||||
"mimetype",
|
||||
"description",
|
||||
"creator__email",
|
||||
"creator__admin_email",
|
||||
"creator__full_name",
|
||||
)
|
||||
ordering = ("-created_at",)
|
||||
readonly_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"hard_deleted_at",
|
||||
"description",
|
||||
"malware_detection_info",
|
||||
"is_ready",
|
||||
"preview_url",
|
||||
"extension",
|
||||
"key_base",
|
||||
"file_key",
|
||||
"upload_state",
|
||||
"type",
|
||||
"mimetype",
|
||||
"size",
|
||||
)
|
||||
autocomplete_fields = ("creator",)
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"id",
|
||||
"title",
|
||||
"type",
|
||||
"creator",
|
||||
"filename",
|
||||
"upload_state",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
_("Content"),
|
||||
{
|
||||
"fields": (
|
||||
"mimetype",
|
||||
"size",
|
||||
"description",
|
||||
"malware_detection_info",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
_("Deletion"),
|
||||
{
|
||||
"fields": (
|
||||
"deleted_at",
|
||||
"hard_deleted_at",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
_("Derived info"),
|
||||
{
|
||||
"fields": (
|
||||
"is_ready",
|
||||
"extension",
|
||||
"key_base",
|
||||
"file_key",
|
||||
"preview_url",
|
||||
)
|
||||
},
|
||||
),
|
||||
(_("Timestamps"), {"fields": ("created_at", "updated_at")}),
|
||||
)
|
||||
|
||||
@admin.display(description=_("File preview"))
|
||||
def preview_url(self, obj):
|
||||
"""Return a clickable preview URL for the file."""
|
||||
if not obj.is_ready:
|
||||
return "-"
|
||||
url = generate_download_file_url(obj, expires_in=60 * 60)
|
||||
|
||||
return format_html(
|
||||
'<a href="{}" target="_blank" rel="noopener noreferrer">Open File</a>', url
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Hide hard deleted files in admin listing and lookups."""
|
||||
return super().get_queryset(request).filter(hard_deleted_at__isnull=True)
|
||||
|
||||
def delete_model(self, request, obj):
|
||||
"""Hard delete instead of calling model.delete()."""
|
||||
hard_delete_file(obj)
|
||||
|
||||
def delete_queryset(self, request, queryset):
|
||||
"""Hard delete all selected files."""
|
||||
for file in queryset:
|
||||
hard_delete_file(file)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
|
||||
class ResourceAccessInline(admin.TabularInline):
|
||||
"""Admin class for the room user access model"""
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ from rest_framework import views as drf_views
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core.utils import build_telephony_config
|
||||
|
||||
|
||||
def exception_handler(exc, context):
|
||||
"""Handle Django ValidationError as an accepted exception.
|
||||
@@ -60,7 +58,13 @@ def get_frontend_configuration(request):
|
||||
"allowed_mimetypes"
|
||||
],
|
||||
},
|
||||
"telephony": build_telephony_config(),
|
||||
"telephony": {
|
||||
"enabled": settings.ROOM_TELEPHONY_ENABLED,
|
||||
"phone_number": settings.ROOM_TELEPHONY_PHONE_NUMBER
|
||||
if settings.ROOM_TELEPHONY_ENABLED
|
||||
else None,
|
||||
"default_country": settings.ROOM_TELEPHONY_DEFAULT_COUNTRY,
|
||||
},
|
||||
"subtitle": {"enabled": settings.ROOM_SUBTITLE_ENABLED},
|
||||
"livekit": {
|
||||
"url": settings.LIVEKIT_CONFIGURATION["url"],
|
||||
|
||||
@@ -456,7 +456,7 @@ class ListFileSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_url(self, obj):
|
||||
"""Return the URL of the file."""
|
||||
if not obj.is_ready:
|
||||
if obj.is_pending_upload:
|
||||
return None
|
||||
|
||||
return f"{settings.MEDIA_BASE_URL}{settings.MEDIA_URL}{quote(obj.file_key)}"
|
||||
|
||||
@@ -39,7 +39,10 @@ from core import enums, models, utils
|
||||
from core.api.filters import ListFileFilter
|
||||
from core.enums import MEDIA_STORAGE_URL_PATTERN
|
||||
from core.recording.enums import FileExtension
|
||||
from core.recording.event.authentication import StorageEventAuthentication
|
||||
from core.recording.event.authentication import (
|
||||
RecordingProcessWebhookAuthentication,
|
||||
StorageEventAuthentication,
|
||||
)
|
||||
from core.recording.event.exceptions import (
|
||||
InvalidBucketError,
|
||||
InvalidFilepathError,
|
||||
@@ -995,6 +998,47 @@ class RecordingViewSet(
|
||||
{"message": "Event processed."},
|
||||
)
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=["post"],
|
||||
url_path="external-process-hook",
|
||||
authentication_classes=[RecordingProcessWebhookAuthentication],
|
||||
)
|
||||
def on_external_process_event_received(self, request, pk=None): # pylint: disable=unused-argument
|
||||
"""Handle incoming external process events for recordings."""
|
||||
logger.debug("Processing external process event %s", request.data)
|
||||
data = request.data
|
||||
if not data.get("job_id"):
|
||||
raise drf_exceptions.ValidationError(detail="No job_id provided")
|
||||
|
||||
job_id = data["job_id"]
|
||||
try:
|
||||
recording = models.Recording.objects.get(external_process_id=job_id)
|
||||
except models.Recording.DoesNotExist as e:
|
||||
logger.warning("No recording found for job_id %s: %s", job_id, e)
|
||||
recording = None
|
||||
|
||||
changed = False
|
||||
if recording and data.get("type") == "transcript":
|
||||
if data.get("status") == "success":
|
||||
logger.info("External process received for recording %s", job_id)
|
||||
recording.status = (
|
||||
models.RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL
|
||||
)
|
||||
changed = True
|
||||
if data.get("status") == "failure":
|
||||
recording.status = models.RecordingStatusChoices.EXTERNAL_PROCESS_FAILED
|
||||
changed = True
|
||||
|
||||
if changed and recording:
|
||||
recording.save()
|
||||
else:
|
||||
logger.info("No changes to save for external process id %s", job_id)
|
||||
|
||||
return drf_response.Response(
|
||||
{"message": "Event processed."},
|
||||
)
|
||||
|
||||
def _auth_get_original_url(self, request):
|
||||
"""
|
||||
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
|
||||
@@ -1196,124 +1240,96 @@ class FileViewSet(
|
||||
"""
|
||||
Check the actual uploaded file and mark it as ready.
|
||||
"""
|
||||
# Ensures we go through authorization checks
|
||||
|
||||
file = self.get_object()
|
||||
|
||||
# Try to update the file with the new state. If the file is already in this state
|
||||
# we are in a concurrent request, and we should reject that request
|
||||
updated_rows = models.File.objects.filter(
|
||||
upload_state=models.FileUploadStateChoices.PENDING,
|
||||
pk=kwargs["pk"],
|
||||
).update(upload_state=models.FileUploadStateChoices.ANALYZING)
|
||||
if updated_rows != 1:
|
||||
if not file.is_pending_upload:
|
||||
raise drf_exceptions.ValidationError(
|
||||
{"file": "This action is only available for files in PENDING state."},
|
||||
code="file_upload_state_not_pending",
|
||||
)
|
||||
file.refresh_from_db()
|
||||
|
||||
s3_client = default_storage.connection.meta.client
|
||||
validation_error = None
|
||||
|
||||
try:
|
||||
# We copy the file to its final destination, we will run the checks on that
|
||||
# final file and ignore any updates to the temporary file. (We cannot revoke the policy,
|
||||
# so the temporary file might still be updated after that.)
|
||||
# The temporary folders will need to be cleaned periodically
|
||||
head_response = s3_client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file.file_key
|
||||
)
|
||||
file_size = head_response["ContentLength"]
|
||||
|
||||
if settings.FILE_UPLOAD_APPLY_RESTRICTIONS:
|
||||
config_for_file_type = settings.FILE_UPLOAD_RESTRICTIONS[file.type]
|
||||
if file_size > config_for_file_type["max_size"]:
|
||||
self._complete_file_deletion(file)
|
||||
logger.info(
|
||||
"upload_ended: file size (%s) for file %s higher than the allowed max size",
|
||||
file_size,
|
||||
file.file_key,
|
||||
)
|
||||
raise drf_exceptions.ValidationError(
|
||||
detail="The file size is higher than the allowed max size.",
|
||||
code="file_size_exceeded",
|
||||
)
|
||||
|
||||
# python-magic recommends using at least the first 2048 bytes
|
||||
# to reduce incorrect identification.
|
||||
# This is a tradeoff between pulling in the whole file and the most likely relevant bytes
|
||||
# of the file for mime type identification.
|
||||
if file_size > 2048:
|
||||
range_response = s3_client.get_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=file.file_key,
|
||||
Range="bytes=0-2047",
|
||||
)
|
||||
file_head = range_response["Body"].read()
|
||||
else:
|
||||
file_head = s3_client.get_object(
|
||||
Bucket=default_storage.bucket_name, Key=file.file_key
|
||||
)["Body"].read()
|
||||
|
||||
# Use improved MIME type detection combining magic bytes and file extension
|
||||
logger.info("upload_ended: detecting mimetype for file: %s", file.file_key)
|
||||
mimetype = utils.detect_mimetype(file_head, filename=file.filename)
|
||||
|
||||
if settings.FILE_UPLOAD_APPLY_RESTRICTIONS:
|
||||
config_for_file_type = settings.FILE_UPLOAD_RESTRICTIONS[file.type]
|
||||
allowed_file_mimetypes = config_for_file_type["allowed_mimetypes"]
|
||||
if mimetype not in allowed_file_mimetypes:
|
||||
self._complete_file_deletion(file)
|
||||
logger.warning(
|
||||
"upload_ended: mimetype not allowed %s for file %s",
|
||||
mimetype,
|
||||
file.file_key,
|
||||
)
|
||||
raise drf_exceptions.ValidationError(
|
||||
detail="The file type is not allowed.",
|
||||
code="file_type_not_allowed",
|
||||
)
|
||||
|
||||
file.upload_state = models.FileUploadStateChoices.READY
|
||||
file.mimetype = mimetype
|
||||
file.size = file_size
|
||||
|
||||
file.save(update_fields=["upload_state", "mimetype", "size"])
|
||||
|
||||
if head_response["ContentType"] != mimetype:
|
||||
logger.info(
|
||||
"upload_ended: content type mismatch between object storage and file,"
|
||||
" updating from %s to %s",
|
||||
head_response["ContentType"],
|
||||
mimetype,
|
||||
)
|
||||
s3_client.copy_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=file.file_key,
|
||||
CopySource={
|
||||
"Bucket": default_storage.bucket_name,
|
||||
"Key": file.temporary_file_key,
|
||||
"Key": file.file_key,
|
||||
},
|
||||
ContentType=mimetype,
|
||||
Metadata=head_response["Metadata"],
|
||||
MetadataDirective="REPLACE",
|
||||
)
|
||||
|
||||
head_response = s3_client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file.file_key
|
||||
)
|
||||
file_size = head_response["ContentLength"]
|
||||
# python-magic recommends using at least the first 2048 bytes
|
||||
# to reduce incorrect identification.
|
||||
# This is a tradeoff between pulling in the whole file and
|
||||
# the most likely relevant bytes
|
||||
# of the file for mime type identification.
|
||||
if file_size > 2048:
|
||||
range_response = s3_client.get_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=file.file_key,
|
||||
Range="bytes=0-2047",
|
||||
)
|
||||
file_head = range_response["Body"].read()
|
||||
else:
|
||||
file_head = s3_client.get_object(
|
||||
Bucket=default_storage.bucket_name, Key=file.file_key
|
||||
)["Body"].read()
|
||||
|
||||
logger.info("upload_ended: detecting mimetype for file: %s", file.file_key)
|
||||
mimetype = utils.detect_mimetype(file_head, filename=file.filename)
|
||||
|
||||
if settings.FILE_UPLOAD_APPLY_RESTRICTIONS:
|
||||
config_for_file_type = settings.FILE_UPLOAD_RESTRICTIONS[file.type]
|
||||
if file_size > config_for_file_type["max_size"]:
|
||||
logger.info(
|
||||
"upload_ended: file size (%s) for file %s higher than the allowed max size",
|
||||
file_size,
|
||||
file.file_key,
|
||||
)
|
||||
validation_error = drf_exceptions.ValidationError(
|
||||
detail="The file size is higher than the allowed max size.",
|
||||
code="file_size_exceeded",
|
||||
)
|
||||
else:
|
||||
# Use improved MIME type detection combining magic bytes and file extension
|
||||
allowed_file_mimetypes = config_for_file_type["allowed_mimetypes"]
|
||||
if mimetype not in allowed_file_mimetypes:
|
||||
logger.warning(
|
||||
"upload_ended: mimetype not allowed %s for file %s",
|
||||
mimetype,
|
||||
file.file_key,
|
||||
)
|
||||
validation_error = drf_exceptions.ValidationError(
|
||||
detail="The file type is not allowed.",
|
||||
code="file_type_not_allowed",
|
||||
)
|
||||
|
||||
if validation_error is not None:
|
||||
self._complete_file_deletion(file)
|
||||
else:
|
||||
file.upload_state = models.FileUploadStateChoices.READY
|
||||
file.mimetype = mimetype
|
||||
file.size = file_size
|
||||
file.save(update_fields=["upload_state", "mimetype", "size"])
|
||||
|
||||
if head_response["ContentType"] != mimetype:
|
||||
logger.info(
|
||||
"upload_ended: content type mismatch between object storage and file,"
|
||||
" updating from %s to %s",
|
||||
head_response["ContentType"],
|
||||
mimetype,
|
||||
)
|
||||
s3_client.copy_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=file.file_key,
|
||||
CopySource={
|
||||
"Bucket": default_storage.bucket_name,
|
||||
"Key": file.file_key,
|
||||
},
|
||||
ContentType=mimetype,
|
||||
Metadata=head_response["Metadata"],
|
||||
MetadataDirective="REPLACE",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to analyze file, reverting to pending state")
|
||||
file.upload_state = models.FileUploadStateChoices.PENDING
|
||||
file.save()
|
||||
raise e
|
||||
|
||||
if validation_error:
|
||||
raise validation_error
|
||||
|
||||
# Not yet implemented
|
||||
# Change the file.upload_state when this will be done
|
||||
# malware_detection.analyse_file(file.file_key, file_id=file.id)
|
||||
@@ -1326,7 +1342,7 @@ class FileViewSet(
|
||||
"""Delete a file completely."""
|
||||
file.soft_delete()
|
||||
file.hard_delete()
|
||||
transaction.on_commit(lambda: process_file_deletion.delay(file.id))
|
||||
process_file_deletion.delay(file.id)
|
||||
|
||||
def _authorize_subrequest(self, request, pattern):
|
||||
"""
|
||||
@@ -1417,7 +1433,7 @@ class FileViewSet(
|
||||
request, MEDIA_STORAGE_URL_PATTERN
|
||||
)
|
||||
|
||||
if not file.is_ready:
|
||||
if file.is_pending_upload:
|
||||
logger.warning("File '%s' is not ready", file.id)
|
||||
raise drf_exceptions.PermissionDenied()
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from pydantic import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
from core import models, utils
|
||||
from core.api.serializers import BaseValidationOnlySerializer, RoomConfiguration
|
||||
from core.api.serializers import BaseValidationOnlySerializer
|
||||
|
||||
OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"
|
||||
|
||||
@@ -35,37 +34,10 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
following the principle of least privilege.
|
||||
"""
|
||||
|
||||
configuration = serializers.JSONField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = models.Room
|
||||
fields = ["id", "name", "slug", "pin_code", "access_level", "configuration"]
|
||||
read_only_fields = ["id", "name", "slug", "pin_code"]
|
||||
|
||||
def validate_configuration(self, value):
|
||||
"""Validate room configuration against the RoomConfiguration schema."""
|
||||
if value is None or value == {}:
|
||||
return value
|
||||
try:
|
||||
RoomConfiguration.model_validate(value)
|
||||
except ValidationError as e:
|
||||
raise serializers.ValidationError(e.errors()) from e
|
||||
return value
|
||||
|
||||
def validate_access_level(self, access_level):
|
||||
"""Reject public access_level unless explicitly allowed or the default is already public."""
|
||||
|
||||
if settings.EXTERNAL_API_DEFAULT_ACCESS_LEVEL == models.RoomAccessLevel.PUBLIC:
|
||||
return access_level
|
||||
|
||||
if (
|
||||
access_level == models.RoomAccessLevel.PUBLIC
|
||||
and not settings.EXTERNAL_API_ALLOW_PUBLIC_ACCESS
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Public rooms are disabled for the external API."
|
||||
)
|
||||
return access_level
|
||||
fields = ["id", "name", "slug", "pin_code", "access_level"]
|
||||
read_only_fields = ["id", "name", "slug", "pin_code", "access_level"]
|
||||
|
||||
def to_representation(self, instance):
|
||||
"""Enrich response with application-specific computed fields."""
|
||||
@@ -96,9 +68,6 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Set secure defaults
|
||||
validated_data["name"] = utils.generate_room_slug()
|
||||
validated_data.setdefault(
|
||||
"access_level", settings.EXTERNAL_API_DEFAULT_ACCESS_LEVEL
|
||||
)
|
||||
validated_data.setdefault("configuration", {})
|
||||
validated_data["access_level"] = models.RoomAccessLevel.TRUSTED
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
@@ -4,7 +4,7 @@ from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import SuspiciousOperation, ValidationError
|
||||
from django.core.validators import validate_email
|
||||
|
||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
||||
@@ -23,11 +23,6 @@ from core import api, models
|
||||
from core.api.feature_flag import FeatureFlag
|
||||
from core.services.jwt_token import JwtTokenService
|
||||
|
||||
from ..services.provisional_user_service import (
|
||||
ProvisionalUserCreationDisabledError,
|
||||
ProvisionalUserIntegrityError,
|
||||
ProvisionalUserService,
|
||||
)
|
||||
from . import authentication, permissions, serializers
|
||||
|
||||
logger = getLogger(__name__)
|
||||
@@ -99,14 +94,40 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
)
|
||||
|
||||
try:
|
||||
user, _ = ProvisionalUserService().get_or_create(email, client_id)
|
||||
except ProvisionalUserCreationDisabledError as not_found_error:
|
||||
raise drf_exceptions.NotFound("User not found.") from not_found_error
|
||||
except ProvisionalUserIntegrityError:
|
||||
return drf_response.Response(
|
||||
{"error": "Failed to create or retrieve provisional user."},
|
||||
status=drf_status.HTTP_409_CONFLICT,
|
||||
)
|
||||
user = models.User.objects.get(email__iexact=email)
|
||||
except models.User.DoesNotExist as e:
|
||||
if (
|
||||
settings.APPLICATION_ALLOW_USER_CREATION
|
||||
and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
|
||||
and not settings.OIDC_USER_SUB_FIELD_IMMUTABLE
|
||||
):
|
||||
# Create a provisional user without `sub`, identified by email only.
|
||||
#
|
||||
# This relies on Django LaSuite implicitly updating the `sub` field on the
|
||||
# user's first successful OIDC authentication. If this stops working,
|
||||
# check for behavior changes in Django LaSuite.
|
||||
#
|
||||
# `OIDC_USER_SUB_FIELD_IMMUTABLE` comes from Django LaSuite and prevents `sub`
|
||||
# updates. We override its default value to allow setting `sub` for
|
||||
# provisional users.
|
||||
user = models.User(
|
||||
sub=None,
|
||||
email=email,
|
||||
)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
logger.info(
|
||||
"Provisional user created via application: user_id=%s, email=%s, client_id=%s",
|
||||
user.id,
|
||||
email,
|
||||
application.client_id,
|
||||
)
|
||||
else:
|
||||
raise drf_exceptions.NotFound("User not found.") from e
|
||||
except models.User.MultipleObjectsReturned as e:
|
||||
raise SuspiciousOperation(
|
||||
"Multiple user accounts share a common email."
|
||||
) from e
|
||||
|
||||
scope = " ".join(application.scopes or [])
|
||||
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
"""Management command to merge duplicate users based on their email address."""
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
|
||||
from core.models import File, RecordingAccess, ResourceAccess, RoleChoices
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
ROLE_PRIORITY = {
|
||||
RoleChoices.OWNER: 3,
|
||||
RoleChoices.ADMIN: 2,
|
||||
RoleChoices.MEMBER: 1,
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Merge duplicate users sharing the same email into the most recently created one.
|
||||
|
||||
The KEPT user is the most recently created. All room memberships, recording
|
||||
accesses and files are transferred to it. When a conflict exists, the
|
||||
higher-privilege role wins. Stale users are then deleted.
|
||||
Each email group is processed inside a single database transaction.
|
||||
"""
|
||||
|
||||
help = __doc__
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Simulate the merge without writing any changes to the database.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--email-filter",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Only merge users whose email contains this string (e.g. '@example.com').",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Execute the management command."""
|
||||
dry_run = options["dry_run"]
|
||||
email_filter = options["email_filter"]
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write("[DRY-RUN] No changes will be written.\n")
|
||||
|
||||
users_qs = User.objects.all()
|
||||
if email_filter:
|
||||
users_qs = users_qs.filter(email__icontains=email_filter)
|
||||
self.stdout.write(f"[INFO] Filtering emails containing '{email_filter}'.\n")
|
||||
|
||||
duplicate_emails = (
|
||||
users_qs.exclude(email__isnull=True)
|
||||
.exclude(email="")
|
||||
.values("email")
|
||||
.annotate(cnt=Count("id"))
|
||||
.filter(cnt__gt=1)
|
||||
.values_list("email", flat=True)
|
||||
)
|
||||
|
||||
if not duplicate_emails:
|
||||
self.stdout.write("[INFO] No duplicate users found. Nothing to do.")
|
||||
return
|
||||
|
||||
self.stdout.write(
|
||||
f"[INFO] Found {len(duplicate_emails)} email(s) with duplicate users."
|
||||
)
|
||||
|
||||
total_merged = 0
|
||||
total_deleted = 0
|
||||
failed_emails = []
|
||||
|
||||
for email in duplicate_emails:
|
||||
# Secondary sort by id ensures a stable, deterministic order when
|
||||
# created_at timestamps are equal (common in tests and bulk imports).
|
||||
users = list(User.objects.filter(email=email).order_by("created_at", "id"))
|
||||
kept_user = users[-1]
|
||||
stale_users = users[:-1]
|
||||
|
||||
self.stdout.write(
|
||||
f"\n[INFO] Email '{email}': {len(users)} users — "
|
||||
f"keeping {kept_user.id} (created {kept_user.created_at.date()})."
|
||||
)
|
||||
for u in stale_users:
|
||||
self.stdout.write(
|
||||
f" stale: {u.id} (created {u.created_at.date()})"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
ra_count = ResourceAccess.objects.filter(user__in=stale_users).count()
|
||||
rca_count = RecordingAccess.objects.filter(user__in=stale_users).count()
|
||||
f_count = File.objects.filter(creator__in=stale_users).count()
|
||||
self.stdout.write(
|
||||
f" [DRY-RUN] Would migrate: {ra_count} ResourceAccess, "
|
||||
f"{rca_count} RecordingAccess, {f_count} File(s)."
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
group_deleted = 0
|
||||
with transaction.atomic():
|
||||
for stale_user in stale_users:
|
||||
self._merge_resource_accesses(stale_user, kept_user)
|
||||
self._merge_recording_accesses(stale_user, kept_user)
|
||||
self._merge_files(stale_user, kept_user)
|
||||
stale_user.delete()
|
||||
group_deleted += 1
|
||||
|
||||
total_deleted += group_deleted
|
||||
total_merged += 1
|
||||
|
||||
except Exception as exc: # noqa: BLE001 #pylint: disable=broad-exception-caught
|
||||
failed_emails.append(email)
|
||||
self.stderr.write(f"[ERROR] Failed to merge '{email}': {exc}")
|
||||
|
||||
if failed_emails:
|
||||
raise CommandError(
|
||||
f"Failed to merge {len(failed_emails)} email group(s): {', '.join(failed_emails)}"
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\n[DONE] Merged {total_merged} group(s), deleted {total_deleted} user(s)."
|
||||
)
|
||||
)
|
||||
|
||||
def _merge_resource_accesses(self, stale_user, kept_user):
|
||||
"""Transfer room memberships from stale_user to kept_user."""
|
||||
for ra in ResourceAccess.objects.filter(user=stale_user):
|
||||
existing = ResourceAccess.objects.filter(
|
||||
user=kept_user, resource=ra.resource
|
||||
).first()
|
||||
|
||||
if existing is None:
|
||||
ra.user = kept_user
|
||||
ra.save(update_fields=["user"])
|
||||
else:
|
||||
if ROLE_PRIORITY.get(ra.role, 0) > ROLE_PRIORITY.get(existing.role, 0):
|
||||
existing.role = ra.role
|
||||
existing.save(update_fields=["role"])
|
||||
ra.delete()
|
||||
|
||||
def _merge_recording_accesses(self, stale_user, kept_user):
|
||||
"""Transfer recording accesses from stale_user to kept_user."""
|
||||
for rca in RecordingAccess.objects.filter(user=stale_user):
|
||||
existing = RecordingAccess.objects.filter(
|
||||
user=kept_user, recording=rca.recording
|
||||
).first()
|
||||
|
||||
if existing is None:
|
||||
rca.user = kept_user
|
||||
rca.save(update_fields=["user"])
|
||||
else:
|
||||
if ROLE_PRIORITY.get(rca.role, 0) > ROLE_PRIORITY.get(existing.role, 0):
|
||||
existing.role = rca.role
|
||||
existing.save(update_fields=["role"])
|
||||
rca.delete()
|
||||
|
||||
def _merge_files(self, stale_user, kept_user):
|
||||
"""Re-assign files created by stale_user to kept_user."""
|
||||
File.objects.filter(creator=stale_user).update(creator=kept_user)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.14 on 2026-05-26 14:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_rename_active_application_is_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='recording',
|
||||
name='external_process_id',
|
||||
field=models.CharField(blank=True, help_text='ID of the external process associated with the recording.', max_length=255, null=True, unique=True, verbose_name='External Process ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recording',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('initiated', 'Initiated'), ('active', 'Active'), ('stopped', 'Stopped'), ('saved', 'Saved'), ('aborted', 'Aborted'), ('failed_to_start', 'Failed to Start'), ('failed_to_stop', 'Failed to Stop'), ('notification_succeeded', 'Notification succeeded'), ('external_process_successful', 'External process successful'), ('external_process_failed', 'External process failed')], default='initiated', max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.14 on 2026-06-02 17:31
|
||||
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('core', '0018_rename_active_application_is_active'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='user',
|
||||
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('email'), condition=models.Q(('sub__isnull', True)), name='unique_email_when_sub_is_null'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.14 on 2026-06-03 12:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_user_unique_email_when_sub_is_null'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='file',
|
||||
name='upload_state',
|
||||
field=models.CharField(choices=[('pending', 'Pending'), ('analyzing', 'Analyzing'), ('ready', 'Ready')], max_length=25),
|
||||
),
|
||||
]
|
||||
@@ -60,6 +60,11 @@ class RecordingStatusChoices(models.TextChoices):
|
||||
FAILED_TO_START = "failed_to_start", _("Failed to Start")
|
||||
FAILED_TO_STOP = "failed_to_stop", _("Failed to Stop")
|
||||
NOTIFICATION_SUCCEEDED = "notification_succeeded", _("Notification succeeded")
|
||||
EXTERNAL_PROCESS_SUCCESSFUL = (
|
||||
"external_process_successful",
|
||||
_("External process successful"),
|
||||
)
|
||||
EXTERNAL_PROCESS_FAILED = "external_process_failed", _("External process failed")
|
||||
|
||||
@classmethod
|
||||
def is_final(cls, status):
|
||||
@@ -73,6 +78,8 @@ class RecordingStatusChoices(models.TextChoices):
|
||||
cls.STOPPED,
|
||||
cls.SAVED,
|
||||
cls.ABORTED,
|
||||
cls.EXTERNAL_PROCESS_SUCCESSFUL,
|
||||
cls.EXTERNAL_PROCESS_FAILED,
|
||||
cls.FAILED_TO_START,
|
||||
cls.FAILED_TO_STOP,
|
||||
}
|
||||
@@ -211,13 +218,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
ordering = ("-created_at",)
|
||||
verbose_name = _("user")
|
||||
verbose_name_plural = _("users")
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
models.functions.Lower("email"),
|
||||
condition=models.Q(sub__isnull=True),
|
||||
name="unique_email_when_sub_is_null",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.email or self.admin_email or str(self.id)
|
||||
@@ -598,6 +598,14 @@ class Recording(BaseModel):
|
||||
verbose_name=_("Recording options"),
|
||||
help_text=_("Recording options"),
|
||||
)
|
||||
external_process_id = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
unique=True,
|
||||
verbose_name=_("External Process ID"),
|
||||
help_text=_("ID of the external process associated with the recording."),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "meet_recording"
|
||||
@@ -653,6 +661,8 @@ class Recording(BaseModel):
|
||||
return self.status in {
|
||||
RecordingStatusChoices.NOTIFICATION_SUCCEEDED,
|
||||
RecordingStatusChoices.SAVED,
|
||||
RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL,
|
||||
RecordingStatusChoices.EXTERNAL_PROCESS_FAILED,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -846,8 +856,8 @@ class FileUploadStateChoices(models.TextChoices):
|
||||
"""Possible states of a file."""
|
||||
|
||||
PENDING = "pending", _("Pending")
|
||||
ANALYZING = "analyzing", _("Analyzing")
|
||||
# Commented out for now, as we may need this when we implement the malware detection logic.
|
||||
# ANALYZING = "analyzing", _("Analyzing")
|
||||
# SUSPICIOUS = "suspicious", _("Suspicious")
|
||||
# FILE_TOO_LARGE_TO_ANALYZE = (
|
||||
# "file_too_large_to_analyze",
|
||||
@@ -925,9 +935,9 @@ class File(BaseModel):
|
||||
return super().delete(using, keep_parents)
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
"""Return whether the file is in a ready upload state"""
|
||||
return self.upload_state == FileUploadStateChoices.READY
|
||||
def is_pending_upload(self):
|
||||
"""Return whether the file is in a pending upload state"""
|
||||
return self.upload_state == FileUploadStateChoices.PENDING
|
||||
|
||||
@property
|
||||
def extension(self):
|
||||
@@ -954,16 +964,6 @@ class File(BaseModel):
|
||||
|
||||
return f"{settings.FILE_UPLOAD_PATH}/{self.pk!s}"
|
||||
|
||||
@property
|
||||
def temporary_key_base(self):
|
||||
"""Temporary key base used while upload is still pending."""
|
||||
if not self.pk:
|
||||
raise RuntimeError(
|
||||
"The file instance must be saved before requesting a storage key."
|
||||
)
|
||||
|
||||
return f"{settings.FILE_UPLOAD_TMP_PATH}/{self.pk!s}"
|
||||
|
||||
@property
|
||||
def file_key(self):
|
||||
"""Key used to store the file in object storage."""
|
||||
@@ -972,12 +972,6 @@ class File(BaseModel):
|
||||
# leaking Personal Information in logs, etc.
|
||||
return f"{self.key_base}{extension!s}"
|
||||
|
||||
@property
|
||||
def temporary_file_key(self):
|
||||
"""Temporary key used to upload the file before it is finalized."""
|
||||
_, extension = splitext(self.filename)
|
||||
return f"{self.temporary_key_base}{extension!s}"
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the file.
|
||||
|
||||
@@ -14,9 +14,9 @@ logger = logging.getLogger(__name__)
|
||||
class MachineUser:
|
||||
"""Represent a non-interactive system user for automated storage operations."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, username: str = "storage_event_user") -> None:
|
||||
self.pk = None
|
||||
self.username = "storage_event_user"
|
||||
self.username = username
|
||||
self.is_active = True
|
||||
|
||||
@property
|
||||
@@ -91,3 +91,29 @@ class StorageEventAuthentication(BaseAuthentication):
|
||||
def authenticate_header(self, request):
|
||||
"""Return the WWW-Authenticate header value."""
|
||||
return f"{self.TOKEN_TYPE} realm='Storage event API'"
|
||||
|
||||
|
||||
class RecordingProcessWebhookAuthentication(BaseAuthentication):
|
||||
"""
|
||||
Custom authentication class for recording process webhook requests.
|
||||
Validates the API key in the Authorization header.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Authenticate the request and return a two-tuple of (user, token).
|
||||
"""
|
||||
logger.info("Authentificating")
|
||||
authorization_header: str = request.headers.get("Authorization") or ""
|
||||
|
||||
if not secrets.compare_digest(
|
||||
authorization_header.removeprefix("Bearer "),
|
||||
settings.SUMMARY_SERVICE_WEBHOOK_API_TOKEN,
|
||||
):
|
||||
logger.warning(
|
||||
"Authentication failed: Bad Authorization header (ip: %s)",
|
||||
request.META.get("REMOTE_ADDR"),
|
||||
)
|
||||
raise AuthenticationFailed()
|
||||
|
||||
return MachineUser("external_process_user"), None
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Service to notify external services when a new recording is ready."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import smtplib
|
||||
import warnings
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.conf import settings
|
||||
@@ -17,6 +19,7 @@ from asgiref.sync import async_to_sync
|
||||
from livekit import api as livekit_api
|
||||
|
||||
from core import models, utils
|
||||
from core.utils import generate_download_s3_file_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -215,18 +218,45 @@ class NotificationService:
|
||||
)(recording.worker_id)
|
||||
|
||||
payload = {
|
||||
# Legacy V1 params to avoid a breaking change
|
||||
"owner_id": str(owner_access.user.id),
|
||||
"recording_filename": recording.key,
|
||||
"metadata_filename": metadata_filename,
|
||||
"email": owner_access.user.email,
|
||||
"sub": owner_access.user.sub,
|
||||
"room": recording.room.name,
|
||||
"language": recording.options.get("language"),
|
||||
"owner_timezone": str(owner_access.user.timezone),
|
||||
"download_link": f"{get_recording_download_base_url()}/{recording.id}",
|
||||
"context_language": owner_access.user.language,
|
||||
"recording_start_at": (started_at.isoformat() if started_at else None),
|
||||
"recording_end_at": (ended_at.isoformat() if ended_at else None),
|
||||
# V2 params
|
||||
"user_sub": owner_access.user.sub,
|
||||
"user_email": owner_access.user.email,
|
||||
"cloud_storage_url": generate_download_s3_file_url(
|
||||
recording.key, expires_in=60 * 60 * 24, override_domain=False
|
||||
),
|
||||
"language": recording.options.get("language", "fr"),
|
||||
"context_language": owner_access.user.language,
|
||||
"push_to_docs_config": {
|
||||
"user_email": owner_access.user.email,
|
||||
"title": "TODO",
|
||||
"download_link": f"{get_recording_download_base_url()}/{recording.id}",
|
||||
# For now the feature flag logic is handled on summary side
|
||||
"auto_create_summary": True,
|
||||
},
|
||||
"metadata": (
|
||||
{
|
||||
"cloud_storage_url": generate_download_s3_file_url(
|
||||
metadata_filename,
|
||||
expires_in=60 * 60 * 24,
|
||||
override_domain=False,
|
||||
),
|
||||
"started_at": started_at.isoformat(),
|
||||
"ended_at": ended_at.isoformat(),
|
||||
}
|
||||
if (started_at and ended_at)
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
headers = {
|
||||
@@ -242,6 +272,31 @@ class NotificationService:
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
is_v2_implementation = False
|
||||
try:
|
||||
response_json = response.json()
|
||||
# We do not require a job_id to avoid a breaking change
|
||||
if job_id := response_json.get("job_id"):
|
||||
recording.external_process_id = job_id
|
||||
recording.save()
|
||||
is_v2_implementation = True
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if not is_v2_implementation:
|
||||
warnings.warn(
|
||||
"You are likely using your own implementation for the summary / "
|
||||
"transcribe service."
|
||||
"We have released a new version and API contract for that service, "
|
||||
"we recommend checking it out"
|
||||
# pylint: disable=line-too-long
|
||||
"https://github.com/suitenumerique/meet/blob/19c2a378e7e66652afbfa3e749badd697e3917ac/src/summary/summary/api/route/tasks_v2.py "
|
||||
"and mimicking it's behavior. We will remove the legacy "
|
||||
"handling in a future version.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
except requests.RequestException as exc:
|
||||
logger.exception(
|
||||
"Summary service error for recording %s. URL: %s. Exception: %s",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Meet storage event parser classes."""
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
@@ -19,9 +18,6 @@ from .exceptions import (
|
||||
ParsingEventDataError,
|
||||
)
|
||||
|
||||
# Additional MIME type mapping
|
||||
mimetypes.add_type("audio/ogg", ".ogg")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -78,8 +74,8 @@ def get_parser() -> EventParser:
|
||||
return event_parser_cls(bucket_name=settings.AWS_STORAGE_BUCKET_NAME)
|
||||
|
||||
|
||||
class BaseS3Parser:
|
||||
"""Base class for handling parsing and validation of S3-compatible storage events."""
|
||||
class MinioParser:
|
||||
"""Handle parsing and validation of Minio storage events."""
|
||||
|
||||
def __init__(self, bucket_name: str, allowed_filetypes=None):
|
||||
"""Initialize parser with target bucket name and accepted filetypes."""
|
||||
@@ -95,6 +91,32 @@ class BaseS3Parser:
|
||||
rf"(?P<url_encoded_folder_path>(?:[^%]+%2F)+)?{settings.RECORDING_OUTPUT_FOLDER}%2F(?P<recording_id>{UUID_REGEX})\.(?P<extension>{FILE_EXT_REGEX})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse(data):
|
||||
"""Convert raw Minio event dictionary to StorageEvent object."""
|
||||
|
||||
if not data:
|
||||
raise ParsingEventDataError("Received empty data.")
|
||||
|
||||
try:
|
||||
record = data["Records"][0]
|
||||
s3 = record["s3"]
|
||||
bucket_name = s3["bucket"]["name"]
|
||||
file_object = s3["object"]
|
||||
filepath = file_object["key"]
|
||||
filetype = file_object["contentType"]
|
||||
except (KeyError, IndexError) as e:
|
||||
raise ParsingEventDataError(f"Missing or malformed key: {e}.") from e
|
||||
try:
|
||||
return StorageEvent(
|
||||
filepath=filepath,
|
||||
filetype=filetype,
|
||||
bucket_name=bucket_name,
|
||||
metadata=None,
|
||||
)
|
||||
except TypeError as e:
|
||||
raise ParsingEventDataError(f"Missing essential data fields: {e}") from e
|
||||
|
||||
def validate(self, event_data: StorageEvent) -> str:
|
||||
"""Verify StorageEvent matches bucket, filetype and filepath requirements."""
|
||||
|
||||
@@ -119,56 +141,9 @@ class BaseS3Parser:
|
||||
return recording_id
|
||||
|
||||
def get_recording_id(self, data):
|
||||
"""Extract recording ID from S3 event through parsing and validation."""
|
||||
"""Extract recording ID from Minio event through parsing and validation."""
|
||||
|
||||
event_data = self.parse(data)
|
||||
return self.validate(event_data)
|
||||
recording_id = self.validate(event_data)
|
||||
|
||||
def parse(self, data: Dict) -> StorageEvent:
|
||||
"""To be implemented by subclasses."""
|
||||
raise NotImplementedError("Subclasses must implement parse()")
|
||||
|
||||
|
||||
class MinioParser(BaseS3Parser):
|
||||
"""Minio specific event parsing."""
|
||||
|
||||
def parse(self, data: Dict) -> StorageEvent:
|
||||
if not data:
|
||||
raise ParsingEventDataError("Received empty data.")
|
||||
try:
|
||||
record = data["Records"][0]
|
||||
s3 = record["s3"]
|
||||
return StorageEvent(
|
||||
filepath=s3["object"]["key"],
|
||||
filetype=s3["object"]["contentType"], # Minio-specific field
|
||||
bucket_name=s3["bucket"]["name"],
|
||||
metadata=None,
|
||||
)
|
||||
except (KeyError, IndexError) as e:
|
||||
raise ParsingEventDataError(f"Malformed Minio event: {e}") from e
|
||||
except TypeError as e:
|
||||
raise ParsingEventDataError(f"Missing essential data fields: {e}") from e
|
||||
|
||||
|
||||
class S3Parser(BaseS3Parser):
|
||||
"""AWS S3 specific event parsing."""
|
||||
|
||||
def parse(self, data: Dict) -> StorageEvent:
|
||||
if not data:
|
||||
raise ParsingEventDataError("Received empty data.")
|
||||
try:
|
||||
# AWS S3 structure can slightly differ from Minio implementation
|
||||
record = data["Records"][0]
|
||||
s3 = record["s3"]
|
||||
filepath = s3["object"]["key"]
|
||||
if not filepath:
|
||||
raise ParsingEventDataError("Missing object key name")
|
||||
filetype, _ = mimetypes.guess_type(filepath)
|
||||
return StorageEvent(
|
||||
filepath=filepath,
|
||||
filetype=filetype,
|
||||
bucket_name=s3["bucket"]["name"],
|
||||
metadata=None,
|
||||
)
|
||||
except (KeyError, IndexError) as e:
|
||||
raise ParsingEventDataError(f"Malformed S3 event: {e}") from e
|
||||
return recording_id
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
"""Service for provisional user creation."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation, ValidationError
|
||||
from django.db import IntegrityError
|
||||
|
||||
from core import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProvisionalUserError(Exception):
|
||||
"""Base exception for provisional user service errors."""
|
||||
|
||||
|
||||
class ProvisionalUserCreationDisabledError(ProvisionalUserError):
|
||||
"""Raised when provisional user creation is disabled by configuration."""
|
||||
|
||||
|
||||
class ProvisionalUserIntegrityError(ProvisionalUserError):
|
||||
"""Raised when a provisional user cannot be created or retrieved after a race condition."""
|
||||
|
||||
|
||||
class ProvisionalUserService:
|
||||
"""Handles creation and retrieval of provisional users.
|
||||
|
||||
A provisional user is created without a `sub`, identified by email only.
|
||||
The `sub` is set on first successful OIDC authentication via Django LaSuite.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the service."""
|
||||
|
||||
# `OIDC_USER_SUB_FIELD_IMMUTABLE` comes from Django LaSuite and prevents `sub`
|
||||
# updates. We override its default value to allow setting `sub` for
|
||||
# provisional users.
|
||||
|
||||
self._is_creation_enabled = (
|
||||
settings.APPLICATION_ALLOW_USER_CREATION
|
||||
and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
|
||||
and not settings.OIDC_USER_SUB_FIELD_IMMUTABLE
|
||||
)
|
||||
|
||||
def _get_by_email(self, email: str) -> models.User | None:
|
||||
"""Return the user with this email, or None if not found."""
|
||||
try:
|
||||
return models.User.objects.get(email__iexact=email)
|
||||
except models.User.DoesNotExist:
|
||||
return None
|
||||
except models.User.MultipleObjectsReturned as e:
|
||||
raise SuspiciousOperation(
|
||||
"Multiple user accounts share a common email."
|
||||
) from e
|
||||
|
||||
def get_or_create(
|
||||
self, email: str, client_id: str
|
||||
) -> tuple[models.User | None, bool]:
|
||||
"""Get or create a provisional user identified by email.
|
||||
|
||||
Args:
|
||||
email: The email address to identify the user.
|
||||
client_id: The application client_id, used for audit logging only.
|
||||
|
||||
Returns:
|
||||
A (user, created) tuple — mirrors get_or_create conventions.
|
||||
|
||||
Raises:
|
||||
ProvisionalUserError: If creation and retrieval both fail.
|
||||
"""
|
||||
user = self._get_by_email(email)
|
||||
if user:
|
||||
return user, False
|
||||
|
||||
if not self._is_creation_enabled:
|
||||
raise ProvisionalUserCreationDisabledError(
|
||||
"Provisional user creation is disabled by configuration."
|
||||
)
|
||||
|
||||
# Create a provisional user without `sub`, identified by email only.
|
||||
# This relies on Django LaSuite implicitly updating the `sub` field on the
|
||||
# user's first successful OIDC authentication. If this stops working,
|
||||
# check for behavior changes in Django LaSuite.
|
||||
try:
|
||||
user = models.User(sub=None, email=email)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
logger.info(
|
||||
"Provisional user created via application: user_id=%s, email=%s, client_id=%s",
|
||||
user.id,
|
||||
email,
|
||||
client_id,
|
||||
)
|
||||
return user, True
|
||||
except (IntegrityError, ValidationError) as e:
|
||||
logger.warning(
|
||||
"Race condition on provisional user creation, fetching existing: "
|
||||
"email=%s, client_id=%s",
|
||||
email,
|
||||
client_id,
|
||||
)
|
||||
user = self._get_by_email(email)
|
||||
|
||||
if user:
|
||||
return user, False
|
||||
|
||||
raise ProvisionalUserIntegrityError(
|
||||
"Failed to create or retrieve provisional user."
|
||||
) from e
|
||||
@@ -346,11 +346,8 @@ def test_authentication_getter_existing_user_change_fields(
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
# Note: .save() triggers uniqueness validation queries for unique fields,
|
||||
# adding extra SELECT queries before the UPDATE:
|
||||
# - unique=True on 'sub'
|
||||
# - unique=True on 'admin_email'
|
||||
# - partial unique index 'unique_email_when_sub_is_null'
|
||||
with django_assert_num_queries(5):
|
||||
# adding extra SELECT queries before the UPDATE (e.g., checking unique=True on 'sub')
|
||||
with django_assert_num_queries(3):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
@@ -118,7 +118,7 @@ def test_api_files_create_file_authenticated_success():
|
||||
|
||||
assert policy_parsed.scheme == "http"
|
||||
assert policy_parsed.netloc in ["minio:9000", "localhost:9000"]
|
||||
assert policy_parsed.path == f"/meet-media-storage/tmp/files/{file.id!s}.png"
|
||||
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png"
|
||||
|
||||
query_params = parse_qs(policy_parsed.query)
|
||||
|
||||
|
||||
@@ -86,11 +86,7 @@ def test_api_files_media_get_own():
|
||||
assert response.content.decode("utf-8") == "my prose"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rejecting_status",
|
||||
[models.FileUploadStateChoices.PENDING, models.FileUploadStateChoices.ANALYZING],
|
||||
)
|
||||
def test_api_files_media_auth_rejects(rejecting_status):
|
||||
def test_api_files_media_auth_file_pending():
|
||||
"""
|
||||
Users who have a specific access to an file, whatever the role, should not be able to
|
||||
retrieve related attachments if the file is not ready.
|
||||
@@ -101,7 +97,7 @@ def test_api_files_media_auth_rejects(rejecting_status):
|
||||
|
||||
file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
upload_state=rejecting_status,
|
||||
upload_state=models.FileUploadStateChoices.PENDING,
|
||||
creator=user,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Test related to item upload ended API."""
|
||||
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from io import BytesIO
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
@@ -82,7 +81,7 @@ def test_api_file_upload_ended_success(settings):
|
||||
)
|
||||
|
||||
default_storage.save(
|
||||
file.temporary_file_key,
|
||||
file.file_key,
|
||||
BytesIO(b"my prose"),
|
||||
)
|
||||
|
||||
@@ -98,7 +97,6 @@ def test_api_file_upload_ended_success(settings):
|
||||
assert response.json()["mimetype"] == "text/plain"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_api_file_upload_ended_mimetype_not_allowed(settings, caplog):
|
||||
"""
|
||||
Test that the API returns a 400 when the mimetype is not allowed.
|
||||
@@ -121,7 +119,7 @@ def test_api_file_upload_ended_mimetype_not_allowed(settings, caplog):
|
||||
)
|
||||
|
||||
default_storage.save(
|
||||
file.temporary_file_key,
|
||||
file.file_key,
|
||||
BytesIO(b"my prose"),
|
||||
)
|
||||
|
||||
@@ -158,7 +156,7 @@ def test_api_file_upload_ended_mimetype_not_allowed_not_checking_mimetype(settin
|
||||
)
|
||||
|
||||
default_storage.save(
|
||||
file.temporary_file_key,
|
||||
file.file_key,
|
||||
BytesIO(b"my prose"),
|
||||
)
|
||||
|
||||
@@ -202,7 +200,7 @@ def test_api_upload_ended_mismatch_mimetype_with_object_storage(settings, caplog
|
||||
|
||||
s3_client.put_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=file.temporary_file_key,
|
||||
Key=file.file_key,
|
||||
ContentType="text/html",
|
||||
Body=BytesIO(
|
||||
b'<meta http-equiv="refresh" content="0; url=https://fichiers.numerique.gouv.fr">'
|
||||
@@ -213,7 +211,7 @@ def test_api_upload_ended_mismatch_mimetype_with_object_storage(settings, caplog
|
||||
)
|
||||
|
||||
head_object = s3_client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file.temporary_file_key
|
||||
Bucket=default_storage.bucket_name, Key=file.file_key
|
||||
)
|
||||
|
||||
assert head_object["ContentType"] == "text/html"
|
||||
@@ -236,10 +234,9 @@ def test_api_upload_ended_mismatch_mimetype_with_object_storage(settings, caplog
|
||||
assert head_object["Metadata"] == {"foo": "bar"}
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_api_upload_ended_file_size_exceeded(settings, caplog):
|
||||
"""
|
||||
Test when the file size exceeds the allowed max upload file size
|
||||
Test when the file size exceed the allowed max upload file size
|
||||
should return a 400 and delete the file.
|
||||
"""
|
||||
|
||||
@@ -259,7 +256,7 @@ def test_api_upload_ended_file_size_exceeded(settings, caplog):
|
||||
)
|
||||
|
||||
default_storage.save(
|
||||
file.temporary_file_key,
|
||||
file.file_key,
|
||||
BytesIO(b"my prose"),
|
||||
)
|
||||
|
||||
@@ -273,48 +270,3 @@ def test_api_upload_ended_file_size_exceeded(settings, caplog):
|
||||
|
||||
assert not models.File.objects.filter(id=file.id).exists()
|
||||
assert not default_storage.exists(file.file_key)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_api_file_upload_ended_concurrent_calls_are_serialized(settings):
|
||||
"""Only one concurrent upload-ended call can finalize a pending upload."""
|
||||
settings.FILE_UPLOAD_APPLY_RESTRICTIONS = True
|
||||
settings.FILE_UPLOAD_RESTRICTIONS = {
|
||||
"background_image": {
|
||||
**settings.FILE_UPLOAD_RESTRICTIONS["background_image"],
|
||||
"allowed_mimetypes": ["text/plain"],
|
||||
},
|
||||
}
|
||||
|
||||
user = factories.UserFactory()
|
||||
file = factories.FileFactory(
|
||||
type=FileTypeChoices.BACKGROUND_IMAGE,
|
||||
filename="my_file.txt",
|
||||
creator=user,
|
||||
)
|
||||
default_storage.save(file.temporary_file_key, BytesIO(b"my prose"))
|
||||
|
||||
def call_upload_ended():
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
return client.post(f"/api/v1.0/files/{file.id!s}/upload-ended/")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
futures = [
|
||||
executor.submit(call_upload_ended),
|
||||
executor.submit(call_upload_ended),
|
||||
]
|
||||
responses = [future.result() for future in futures]
|
||||
|
||||
status_codes = sorted(response.status_code for response in responses)
|
||||
assert status_codes == [200, 400]
|
||||
|
||||
failed_response = next(
|
||||
response for response in responses if response.status_code == 400
|
||||
)
|
||||
assert failed_response.json() == {
|
||||
"file": "This action is only available for files in PENDING state."
|
||||
}
|
||||
|
||||
file.refresh_from_db()
|
||||
assert file.upload_state == FileUploadStateChoices.READY
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
"""Tests for the merge_duplicate_users management command."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import base, call_command
|
||||
|
||||
import pytest
|
||||
|
||||
from core.factories import (
|
||||
FileFactory,
|
||||
UserFactory,
|
||||
UserRecordingAccessFactory,
|
||||
UserResourceAccessFactory,
|
||||
)
|
||||
from core.models import RecordingAccess, ResourceAccess, RoleChoices, User
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
# pylint: disable=W0613
|
||||
|
||||
|
||||
def test_merge_no_duplicates_does_nothing():
|
||||
"""Command should do nothing when no duplicate users exist."""
|
||||
user = UserFactory(email="unique@example.com")
|
||||
call_command("merge_duplicate_users")
|
||||
assert User.objects.count() == 1
|
||||
assert User.objects.filter(id=user.id).exists()
|
||||
|
||||
|
||||
def test_merge_keeps_most_recently_created_user():
|
||||
"""Command should keep the most recently created user when duplicates exist."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
call_command("merge_duplicate_users")
|
||||
assert not User.objects.filter(id=user1.id).exists()
|
||||
assert User.objects.filter(id=user2.id).exists()
|
||||
|
||||
|
||||
def test_merge_deletes_all_stale_users():
|
||||
"""Command should delete all stale users and keep only the most recently created one."""
|
||||
email = "many@example.com"
|
||||
UserFactory(email=email)
|
||||
UserFactory(email=email)
|
||||
user_kept = UserFactory(email=email)
|
||||
call_command("merge_duplicate_users")
|
||||
assert User.objects.filter(email=email).count() == 1
|
||||
assert User.objects.filter(id=user_kept.id).exists()
|
||||
|
||||
|
||||
# ── ResourceAccess ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_transfers_resource_access_to_kept_user():
|
||||
"""ResourceAccess should be transferred to the kept user when stale user is merged."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
ra = UserResourceAccessFactory(user=user1)
|
||||
call_command("merge_duplicate_users")
|
||||
ra.refresh_from_db()
|
||||
assert ra.user == user2
|
||||
|
||||
|
||||
def test_merge_transfers_multiple_room_accesses():
|
||||
"""All ResourceAccesses should be transferred to the kept user when stale user is merged."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
accesses = UserResourceAccessFactory.create_batch(3, user=user1)
|
||||
call_command("merge_duplicate_users")
|
||||
assert not ResourceAccess.objects.filter(user=user1).exists()
|
||||
for ra in accesses:
|
||||
assert ResourceAccess.objects.filter(user=user2, resource=ra.resource).exists()
|
||||
|
||||
|
||||
def test_merge_all_resource_accesses_owned_by_kept_user_nothing_changes():
|
||||
"""ResourceAccesses should remain unchanged when all are already owned by the kept user."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
accesses = UserResourceAccessFactory.create_batch(3, user=user2)
|
||||
call_command("merge_duplicate_users")
|
||||
assert not ResourceAccess.objects.filter(user=user1).exists()
|
||||
for ra in accesses:
|
||||
ra.refresh_from_db()
|
||||
assert ra.user == user2
|
||||
|
||||
|
||||
def test_merge_resource_access_conflict_upgrades_to_owner():
|
||||
"""ResourceAccess role should be upgraded to owner when stale user has a higher role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
ra1 = UserResourceAccessFactory(user=user1, role=RoleChoices.OWNER)
|
||||
ra2 = UserResourceAccessFactory(
|
||||
user=user2, resource=ra1.resource, role=RoleChoices.MEMBER
|
||||
)
|
||||
other_accesses = UserResourceAccessFactory.create_batch(
|
||||
3, user=user1, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
ra2.refresh_from_db()
|
||||
assert ra2.role == RoleChoices.OWNER
|
||||
assert not ResourceAccess.objects.filter(user=user1).exists()
|
||||
for ra in other_accesses:
|
||||
assert ResourceAccess.objects.filter(
|
||||
user=user2, resource=ra.resource, role=RoleChoices.MEMBER
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_resource_access_conflict_upgrades_to_admin():
|
||||
"""ResourceAccess role should be upgraded to admin when stale user has a higher role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
ra1 = UserResourceAccessFactory(user=user1, role=RoleChoices.ADMIN)
|
||||
ra2 = UserResourceAccessFactory(
|
||||
user=user2, resource=ra1.resource, role=RoleChoices.MEMBER
|
||||
)
|
||||
other_accesses = UserResourceAccessFactory.create_batch(
|
||||
3, user=user1, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
ra2.refresh_from_db()
|
||||
assert ra2.role == RoleChoices.ADMIN
|
||||
assert not ResourceAccess.objects.filter(user=user1).exists()
|
||||
for ra in other_accesses:
|
||||
assert ResourceAccess.objects.filter(
|
||||
user=user2, resource=ra.resource, role=RoleChoices.MEMBER
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_resource_access_conflict_does_not_downgrade_role():
|
||||
"""ResourceAccess role should not be downgraded when stale user has a lower role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
ra1 = UserResourceAccessFactory(user=user1, role=RoleChoices.MEMBER)
|
||||
ra2 = UserResourceAccessFactory(
|
||||
user=user2, resource=ra1.resource, role=RoleChoices.OWNER
|
||||
)
|
||||
other_accesses = UserResourceAccessFactory.create_batch(
|
||||
3, user=user1, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
ra2.refresh_from_db()
|
||||
assert ra2.role == RoleChoices.OWNER
|
||||
assert not ResourceAccess.objects.filter(user=user1).exists()
|
||||
for ra in other_accesses:
|
||||
assert ResourceAccess.objects.filter(
|
||||
user=user2, resource=ra.resource, role=RoleChoices.MEMBER
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_resource_access_conflict_equal_role_keeps_single_access():
|
||||
"""ResourceAccess should keep one entry for the kept user when both ones have the same role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
ra1 = UserResourceAccessFactory(user=user1, role=RoleChoices.MEMBER)
|
||||
UserResourceAccessFactory(
|
||||
user=user2, resource=ra1.resource, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
accesses = ResourceAccess.objects.filter(resource=ra1.resource)
|
||||
assert accesses.count() == 1
|
||||
assert accesses.first().user == user2
|
||||
assert accesses.first().role == RoleChoices.MEMBER
|
||||
|
||||
|
||||
# ── RecordingAccess ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_transfers_recording_access_to_kept_user():
|
||||
"""RecordingAccess should be transferred to the kept user when stale user is merged."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
rca = UserRecordingAccessFactory(user=user1)
|
||||
call_command("merge_duplicate_users")
|
||||
rca.refresh_from_db()
|
||||
assert rca.user == user2
|
||||
|
||||
|
||||
def test_merge_transfers_multiple_recording_accesses():
|
||||
"""All RecordingAccesses should be transferred to the kept user when stale user is merged."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
accesses = UserRecordingAccessFactory.create_batch(3, user=user1)
|
||||
call_command("merge_duplicate_users")
|
||||
assert not RecordingAccess.objects.filter(user=user1).exists()
|
||||
for rca in accesses:
|
||||
assert RecordingAccess.objects.filter(
|
||||
user=user2, recording=rca.recording
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_all_recording_accesses_owned_by_kept_user_nothing_changes():
|
||||
"""RecordingAccesses should remain unchanged when all are already owned by the kept user."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
accesses = UserRecordingAccessFactory.create_batch(3, user=user2)
|
||||
call_command("merge_duplicate_users")
|
||||
assert not RecordingAccess.objects.filter(user=user1).exists()
|
||||
for rca in accesses:
|
||||
rca.refresh_from_db()
|
||||
assert rca.user == user2
|
||||
|
||||
|
||||
def test_merge_recording_access_conflict_upgrades_to_owner():
|
||||
"""RecordingAccess role should be upgraded to owner when stale user has a higher role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
rca1 = UserRecordingAccessFactory(user=user1, role=RoleChoices.OWNER)
|
||||
rca2 = UserRecordingAccessFactory(
|
||||
user=user2, recording=rca1.recording, role=RoleChoices.MEMBER
|
||||
)
|
||||
other_accesses = UserRecordingAccessFactory.create_batch(
|
||||
3, user=user1, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
rca2.refresh_from_db()
|
||||
assert rca2.role == RoleChoices.OWNER
|
||||
assert not RecordingAccess.objects.filter(user=user1).exists()
|
||||
for rca in other_accesses:
|
||||
assert RecordingAccess.objects.filter(
|
||||
user=user2, recording=rca.recording, role=RoleChoices.MEMBER
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_recording_access_conflict_upgrades_to_admin():
|
||||
"""RecordingAccess role should be upgraded to admin when stale user has a higher role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
rca1 = UserRecordingAccessFactory(user=user1, role=RoleChoices.ADMIN)
|
||||
rca2 = UserRecordingAccessFactory(
|
||||
user=user2, recording=rca1.recording, role=RoleChoices.MEMBER
|
||||
)
|
||||
other_accesses = UserRecordingAccessFactory.create_batch(
|
||||
3, user=user1, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
rca2.refresh_from_db()
|
||||
assert rca2.role == RoleChoices.ADMIN
|
||||
assert not RecordingAccess.objects.filter(user=user1).exists()
|
||||
for rca in other_accesses:
|
||||
assert RecordingAccess.objects.filter(
|
||||
user=user2, recording=rca.recording, role=RoleChoices.MEMBER
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_recording_access_conflict_does_not_downgrade_role():
|
||||
"""RecordingAccess role should not be downgraded when stale user has a lower role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
rca1 = UserRecordingAccessFactory(user=user1, role=RoleChoices.MEMBER)
|
||||
rca2 = UserRecordingAccessFactory(
|
||||
user=user2, recording=rca1.recording, role=RoleChoices.OWNER
|
||||
)
|
||||
other_accesses = UserRecordingAccessFactory.create_batch(
|
||||
3, user=user1, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
rca2.refresh_from_db()
|
||||
assert rca2.role == RoleChoices.OWNER
|
||||
assert not RecordingAccess.objects.filter(user=user1).exists()
|
||||
for rca in other_accesses:
|
||||
assert RecordingAccess.objects.filter(
|
||||
user=user2, recording=rca.recording, role=RoleChoices.MEMBER
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_recording_access_conflict_equal_role_keeps_single_access():
|
||||
"""RecordingAccess should keep one entry for the user when both users have the same role."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
rca1 = UserRecordingAccessFactory(user=user1, role=RoleChoices.MEMBER)
|
||||
UserRecordingAccessFactory(
|
||||
user=user2, recording=rca1.recording, role=RoleChoices.MEMBER
|
||||
)
|
||||
call_command("merge_duplicate_users")
|
||||
accesses = RecordingAccess.objects.filter(recording=rca1.recording)
|
||||
assert accesses.count() == 1
|
||||
assert accesses.first().user == user2
|
||||
assert accesses.first().role == RoleChoices.MEMBER
|
||||
|
||||
|
||||
# ── Files ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_reassigns_files_to_kept_user():
|
||||
"""Files should be reassigned to the kept user when stale user is merged."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
files = FileFactory.create_batch(3, creator=user1)
|
||||
call_command("merge_duplicate_users")
|
||||
for f in files:
|
||||
f.refresh_from_db()
|
||||
assert f.creator == user2
|
||||
|
||||
|
||||
def test_merge_kept_user_own_files_untouched():
|
||||
"""Files already owned by the kept user should remain unchanged after merge."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
FileFactory(creator=user1)
|
||||
kept_file = FileFactory(creator=user2)
|
||||
call_command("merge_duplicate_users")
|
||||
kept_file.refresh_from_db()
|
||||
assert kept_file.creator == user2
|
||||
|
||||
|
||||
# ── Dry-run ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_dry_run_does_not_delete_users():
|
||||
"""Command should not delete any users when dry-run is enabled."""
|
||||
UserFactory(email="dup@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
call_command("merge_duplicate_users", dry_run=True)
|
||||
assert User.objects.filter(email="dup@example.com").count() == 2
|
||||
|
||||
|
||||
def test_merge_dry_run_does_not_move_resource_access():
|
||||
"""Command should not move resource accesses when dry-run is enabled."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
ra = UserResourceAccessFactory(user=user1)
|
||||
call_command("merge_duplicate_users", dry_run=True)
|
||||
ra.refresh_from_db()
|
||||
assert ra.user == user1
|
||||
|
||||
|
||||
def test_merge_dry_run_does_not_move_recording_access():
|
||||
"""Command should not move recording accesses when dry-run is enabled."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
ra = UserRecordingAccessFactory(user=user1)
|
||||
call_command("merge_duplicate_users", dry_run=True)
|
||||
ra.refresh_from_db()
|
||||
assert ra.user == user1
|
||||
|
||||
|
||||
def test_merge_dry_run_does_not_move_files():
|
||||
"""Command should not reassign files when dry-run is enabled."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
f = FileFactory(creator=user1)
|
||||
call_command("merge_duplicate_users", dry_run=True)
|
||||
f.refresh_from_db()
|
||||
assert f.creator == user1
|
||||
|
||||
|
||||
# ── Isolation ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_non_duplicate_users_untouched():
|
||||
"""Non-duplicate users should remain untouched when other duplicates are merged."""
|
||||
unique = UserFactory(email="unique@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
call_command("merge_duplicate_users")
|
||||
assert User.objects.filter(id=unique.id).exists()
|
||||
|
||||
|
||||
def test_merge_non_duplicate_resource_access_untouched():
|
||||
"""ResourceAccess of non-duplicate users should remain untouched."""
|
||||
unique = UserFactory(email="unique@example.com")
|
||||
ra = UserResourceAccessFactory(user=unique)
|
||||
UserFactory(email="dup@example.com")
|
||||
UserFactory(email="dup@example.com")
|
||||
call_command("merge_duplicate_users")
|
||||
ra.refresh_from_db()
|
||||
assert ra.user == unique
|
||||
|
||||
|
||||
def test_merge_multiple_email_groups_all_merged():
|
||||
"""Command should merge all duplicate email groups in a single run."""
|
||||
for i in range(3):
|
||||
UserFactory(email=f"group{i}@example.com")
|
||||
UserFactory(email=f"group{i}@example.com")
|
||||
call_command("merge_duplicate_users")
|
||||
for i in range(3):
|
||||
assert User.objects.filter(email=f"group{i}@example.com").count() == 1
|
||||
assert User.objects.count() == 3
|
||||
|
||||
|
||||
# ── NULL / blank email guard ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_does_not_merge_users_with_null_email():
|
||||
"""Users with NULL email must never be merged together, even if multiple exist."""
|
||||
user1 = UserFactory(email=None)
|
||||
user2 = UserFactory(email=None)
|
||||
call_command("merge_duplicate_users")
|
||||
assert User.objects.filter(id=user1.id).exists()
|
||||
assert User.objects.filter(id=user2.id).exists()
|
||||
|
||||
|
||||
def test_merge_does_not_merge_users_with_blank_email():
|
||||
"""Users with empty-string email must never be merged together, even if multiple exist."""
|
||||
user1 = UserFactory(email="")
|
||||
user2 = UserFactory(email="")
|
||||
call_command("merge_duplicate_users")
|
||||
assert User.objects.filter(id=user1.id).exists()
|
||||
assert User.objects.filter(id=user2.id).exists()
|
||||
|
||||
|
||||
# ── Atomicity ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.management.commands.merge_duplicate_users.Command._merge_recording_accesses",
|
||||
side_effect=Exception("forced failure"),
|
||||
)
|
||||
def test_merge_is_atomic_rolls_back_all_on_any_failure(mock_reassign_files):
|
||||
"""Merge should be fully rolled back when any step fails."""
|
||||
user1 = UserFactory(email="dup@example.com")
|
||||
user2 = UserFactory(email="dup@example.com")
|
||||
resource_accesses = UserResourceAccessFactory.create_batch(3, user=user1)
|
||||
recording_accesses = UserRecordingAccessFactory.create_batch(3, user=user1)
|
||||
files = FileFactory.create_batch(3, creator=user1)
|
||||
|
||||
with pytest.raises(base.CommandError):
|
||||
call_command("merge_duplicate_users")
|
||||
|
||||
assert User.objects.filter(id=user1.id).exists()
|
||||
assert User.objects.filter(id=user2.id).exists()
|
||||
for ra in resource_accesses:
|
||||
ra.refresh_from_db()
|
||||
assert ra.user == user1
|
||||
for rca in recording_accesses:
|
||||
rca.refresh_from_db()
|
||||
assert rca.user == user1
|
||||
for f in files:
|
||||
f.refresh_from_db()
|
||||
assert f.creator == user1
|
||||
|
||||
|
||||
# ── Email filter ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_merge_email_filter_only_merges_matching_emails():
|
||||
"""Command should only merge users whose email matches the filter."""
|
||||
UserFactory(email="user1@example.com")
|
||||
UserFactory(email="user1@example.com")
|
||||
other1 = UserFactory(email="user1@other.com")
|
||||
other2 = UserFactory(email="user1@other.com")
|
||||
call_command("merge_duplicate_users", email_filter="@example.com")
|
||||
assert User.objects.filter(email="user1@example.com").count() == 1
|
||||
assert User.objects.filter(id=other1.id).exists()
|
||||
assert User.objects.filter(id=other2.id).exists()
|
||||
|
||||
|
||||
def test_merge_email_filter_no_match_does_nothing():
|
||||
"""Command should do nothing when the email filter matches no users."""
|
||||
UserFactory(email="user1@example.com")
|
||||
UserFactory(email="user1@example.com")
|
||||
call_command("merge_duplicate_users", email_filter="@nomatch.com")
|
||||
assert User.objects.filter(email="user1@example.com").count() == 2
|
||||
|
||||
|
||||
def test_merge_email_filter_is_case_insensitive():
|
||||
"""Command should match emails case-insensitively when filtering."""
|
||||
UserFactory(email="user1@Example.com")
|
||||
UserFactory(email="user1@Example.com")
|
||||
call_command("merge_duplicate_users", email_filter="@example.com")
|
||||
assert User.objects.filter(email="user1@Example.com").count() == 1
|
||||
@@ -18,13 +18,10 @@ from core.recording.event.exceptions import (
|
||||
)
|
||||
from core.recording.event.parsers import (
|
||||
MinioParser,
|
||||
S3Parser,
|
||||
StorageEvent,
|
||||
get_parser,
|
||||
)
|
||||
|
||||
# MinioParser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def valid_minio_event():
|
||||
@@ -50,7 +47,7 @@ def minio_parser():
|
||||
return MinioParser(bucket_name="test-bucket")
|
||||
|
||||
|
||||
def test_minio_parse_valid_event(minio_parser, valid_minio_event):
|
||||
def test_parse_valid_event(minio_parser, valid_minio_event):
|
||||
"""Test parsing a valid Minio event."""
|
||||
event = minio_parser.parse(valid_minio_event)
|
||||
assert isinstance(event, StorageEvent)
|
||||
@@ -60,33 +57,13 @@ def test_minio_parse_valid_event(minio_parser, valid_minio_event):
|
||||
assert event.metadata is None
|
||||
|
||||
|
||||
def test_minio_parse_with_video_type(minio_parser):
|
||||
"""Test parsing event with video file type."""
|
||||
video_event = {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": "46d1a121-2426-484d-8fb3-09b5d886f7a8.mp4",
|
||||
"contentType": "video/mp4",
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
event = minio_parser.parse(video_event)
|
||||
assert event.filetype == "video/mp4"
|
||||
assert event.filepath.endswith(".mp4")
|
||||
|
||||
|
||||
def test_minio_parse_empty_data(minio_parser):
|
||||
def test_parse_empty_data(minio_parser):
|
||||
"""Test parsing empty event data raises error."""
|
||||
with pytest.raises(ParsingEventDataError, match="Received empty data."):
|
||||
minio_parser.parse({})
|
||||
|
||||
|
||||
def test_minio_parse_missing_keys(minio_parser):
|
||||
def test_parse_missing_keys(minio_parser):
|
||||
"""Test parsing event with missing key."""
|
||||
|
||||
invalid_minio_event = {
|
||||
@@ -100,11 +77,11 @@ def test_minio_parse_missing_keys(minio_parser):
|
||||
]
|
||||
}
|
||||
|
||||
with pytest.raises(ParsingEventDataError, match="Malformed Minio event:"):
|
||||
with pytest.raises(ParsingEventDataError, match="Missing or malformed key"):
|
||||
minio_parser.parse(invalid_minio_event)
|
||||
|
||||
|
||||
def test_minio_parse_none_key(minio_parser):
|
||||
def test_parse_none_key(minio_parser):
|
||||
"""Test parsing event with None field."""
|
||||
|
||||
invalid_minio_event = {
|
||||
@@ -125,7 +102,7 @@ def test_minio_parse_none_key(minio_parser):
|
||||
minio_parser.parse(invalid_minio_event)
|
||||
|
||||
|
||||
def test_minio_validate_invalid_bucket(minio_parser):
|
||||
def test_validate_invalid_bucket(minio_parser):
|
||||
"""Test validation with wrong bucket name."""
|
||||
event = StorageEvent(
|
||||
filepath="recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
@@ -137,7 +114,7 @@ def test_minio_validate_invalid_bucket(minio_parser):
|
||||
minio_parser.validate(event)
|
||||
|
||||
|
||||
def test_minio_validate_invalid_filetype(minio_parser):
|
||||
def test_validate_invalid_filetype(minio_parser):
|
||||
"""Test validation with unsupported file type."""
|
||||
event = StorageEvent(
|
||||
filepath="recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.txt",
|
||||
@@ -162,7 +139,7 @@ def test_minio_validate_invalid_filetype(minio_parser):
|
||||
"folder%2Fuploads%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # nested but no recordings/
|
||||
],
|
||||
)
|
||||
def test_minio_validate_invalid_filepath(invalid_filepath, minio_parser):
|
||||
def test_validate_invalid_filepath(invalid_filepath, minio_parser):
|
||||
"""Test validation with malformed filepath."""
|
||||
event = StorageEvent(
|
||||
filepath=invalid_filepath,
|
||||
@@ -174,7 +151,7 @@ def test_minio_validate_invalid_filepath(invalid_filepath, minio_parser):
|
||||
minio_parser.validate(event)
|
||||
|
||||
|
||||
def test_minio_validate_valid_event(minio_parser):
|
||||
def test_validate_valid_event(minio_parser):
|
||||
"""Test validation with valid event data."""
|
||||
event = StorageEvent(
|
||||
filepath="recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
@@ -186,13 +163,13 @@ def test_minio_validate_valid_event(minio_parser):
|
||||
assert recording_id == "46d1a121-2426-484d-8fb3-09b5d886f7a8"
|
||||
|
||||
|
||||
def test_minio_get_recording_id_success(minio_parser, valid_minio_event):
|
||||
def test_get_recording_id_success(minio_parser, valid_minio_event):
|
||||
"""Test successful extraction of recording ID."""
|
||||
recording_id = minio_parser.get_recording_id(valid_minio_event)
|
||||
assert recording_id == "46d1a121-2426-484d-8fb3-09b5d886f7a8"
|
||||
|
||||
|
||||
def test_minio_validate_filepath_with_folder(minio_parser):
|
||||
def test_validate_filepath_with_folder(minio_parser):
|
||||
"""Test validation of filepath with folder structure."""
|
||||
event = StorageEvent(
|
||||
filepath="parent_folder%2Frecordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
@@ -204,21 +181,41 @@ def test_minio_validate_filepath_with_folder(minio_parser):
|
||||
assert recording_id == "46d1a121-2426-484d-8fb3-09b5d886f7a8"
|
||||
|
||||
|
||||
def test_minio_empty_allowed_filetypes():
|
||||
def test_parse_with_video_type(minio_parser):
|
||||
"""Test parsing event with video file type."""
|
||||
video_event = {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": "46d1a121-2426-484d-8fb3-09b5d886f7a8.mp4",
|
||||
"contentType": "video/mp4",
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
event = minio_parser.parse(video_event)
|
||||
assert event.filetype == "video/mp4"
|
||||
assert event.filepath.endswith(".mp4")
|
||||
|
||||
|
||||
def test_empty_allowed_filetypes():
|
||||
"""Test MinioParser with empty allowed_filetypes."""
|
||||
empty_types = set()
|
||||
parser = MinioParser(bucket_name="test-bucket", allowed_filetypes=empty_types)
|
||||
assert parser._allowed_filetypes == {"audio/ogg", "video/mp4"}
|
||||
|
||||
|
||||
def test_minio_custom_allowed_filetypes():
|
||||
def test_custom_allowed_filetypes():
|
||||
"""Test MinioParser with empty allowed_filetypes."""
|
||||
custom_types = {"audio/mp3", "video/mov"}
|
||||
parser = MinioParser(bucket_name="test-bucket", allowed_filetypes=custom_types)
|
||||
assert parser._allowed_filetypes == {"audio/mp3", "video/mov"}
|
||||
|
||||
|
||||
def test_minio_validate_custom_filetypes():
|
||||
def test_validate_custom_filetypes():
|
||||
"""Test validation of filepath with folder structure."""
|
||||
|
||||
parser = MinioParser(bucket_name="test-bucket", allowed_filetypes={"audio/mp3"})
|
||||
@@ -232,143 +229,18 @@ def test_minio_validate_custom_filetypes():
|
||||
parser.validate(event)
|
||||
|
||||
|
||||
def test_minio_constructor_none_bucket():
|
||||
def test_constructor_none_bucket():
|
||||
"""Test MinioParser constructor with None bucket name."""
|
||||
with pytest.raises(ValueError, match="Bucket name cannot be None or empty"):
|
||||
MinioParser(bucket_name=None)
|
||||
|
||||
|
||||
def test_minio_constructor_empty_bucket():
|
||||
def test_constructor_empty_bucket():
|
||||
"""Test MinioParser constructor with empty bucket name."""
|
||||
with pytest.raises(ValueError, match="Bucket name cannot be None or empty"):
|
||||
MinioParser(bucket_name="")
|
||||
|
||||
|
||||
# S3Parser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def valid_s3_event():
|
||||
"""Mock a valid S3 event."""
|
||||
return {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_parser():
|
||||
"""Mock an S3 parser."""
|
||||
return S3Parser(bucket_name="test-bucket")
|
||||
|
||||
|
||||
def test_s3_parse_valid_event(s3_parser, valid_s3_event):
|
||||
"""Test parsing a valid S3 event."""
|
||||
event = s3_parser.parse(valid_s3_event)
|
||||
assert isinstance(event, StorageEvent)
|
||||
assert event.filepath == "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg"
|
||||
assert event.filetype == "audio/ogg"
|
||||
assert event.bucket_name == "test-bucket"
|
||||
assert event.metadata is None
|
||||
|
||||
|
||||
def test_s3_parse_empty_data(s3_parser):
|
||||
"""Test parsing empty S3 event data raises error."""
|
||||
with pytest.raises(ParsingEventDataError, match="Received empty data."):
|
||||
s3_parser.parse({})
|
||||
|
||||
|
||||
def test_s3_parse_missing_keys(s3_parser):
|
||||
"""Test parsing S3 event with missing key."""
|
||||
invalid_s3_event = {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
# Missing 'object' key
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with pytest.raises(ParsingEventDataError, match="Malformed S3 event:"):
|
||||
s3_parser.parse(invalid_s3_event)
|
||||
|
||||
|
||||
def test_s3_parse_none_key(s3_parser):
|
||||
"""Test parsing S3 event with None field."""
|
||||
invalid_s3_event = {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": None,
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with pytest.raises(ParsingEventDataError, match="Missing object key name"):
|
||||
s3_parser.parse(invalid_s3_event)
|
||||
|
||||
|
||||
def test_s3_parse_with_video_type(s3_parser):
|
||||
"""Test parsing S3 event with mp4 file extension."""
|
||||
video_event = {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.mp4",
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
event = s3_parser.parse(video_event)
|
||||
assert event.filetype == "video/mp4"
|
||||
assert event.filepath.endswith(".mp4")
|
||||
|
||||
|
||||
def test_s3_parse_unrecognized_extension(s3_parser):
|
||||
"""Test parsing S3 event with unrecognized file extension."""
|
||||
event_with_unknown_ext = {
|
||||
"Records": [
|
||||
{
|
||||
"s3": {
|
||||
"bucket": {"name": "test-bucket"},
|
||||
"object": {
|
||||
"key": "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.zzunknown999",
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with pytest.raises(TypeError, match="filetype cannot be None"):
|
||||
s3_parser.parse(event_with_unknown_ext)
|
||||
|
||||
|
||||
def test_s3_get_recording_id_success(s3_parser, valid_s3_event):
|
||||
"""Test successful extraction of recording ID from S3 event."""
|
||||
recording_id = s3_parser.get_recording_id(valid_s3_event)
|
||||
assert recording_id == "46d1a121-2426-484d-8fb3-09b5d886f7a8"
|
||||
|
||||
|
||||
# get_parser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_lru_cache():
|
||||
"""Fixture to clear the LRU cache between tests."""
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Test recordings API endpoints: external process hook.
|
||||
"""
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument
|
||||
|
||||
import pytest
|
||||
|
||||
from ...factories import RecordingFactory
|
||||
from ...models import RecordingStatusChoices
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def external_process_settings(settings):
|
||||
"""Configure authentication token for the external process webhook."""
|
||||
settings.SUMMARY_SERVICE_WEBHOOK_API_TOKEN = "testWebhookToken"
|
||||
return settings
|
||||
|
||||
|
||||
def test_external_process_event_missing_authorization_header(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""Requests without authorization must be rejected."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-1", "type": "transcript", "status": "success"},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_external_process_event_wrong_bearer_token(external_process_settings, client):
|
||||
"""Requests with invalid bearer token must be rejected."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-1", "type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer wrongToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_external_process_event_missing_job_id(external_process_settings, client):
|
||||
"""Payload without job_id must fail validation."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "No job_id provided" in str(response.json())
|
||||
|
||||
|
||||
def test_external_process_event_success_updates_recording_status(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""A successful transcript process should update recording status."""
|
||||
recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.SAVED,
|
||||
external_process_id="job-123",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-123", "type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.EXTERNAL_PROCESS_SUCCESSFUL
|
||||
|
||||
|
||||
def test_external_process_event_failure_updates_recording_status(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""A failing transcript process should update recording status."""
|
||||
recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.SAVED,
|
||||
external_process_id="job-456",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-456", "type": "transcript", "status": "failure"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.EXTERNAL_PROCESS_FAILED
|
||||
|
||||
|
||||
def test_external_process_event_unknown_recording_is_ignored(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""Unknown job_id should not fail the webhook processing."""
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "missing-job", "type": "transcript", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
|
||||
def test_external_process_event_non_transcript_event_does_not_change_status(
|
||||
external_process_settings, client
|
||||
):
|
||||
"""Only transcript events should update recording status."""
|
||||
recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.SAVED,
|
||||
external_process_id="job-789",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/external-process-hook/",
|
||||
{"job_id": "job-789", "type": "thumbnail", "status": "success"},
|
||||
HTTP_AUTHORIZATION="Bearer testWebhookToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.SAVED
|
||||
@@ -776,7 +776,7 @@ def test_update_participant_invalid_permission():
|
||||
"loc": ["invalid-attributes"],
|
||||
"msg": "Extra inputs are not permitted",
|
||||
"input": "True",
|
||||
"url": "https://errors.pydantic.dev/2.13/v/extra_forbidden",
|
||||
"url": "https://errors.pydantic.dev/2.12/v/extra_forbidden",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ Test suite for generated openapi schema.
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import Client
|
||||
@@ -34,26 +33,10 @@ def test_openapi_client_schema():
|
||||
)
|
||||
assert output.getvalue() == ""
|
||||
|
||||
response = Client().get("/api/v1.0/swagger.json")
|
||||
response = Client().get("/v1.0/swagger.json")
|
||||
|
||||
assert response.status_code == 200
|
||||
with open(
|
||||
"core/tests/swagger/swagger.json", "r", encoding="utf-8"
|
||||
) as expected_schema:
|
||||
assert response.json() == json.load(expected_schema)
|
||||
|
||||
|
||||
@patch(
|
||||
"django.contrib.staticfiles.storage.staticfiles_storage.url",
|
||||
side_effect=lambda name: f"/static/{name}",
|
||||
)
|
||||
# pylint: disable=unused-argument
|
||||
def test_openapi_documentation_routes(mock_staticfiles):
|
||||
"""Swagger and ReDoc documentation should be served on canonical URLs."""
|
||||
client = Client()
|
||||
|
||||
swagger_response = client.get("/api/v1.0/swagger/")
|
||||
redoc_response = client.get("/api/v1.0/redoc/")
|
||||
|
||||
assert swagger_response.status_code == 200
|
||||
assert redoc_response.status_code == 200
|
||||
|
||||
@@ -250,112 +250,6 @@ def test_api_rooms_list_filters_by_user():
|
||||
assert str(room2.id) not in returned_ids
|
||||
|
||||
|
||||
def test_api_rooms_list_access_level_in_results():
|
||||
"""Rooms should include the correct access_level for each room."""
|
||||
user = UserFactory()
|
||||
room_trusted = RoomFactory(
|
||||
users=[(user, RoleChoices.OWNER)], access_level=RoomAccessLevel.TRUSTED
|
||||
)
|
||||
room_restricted = RoomFactory(
|
||||
users=[(user, RoleChoices.OWNER)], access_level=RoomAccessLevel.RESTRICTED
|
||||
)
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = {r["id"]: r for r in response.data["results"]}
|
||||
assert results[str(room_trusted.id)]["access_level"] == RoomAccessLevel.TRUSTED
|
||||
assert (
|
||||
results[str(room_restricted.id)]["access_level"] == RoomAccessLevel.RESTRICTED
|
||||
)
|
||||
|
||||
|
||||
def test_api_rooms_list_does_not_expose_sensitive_fields():
|
||||
"""Rooms should not expose pin_code or accesses."""
|
||||
user = UserFactory()
|
||||
RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.data["results"][0]
|
||||
assert "pin_code" not in result
|
||||
assert "accesses" not in result
|
||||
assert "livekit" not in result
|
||||
|
||||
|
||||
def test_api_rooms_list_expected_fields(settings):
|
||||
"""Rooms should expose exactly the expected fields."""
|
||||
|
||||
settings.APPLICATION_BASE_URL = "https://example.com"
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
|
||||
user = UserFactory()
|
||||
RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert set(response.data["results"][0].keys()) == {
|
||||
"id",
|
||||
"name",
|
||||
"slug",
|
||||
"access_level",
|
||||
"configuration",
|
||||
"telephony",
|
||||
"url",
|
||||
}
|
||||
|
||||
|
||||
def test_api_rooms_list_expected_fields_without_telephony(settings):
|
||||
"""Rooms shouldn't expose telephony related fields when disabled."""
|
||||
|
||||
settings.APPLICATION_BASE_URL = "https://example.com"
|
||||
settings.ROOM_TELEPHONY_ENABLED = False
|
||||
|
||||
user = UserFactory()
|
||||
RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "telephony" not in set(response.data["results"][0].keys())
|
||||
|
||||
|
||||
def test_api_rooms_list_expected_fields_missing_base_url(settings):
|
||||
"""Rooms shouldn't expose URL field when the application base url is missing."""
|
||||
|
||||
settings.APPLICATION_BASE_URL = None
|
||||
|
||||
user = UserFactory()
|
||||
RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_LIST])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "url" not in set(response.data["results"][0].keys())
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_requires_authentication():
|
||||
"""Retrieving rooms without authentication should return 401."""
|
||||
|
||||
@@ -489,7 +383,6 @@ def test_api_rooms_retrieve_success(settings):
|
||||
"name": room.name,
|
||||
"slug": room.slug,
|
||||
"access_level": str(room.access_level),
|
||||
"configuration": room.configuration,
|
||||
"url": f"http://your-application.com/{room.slug}",
|
||||
"telephony": {
|
||||
"enabled": True,
|
||||
@@ -672,40 +565,11 @@ def test_api_rooms_create_success():
|
||||
assert "slug" in response.data
|
||||
assert "name" in response.data
|
||||
assert response.data["name"] == response.data["slug"]
|
||||
assert response.data["configuration"] == {}
|
||||
|
||||
# Verify room was created with user as owner
|
||||
room = Room.objects.get(id=response.data["id"])
|
||||
assert room.get_role(user) == RoleChoices.OWNER
|
||||
assert room.access_level == "trusted"
|
||||
assert room.configuration == {}
|
||||
|
||||
|
||||
def test_api_rooms_create_with_configuration_success():
|
||||
"""Creating a room with a validated configuration should succeed."""
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
token = generate_test_token(
|
||||
user, [ApplicationScope.ROOMS_CREATE, ApplicationScope.ROOMS_LIST]
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{
|
||||
"access_level": RoomAccessLevel.RESTRICTED,
|
||||
"configuration": {"can_publish_sources": ["camera"]},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
room = Room.objects.get(id=response.data["id"])
|
||||
assert room.access_level == RoomAccessLevel.RESTRICTED
|
||||
assert room.configuration == {"can_publish_sources": ["camera"]}
|
||||
assert response.data["configuration"] == {"can_publish_sources": ["camera"]}
|
||||
|
||||
|
||||
def test_api_rooms_create_readonly_enforcement():
|
||||
@@ -723,6 +587,7 @@ def test_api_rooms_create_readonly_enforcement():
|
||||
"id": "fake-id",
|
||||
"slug": "fake-slug",
|
||||
"name": "fake-name",
|
||||
"access_level": "public",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -734,150 +599,41 @@ def test_api_rooms_create_readonly_enforcement():
|
||||
assert response.data["slug"] != "fake-slug"
|
||||
assert "id" in response.data
|
||||
assert response.data["name"] != "fake-name"
|
||||
assert response.data["configuration"] == {}
|
||||
|
||||
# Verify room was created with user as owner
|
||||
room = Room.objects.get(id=response.data["id"])
|
||||
assert room.get_role(user) == RoleChoices.OWNER
|
||||
assert room.access_level == "trusted"
|
||||
assert room.configuration == {}
|
||||
|
||||
|
||||
def test_api_rooms_create_rejects_invalid_configuration():
|
||||
"""Creating a room with unsupported configuration keys should fail."""
|
||||
def test_api_rooms_unknown_actions():
|
||||
"""Updating or deleting a room are not supported yet."""
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(
|
||||
user,
|
||||
[
|
||||
ApplicationScope.ROOMS_RETRIEVE,
|
||||
ApplicationScope.ROOMS_DELETE,
|
||||
ApplicationScope.ROOMS_UPDATE,
|
||||
],
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{
|
||||
"configuration": {
|
||||
"unsupported_flag": True,
|
||||
}
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "extra inputs are not permitted" in str(response.data).lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_configuration",
|
||||
[
|
||||
{"can_publish_sources": ["invalid-source"]},
|
||||
{"everyone_can_mute": "invalid-value"},
|
||||
],
|
||||
)
|
||||
def test_api_rooms_create_rejects_invalid_configuration_values(invalid_configuration):
|
||||
"""Creating a room with invalid configuration values should fail."""
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
assert response.status_code == 405
|
||||
assert 'method "delete" not allowed.' in str(response.data).lower()
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{"configuration": invalid_configuration},
|
||||
format="json",
|
||||
)
|
||||
response = client.patch(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_api_rooms_create_public_access_disabled_by_default():
|
||||
"""Public rooms should be disabled for the external API by default."""
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{"access_level": RoomAccessLevel.PUBLIC},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "public rooms are disabled" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_public_access_enabled_with_settings(settings):
|
||||
"""Public rooms should be creatable when explicitly enabled."""
|
||||
|
||||
settings.EXTERNAL_API_ALLOW_PUBLIC_ACCESS = True
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{"access_level": RoomAccessLevel.PUBLIC},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
room = Room.objects.get(id=response.data["id"])
|
||||
assert room.access_level == RoomAccessLevel.PUBLIC
|
||||
assert response.data["access_level"] == RoomAccessLevel.PUBLIC
|
||||
|
||||
|
||||
def test_api_rooms_create_default_access_level_respects_settings(settings):
|
||||
"""Room creation should reflect the EXTERNAL_API_DEFAULT_ACCESS_LEVEL setting."""
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.data["access_level"] == RoomAccessLevel.TRUSTED
|
||||
|
||||
settings.EXTERNAL_API_DEFAULT_ACCESS_LEVEL = "public"
|
||||
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.data["access_level"] == RoomAccessLevel.PUBLIC
|
||||
|
||||
|
||||
def test_api_rooms_create_public_access_level_when_default_is_public(settings):
|
||||
"""Explicit public access_level is accepted when the default is already public."""
|
||||
settings.EXTERNAL_API_ALLOW_PUBLIC_ACCESS = False
|
||||
settings.EXTERNAL_API_DEFAULT_ACCESS_LEVEL = "public"
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_CREATE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
|
||||
# No access_level in body — default kicks in, public room is created.
|
||||
response = client.post("/external-api/v1.0/rooms/", {}, format="json")
|
||||
assert response.status_code == 201
|
||||
assert response.data["access_level"] == RoomAccessLevel.PUBLIC
|
||||
|
||||
# Explicit access_level=public in body — still rejected.
|
||||
response = client.post(
|
||||
"/external-api/v1.0/rooms/",
|
||||
{"access_level": RoomAccessLevel.PUBLIC},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.data["access_level"] == RoomAccessLevel.PUBLIC
|
||||
assert response.status_code == 405
|
||||
assert 'method "patch" not allowed.' in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_response_no_url(settings):
|
||||
|
||||
@@ -4,8 +4,6 @@ Tests for external API /token endpoint
|
||||
|
||||
# pylint: disable=W0621
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
@@ -17,7 +15,6 @@ from core.factories import (
|
||||
UserFactory,
|
||||
)
|
||||
from core.models import ApplicationScope, User
|
||||
from core.services import provisional_user_service
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -439,88 +436,3 @@ def test_api_applications_token_existing_user(settings):
|
||||
"delegated": True,
|
||||
"scope": "rooms:list rooms:create",
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(provisional_user_service.ProvisionalUserService, "_get_by_email")
|
||||
def test_api_applications_token_new_user_race_condition(mock_get_by_email, settings):
|
||||
"""Should handle race condition where two concurrent requests create the same user."""
|
||||
settings.APPLICATION_ALLOW_USER_CREATION = True
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
settings.OIDC_USER_SUB_FIELD_IMMUTABLE = False
|
||||
|
||||
application = ApplicationFactory(
|
||||
is_active=True, scopes=[ApplicationScope.ROOMS_LIST]
|
||||
)
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
application.save()
|
||||
|
||||
email = "john.doe@example.com"
|
||||
|
||||
# First call: lie and say user doesn't exist, simulating the race window
|
||||
# Second call (recovery path): return the real user
|
||||
existing_user = UserFactory(sub=None, email=email)
|
||||
mock_get_by_email.side_effect = [None, existing_user]
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": application.client_id,
|
||||
"client_secret": plain_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert mock_get_by_email.call_count == 2
|
||||
|
||||
token = response.data["access_token"]
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithms=[settings.APPLICATION_JWT_ALG],
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
)
|
||||
assert payload["user_id"] == str(existing_user.id)
|
||||
assert User.objects.filter(email=email).count() == 1
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
provisional_user_service.ProvisionalUserService,
|
||||
"get_or_create",
|
||||
side_effect=provisional_user_service.ProvisionalUserIntegrityError,
|
||||
)
|
||||
def test_api_applications_token_new_user_race_condition_unrecoverable(
|
||||
mock_get_or_create, settings
|
||||
):
|
||||
"""Should return 500 when ProvisionalUserIntegrityError is raised."""
|
||||
|
||||
settings.APPLICATION_ALLOW_USER_CREATION = True
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
settings.OIDC_USER_SUB_FIELD_IMMUTABLE = False
|
||||
|
||||
application = ApplicationFactory(
|
||||
is_active=True, scopes=[ApplicationScope.ROOMS_LIST]
|
||||
)
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
application.save()
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": application.client_id,
|
||||
"client_secret": plain_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": "john.doe@example.com",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
assert mock_get_or_create.call_count == 1
|
||||
|
||||
@@ -44,73 +44,3 @@ def test_models_users_send_mail_main_missing():
|
||||
user.email_user("my subject", "my message")
|
||||
|
||||
assert str(excinfo.value) == "User has no email address."
|
||||
|
||||
|
||||
def test_models_users_email_unique_when_sub_is_null():
|
||||
"""Email should be unique among users with no sub (pending users)."""
|
||||
user = factories.UserFactory(sub=None, email="test@example.com")
|
||||
with pytest.raises(
|
||||
ValidationError, match="Constraint “unique_email_when_sub_is_null” is violated."
|
||||
):
|
||||
factories.UserFactory(sub=None, email=user.email)
|
||||
|
||||
|
||||
def test_models_users_email_unique_case_insensitive_when_sub_is_null():
|
||||
"""Email uniqueness should be case-insensitive among users with no sub (pending users)."""
|
||||
factories.UserFactory(sub=None, email="Test@example.com")
|
||||
with pytest.raises(
|
||||
ValidationError, match="Constraint “unique_email_when_sub_is_null” is violated."
|
||||
):
|
||||
factories.UserFactory(sub=None, email="test@example.com")
|
||||
|
||||
|
||||
def test_models_users_email_not_unique_when_sub_is_set():
|
||||
"""Email uniqueness should not be enforced when users have a sub."""
|
||||
user = factories.UserFactory(sub="sub-1", email="test@example.com")
|
||||
user2 = factories.UserFactory(sub="sub-2", email=user.email)
|
||||
assert user2.email == user.email
|
||||
|
||||
|
||||
def test_models_users_email_not_unique_between_sub_null_and_sub_set():
|
||||
"""A user with a sub and a pending user (sub=None) can share the same email."""
|
||||
user = factories.UserFactory(sub="sub-1", email="test@example.com")
|
||||
user2 = factories.UserFactory(sub=None, email=user.email)
|
||||
assert user2.email == user.email
|
||||
|
||||
|
||||
def test_models_users_email_unique_constraint_allows_multiple_null_emails():
|
||||
"""Multiple users with sub=None and email=None should be allowed."""
|
||||
factories.UserFactory(sub=None, email=None)
|
||||
factories.UserFactory(sub=None, email=None)
|
||||
|
||||
|
||||
def test_models_users_sub_null_email_null_does_not_prevent_creation():
|
||||
"""Multiple pending users (sub=None, email=None) can be created without conflict.
|
||||
|
||||
sub=None is not unique-constrained. email uniqueness is only enforced among
|
||||
sub=None users with a non-null email, so email=None bypasses it (NULL != NULL in SQL).
|
||||
"""
|
||||
# Ghost row can still appear from bad code path
|
||||
u1 = factories.UserFactory(sub=None, email=None)
|
||||
u2 = factories.UserFactory(sub=None, email=None)
|
||||
assert u1.pk != u2.pk
|
||||
|
||||
|
||||
def test_models_users_sub_can_be_null():
|
||||
"""sub is nullable: pending users exist before OIDC activation."""
|
||||
user = factories.UserFactory(sub=None)
|
||||
user.refresh_from_db()
|
||||
assert user.sub is None
|
||||
|
||||
|
||||
def test_models_users_sub_null_does_not_prevent_creation():
|
||||
"""Multiple users can be created with sub=None (pending state)."""
|
||||
u1 = factories.UserFactory(sub=None)
|
||||
u2 = factories.UserFactory(sub=None)
|
||||
assert u1.pk != u2.pk
|
||||
|
||||
|
||||
def test_models_users_sub_blank_is_accepted():
|
||||
"""sub='' passes validation because blank=True; null is preferred but not enforced."""
|
||||
user = factories.UserFactory.build(sub="")
|
||||
user.full_clean()
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"""
|
||||
Test utils.build_telephony_config
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from core.utils import build_telephony_config
|
||||
|
||||
|
||||
def test_build_telephony_config_disabled(settings):
|
||||
"""Returns {"enabled": False} when telephony is disabled."""
|
||||
settings.ROOM_TELEPHONY_ENABLED = False
|
||||
config = build_telephony_config()
|
||||
assert config == {"enabled": False}
|
||||
|
||||
|
||||
def test_build_telephony_config_enabled_with_valid_number(settings):
|
||||
"""Returns full config with country and international number when telephony is enabled."""
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER = "0123456789"
|
||||
settings.ROOM_TELEPHONY_DEFAULT_COUNTRY = "FR"
|
||||
config = build_telephony_config()
|
||||
assert config == {
|
||||
"enabled": True,
|
||||
"default_country": "FR",
|
||||
"international_phone_number": "+33 1 23 45 67 89",
|
||||
}
|
||||
|
||||
|
||||
def test_build_telephony_config_enabled_with_invalid_number(settings):
|
||||
"""Returns {"enabled": False} when phone number cannot be parsed."""
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER = "not-a-number"
|
||||
settings.ROOM_TELEPHONY_DEFAULT_COUNTRY = "FR"
|
||||
config = build_telephony_config()
|
||||
assert config == {"enabled": False}
|
||||
|
||||
|
||||
def test_build_telephony_config_enabled_with_missing_number(settings):
|
||||
"""Returns {"enabled": False} when phone number is not configured."""
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER = ""
|
||||
settings.ROOM_TELEPHONY_DEFAULT_COUNTRY = "FR"
|
||||
config = build_telephony_config()
|
||||
assert config == {"enabled": False}
|
||||
|
||||
|
||||
def test_build_telephony_config_enabled_with_missing_number_warns(settings, caplog):
|
||||
"""Logs a warning when telephony is enabled but phone number is not configured."""
|
||||
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER = ""
|
||||
settings.ROOM_TELEPHONY_DEFAULT_COUNTRY = "FR"
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
build_telephony_config()
|
||||
|
||||
assert "ROOM_TELEPHONY_PHONE_NUMBER" in caplog.text
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
Test utils._format_telephony_phone_number
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from core.utils import _format_telephony_phone_number
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_lru_cache():
|
||||
"""Clear the lru_cache before each test to ensure isolation."""
|
||||
_format_telephony_phone_number.cache_clear()
|
||||
yield
|
||||
_format_telephony_phone_number.cache_clear()
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_missing_raw_number():
|
||||
"""Returns (None, None) when raw_number is empty."""
|
||||
country, international = _format_telephony_phone_number("", "FR")
|
||||
assert country is None
|
||||
assert international is None
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_none_raw_number():
|
||||
"""Returns (None, None) when raw_number is None."""
|
||||
country, international = _format_telephony_phone_number(None, "FR")
|
||||
assert country is None
|
||||
assert international is None
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_missing_default_country():
|
||||
"""Returns (None, None) when default_country is empty."""
|
||||
country, international = _format_telephony_phone_number("+33123456789", "")
|
||||
assert country is None
|
||||
assert international is None
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_none_default_country():
|
||||
"""Returns (None, None) when default_country is None."""
|
||||
country, international = _format_telephony_phone_number("+33123456789", None)
|
||||
assert country is None
|
||||
assert international is None
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_both_missing():
|
||||
"""Returns (None, None) when both inputs are missing."""
|
||||
country, international = _format_telephony_phone_number(None, None)
|
||||
assert country is None
|
||||
assert international is None
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_invalid_number(caplog):
|
||||
"""Returns (None, None) and logs a warning when the number cannot be parsed."""
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
country, international = _format_telephony_phone_number("not-a-number", "FR")
|
||||
|
||||
assert country is None
|
||||
assert international is None
|
||||
assert "not-a-number" in caplog.text
|
||||
assert "FR" in caplog.text
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_valid_french_number():
|
||||
"""Returns correct country and international format for a valid French number."""
|
||||
country, international = _format_telephony_phone_number("0123456789", "FR")
|
||||
assert country == "FR"
|
||||
assert international == "+33 1 23 45 67 89"
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_valid_e164_number():
|
||||
"""Returns correct result for an E.164-formatted number (no default country needed)."""
|
||||
country, international = _format_telephony_phone_number("+33123456789", "US")
|
||||
assert country == "FR"
|
||||
assert international == "+33 1 23 45 67 89"
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_valid_us_number():
|
||||
"""Returns correct country and international format for a valid US number."""
|
||||
country, international = _format_telephony_phone_number("2025550123", "US")
|
||||
assert country == "US"
|
||||
assert international == "+1 202-555-0123"
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_valid_german_number():
|
||||
"""Returns correct country and international format for a valid German number."""
|
||||
country, international = _format_telephony_phone_number("03012345678", "DE")
|
||||
assert country == "DE"
|
||||
assert international == "+49 30 12345678"
|
||||
|
||||
|
||||
def test_format_telephony_phone_number_lru_cache():
|
||||
"""Results are cached: the same inputs return the same object."""
|
||||
result1 = _format_telephony_phone_number("0123456789", "FR")
|
||||
result2 = _format_telephony_phone_number("0123456789", "FR")
|
||||
assert result1 is result2
|
||||
# pylint: disable=no-value-for-parameter
|
||||
cache_info = _format_telephony_phone_number.cache_info()
|
||||
assert cache_info.hits >= 1
|
||||
@@ -12,7 +12,6 @@ import mimetypes
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
from functools import lru_cache
|
||||
from typing import List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -23,7 +22,6 @@ import aiohttp
|
||||
import boto3
|
||||
import botocore
|
||||
import magic
|
||||
import phonenumbers
|
||||
from asgiref.sync import async_to_sync
|
||||
from livekit.api import ( # pylint: disable=E0611
|
||||
AccessToken,
|
||||
@@ -426,7 +424,7 @@ def generate_upload_policy(file):
|
||||
Originally taken from https://github.com/suitenumerique/drive/blob/564822d31f071c6dfacd112ef4b7146c73077cd9/src/backend/core/api/utils.py#L102 # pylint: disable=line-too-long
|
||||
"""
|
||||
|
||||
key = file.temporary_file_key
|
||||
key = file.file_key
|
||||
|
||||
# This settings should be used if the backend application and the frontend application
|
||||
# can't connect to the object storage with the same domain. This is the case in the
|
||||
@@ -459,14 +457,14 @@ def generate_upload_policy(file):
|
||||
return policy
|
||||
|
||||
|
||||
def generate_download_file_url(file, *, expires_in: int, override_domain: bool = True):
|
||||
def generate_download_s3_file_url(
|
||||
key, *, expires_in: int, override_domain: bool = True
|
||||
):
|
||||
"""
|
||||
Generate a S3 signed download url for a given file.
|
||||
Generate a S3 signed download url for a given key.
|
||||
"""
|
||||
|
||||
key = file.file_key
|
||||
|
||||
# This setting should be used if the backend application and the frontend application
|
||||
# This settings should be used if the backend application and the frontend application
|
||||
# can't connect to the object storage with the same domain. This is the case in the
|
||||
# docker compose stack used in development. The frontend application will use localhost
|
||||
# to connect to the object storage while the backend application will use the object storage
|
||||
@@ -492,58 +490,3 @@ def generate_download_file_url(file, *, expires_in: int, override_domain: bool =
|
||||
Params={"Bucket": default_storage.bucket_name, "Key": key},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _format_telephony_phone_number(raw_number, default_country):
|
||||
"""Parse a configured phone number and return (country, international_format).
|
||||
|
||||
Returns (None, None) if the inputs are missing or the number cannot be
|
||||
parsed. Logs a warning on parse failure so operators see the misconfiguration.
|
||||
"""
|
||||
if not raw_number or not default_country:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
parsed = phonenumbers.parse(raw_number, default_country)
|
||||
except phonenumbers.NumberParseException:
|
||||
logger.warning(
|
||||
"ROOM_TELEPHONY_PHONE_NUMBER %r is not a valid phone number for "
|
||||
"default country %r; telephony block will be returned without "
|
||||
"formatted number.",
|
||||
raw_number,
|
||||
default_country,
|
||||
)
|
||||
return None, None
|
||||
|
||||
country = phonenumbers.region_code_for_number(parsed)
|
||||
international = phonenumbers.format_number(
|
||||
parsed, phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
||||
)
|
||||
return country, international
|
||||
|
||||
|
||||
def build_telephony_config():
|
||||
"""Build the telephony block of the frontend configuration."""
|
||||
if not settings.ROOM_TELEPHONY_ENABLED:
|
||||
return {"enabled": False}
|
||||
|
||||
country, international = _format_telephony_phone_number(
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER,
|
||||
settings.ROOM_TELEPHONY_DEFAULT_COUNTRY,
|
||||
)
|
||||
|
||||
if international is None:
|
||||
logger.warning(
|
||||
"Telephony is enabled but ROOM_TELEPHONY_PHONE_NUMBER %r with "
|
||||
"default country %r could not be formatted; telephony will be disabled.",
|
||||
settings.ROOM_TELEPHONY_PHONE_NUMBER,
|
||||
settings.ROOM_TELEPHONY_DEFAULT_COUNTRY,
|
||||
)
|
||||
return {"enabled": False}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"default_country": country,
|
||||
"international_phone_number": international,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-21 14:09+0000\n"
|
||||
"POT-Creation-Date: 2026-05-26 16:20+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -88,17 +88,17 @@ msgstr "Scopes"
|
||||
msgid "Creator is me"
|
||||
msgstr "Ersteller bin ich"
|
||||
|
||||
#: core/api/serializers.py:88
|
||||
#: core/api/serializers.py:89
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
||||
"hinzuzufügen."
|
||||
|
||||
#: core/api/serializers.py:509
|
||||
#: core/api/serializers.py:534
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "Diese Dateiendung ist nicht erlaubt."
|
||||
|
||||
#: core/api/viewsets.py:1090
|
||||
#: core/api/viewsets.py:1228
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "Sie haben die maximale Anzahl an Dateien dieses Typs erreicht."
|
||||
|
||||
@@ -146,51 +146,59 @@ msgstr "Stopp fehlgeschlagen"
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Benachrichtigung erfolgreich"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:65
|
||||
msgid "External process successful"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:67
|
||||
msgid "External process failed"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:96
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:97
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSKRIPT"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:103
|
||||
msgid "Public Access"
|
||||
msgstr "Öffentlicher Zugriff"
|
||||
|
||||
#: core/models.py:97
|
||||
#: core/models.py:104
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrauenswürdiger Zugriff"
|
||||
|
||||
#: core/models.py:98
|
||||
#: core/models.py:105
|
||||
msgid "Restricted Access"
|
||||
msgstr "Eingeschränkter Zugriff"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:117
|
||||
msgid "id"
|
||||
msgstr "ID"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:118
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Primärschlüssel des Eintrags als UUID"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:124
|
||||
msgid "created on"
|
||||
msgstr "erstellt am"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:125
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
||||
|
||||
#: core/models.py:123
|
||||
#: core/models.py:130
|
||||
msgid "updated on"
|
||||
msgstr "aktualisiert am"
|
||||
|
||||
#: core/models.py:124
|
||||
#: core/models.py:131
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:151
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -198,11 +206,11 @@ msgstr ""
|
||||
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
|
||||
"und die Zeichen @/./+/-/_ enthalten."
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:157
|
||||
msgid "sub"
|
||||
msgstr "Sub"
|
||||
|
||||
#: core/models.py:152
|
||||
#: core/models.py:159
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -210,55 +218,55 @@ msgstr ""
|
||||
"Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. "
|
||||
"Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ Zeichen erlaubt."
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:168
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:173
|
||||
msgid "admin email address"
|
||||
msgstr "Administrator-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:175
|
||||
msgid "full name"
|
||||
msgstr "Vollständiger Name"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:177
|
||||
msgid "short name"
|
||||
msgstr "Kurzname"
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:183
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: core/models.py:177
|
||||
#: core/models.py:184
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:190
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:193
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:195
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:198
|
||||
msgid "staff status"
|
||||
msgstr "Mitarbeiterstatus"
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:200
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:203
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: core/models.py:199
|
||||
#: core/models.py:206
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -266,66 +274,66 @@ msgstr ""
|
||||
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
||||
"anstelle des Löschens des Kontos."
|
||||
|
||||
#: core/models.py:212
|
||||
#: core/models.py:219
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:213
|
||||
#: core/models.py:220
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:272
|
||||
#: core/models.py:279
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:273
|
||||
#: core/models.py:280
|
||||
msgid "Resources"
|
||||
msgstr "Ressourcen"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:338
|
||||
msgid "Resource access"
|
||||
msgstr "Ressourcenzugriff"
|
||||
|
||||
#: core/models.py:332
|
||||
#: core/models.py:339
|
||||
msgid "Resource accesses"
|
||||
msgstr "Ressourcenzugriffe"
|
||||
|
||||
#: core/models.py:338
|
||||
#: core/models.py:345
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
||||
"bereits."
|
||||
|
||||
#: core/models.py:394
|
||||
#: core/models.py:402
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-Raumkonfiguration"
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:403
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
||||
|
||||
#: core/models.py:402
|
||||
#: core/models.py:410
|
||||
msgid "Room PIN code"
|
||||
msgstr "PIN-Code für den Raum"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:411
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
||||
|
||||
#: core/models.py:409 core/models.py:563
|
||||
#: core/models.py:417 core/models.py:571
|
||||
msgid "Room"
|
||||
msgstr "Raum"
|
||||
|
||||
#: core/models.py:410
|
||||
#: core/models.py:418
|
||||
msgid "Rooms"
|
||||
msgstr "Räume"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:582
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker-ID"
|
||||
|
||||
#: core/models.py:576
|
||||
#: core/models.py:584
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -334,141 +342,149 @@ msgstr ""
|
||||
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
||||
"ermöglicht."
|
||||
|
||||
#: core/models.py:584
|
||||
#: core/models.py:592
|
||||
msgid "Recording mode"
|
||||
msgstr "Aufzeichnungsmodus"
|
||||
|
||||
#: core/models.py:585
|
||||
#: core/models.py:593
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
||||
|
||||
#: core/models.py:590 core/models.py:591
|
||||
#: core/models.py:598 core/models.py:599
|
||||
msgid "Recording options"
|
||||
msgstr "Aufnahmeoptionen"
|
||||
|
||||
#: core/models.py:597
|
||||
#: core/models.py:606
|
||||
msgid "External Process ID"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:607
|
||||
msgid "ID of the external process associated with the recording."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:613
|
||||
msgid "Recording"
|
||||
msgstr "Aufzeichnung"
|
||||
|
||||
#: core/models.py:598
|
||||
#: core/models.py:614
|
||||
msgid "Recordings"
|
||||
msgstr "Aufzeichnungen"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:724
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Beziehung Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:707
|
||||
#: core/models.py:725
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:713
|
||||
#: core/models.py:731
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:719
|
||||
#: core/models.py:737
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:725
|
||||
#: core/models.py:743
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:760
|
||||
msgid "Create rooms"
|
||||
msgstr "Räume erstellen"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:761
|
||||
msgid "List rooms"
|
||||
msgstr "Räume auflisten"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:762
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Raumdetails abrufen"
|
||||
|
||||
#: core/models.py:745
|
||||
#: core/models.py:763
|
||||
msgid "Update rooms"
|
||||
msgstr "Räume aktualisieren"
|
||||
|
||||
#: core/models.py:746
|
||||
#: core/models.py:764
|
||||
msgid "Delete rooms"
|
||||
msgstr "Räume löschen"
|
||||
|
||||
#: core/models.py:759
|
||||
#: core/models.py:777
|
||||
msgid "Application name"
|
||||
msgstr "Anwendungsname"
|
||||
|
||||
#: core/models.py:760
|
||||
#: core/models.py:778
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschreibender Name für diese Anwendung."
|
||||
|
||||
#: core/models.py:770
|
||||
#: core/models.py:788
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
|
||||
|
||||
#: core/models.py:781
|
||||
#: core/models.py:799
|
||||
msgid "Application"
|
||||
msgstr "Anwendung"
|
||||
|
||||
#: core/models.py:782
|
||||
#: core/models.py:800
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
|
||||
#: core/models.py:805
|
||||
#: core/models.py:823
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Geben Sie eine gültige Domain ein"
|
||||
|
||||
#: core/models.py:808
|
||||
#: core/models.py:826
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:809
|
||||
#: core/models.py:827
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
|
||||
|
||||
#: core/models.py:821
|
||||
#: core/models.py:839
|
||||
msgid "Application domain"
|
||||
msgstr "Anwendungsdomain"
|
||||
|
||||
#: core/models.py:822
|
||||
#: core/models.py:840
|
||||
msgid "Application domains"
|
||||
msgstr "Anwendungsdomains"
|
||||
|
||||
#: core/models.py:840
|
||||
#: core/models.py:858
|
||||
msgid "Pending"
|
||||
msgstr "Ausstehend"
|
||||
|
||||
#: core/models.py:848
|
||||
#: core/models.py:866
|
||||
msgid "Ready"
|
||||
msgstr "Bereit"
|
||||
|
||||
#: core/models.py:854
|
||||
#: core/models.py:872
|
||||
msgid "Background image"
|
||||
msgstr "Hintergrundbild"
|
||||
|
||||
#: core/models.py:866
|
||||
#: core/models.py:884
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: core/models.py:890
|
||||
#: core/models.py:908
|
||||
msgid "Malware detection info when the analysis status is unsafe."
|
||||
msgstr ""
|
||||
"Informationen zur Malware-Erkennung, wenn der Analyse-Status unsicher ist."
|
||||
|
||||
#: core/models.py:895
|
||||
#: core/models.py:913
|
||||
msgid "File"
|
||||
msgstr "Datei"
|
||||
|
||||
#: core/models.py:896
|
||||
#: core/models.py:914
|
||||
msgid "Files"
|
||||
msgstr "Dateien"
|
||||
|
||||
#: core/models.py:1000
|
||||
#: core/models.py:1018
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "Diese Datei wurde bereits endgültig gelöscht."
|
||||
|
||||
#: core/models.py:1010
|
||||
#: core/models.py:1028
|
||||
#, fuzzy
|
||||
#| msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
@@ -476,7 +492,7 @@ msgstr ""
|
||||
"Um eine Datei endgültig zu löschen, muss sie zuvor weich gelöscht worden "
|
||||
"sein."
|
||||
|
||||
#: core/recording/event/notification.py:116
|
||||
#: core/recording/event/notification.py:124
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Ihre Aufzeichnung ist bereit"
|
||||
|
||||
@@ -576,7 +592,9 @@ msgstr "So speichern Sie diese Aufzeichnung dauerhaft:"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:208
|
||||
#: core/templates/mail/text/screen_recording.txt:13
|
||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
#, fuzzy
|
||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
msgid "Click the \"Open\" button below "
|
||||
msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:209
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-21 14:09+0000\n"
|
||||
"POT-Creation-Date: 2026-05-26 16:20+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -88,15 +88,15 @@ msgstr "Scopes"
|
||||
msgid "Creator is me"
|
||||
msgstr "Creator is me"
|
||||
|
||||
#: core/api/serializers.py:88
|
||||
#: core/api/serializers.py:89
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr "You must be administrator or owner of a room to add accesses to it."
|
||||
|
||||
#: core/api/serializers.py:509
|
||||
#: core/api/serializers.py:534
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "This file extension is not allowed."
|
||||
|
||||
#: core/api/viewsets.py:1090
|
||||
#: core/api/viewsets.py:1228
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "You have reached the maximum number of files for this type."
|
||||
|
||||
@@ -144,51 +144,59 @@ msgstr "Failed to Stop"
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification succeeded"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:65
|
||||
msgid "External process successful"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:67
|
||||
msgid "External process failed"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:96
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCREEN_RECORDING"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:97
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:103
|
||||
msgid "Public Access"
|
||||
msgstr "Public Access"
|
||||
|
||||
#: core/models.py:97
|
||||
#: core/models.py:104
|
||||
msgid "Trusted Access"
|
||||
msgstr "Trusted Access"
|
||||
|
||||
#: core/models.py:98
|
||||
#: core/models.py:105
|
||||
msgid "Restricted Access"
|
||||
msgstr "Restricted Access"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:117
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:118
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primary key for the record as UUID"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:124
|
||||
msgid "created on"
|
||||
msgstr "created on"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:125
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date and time at which a record was created"
|
||||
|
||||
#: core/models.py:123
|
||||
#: core/models.py:130
|
||||
msgid "updated on"
|
||||
msgstr "updated on"
|
||||
|
||||
#: core/models.py:124
|
||||
#: core/models.py:131
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date and time at which a record was last updated"
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:151
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -196,11 +204,11 @@ msgstr ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:157
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:152
|
||||
#: core/models.py:159
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -208,55 +216,55 @@ msgstr ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:168
|
||||
msgid "identity email address"
|
||||
msgstr "identity email address"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:173
|
||||
msgid "admin email address"
|
||||
msgstr "admin email address"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:175
|
||||
msgid "full name"
|
||||
msgstr "full name"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:177
|
||||
msgid "short name"
|
||||
msgstr "short name"
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:183
|
||||
msgid "language"
|
||||
msgstr "language"
|
||||
|
||||
#: core/models.py:177
|
||||
#: core/models.py:184
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "The language in which the user wants to see the interface."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:190
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "The timezone in which the user wants to see times."
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:193
|
||||
msgid "device"
|
||||
msgstr "device"
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:195
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Whether the user is a device or a real user."
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:198
|
||||
msgid "staff status"
|
||||
msgstr "staff status"
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:200
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Whether the user can log into this admin site."
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:203
|
||||
msgid "active"
|
||||
msgstr "active"
|
||||
|
||||
#: core/models.py:199
|
||||
#: core/models.py:206
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -264,63 +272,63 @@ msgstr ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
|
||||
#: core/models.py:212
|
||||
#: core/models.py:219
|
||||
msgid "user"
|
||||
msgstr "user"
|
||||
|
||||
#: core/models.py:213
|
||||
#: core/models.py:220
|
||||
msgid "users"
|
||||
msgstr "users"
|
||||
|
||||
#: core/models.py:272
|
||||
#: core/models.py:279
|
||||
msgid "Resource"
|
||||
msgstr "Resource"
|
||||
|
||||
#: core/models.py:273
|
||||
#: core/models.py:280
|
||||
msgid "Resources"
|
||||
msgstr "Resources"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:338
|
||||
msgid "Resource access"
|
||||
msgstr "Resource access"
|
||||
|
||||
#: core/models.py:332
|
||||
#: core/models.py:339
|
||||
msgid "Resource accesses"
|
||||
msgstr "Resource accesses"
|
||||
|
||||
#: core/models.py:338
|
||||
#: core/models.py:345
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Resource access with this User and Resource already exists."
|
||||
|
||||
#: core/models.py:394
|
||||
#: core/models.py:402
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio room configuration"
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:403
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Values for Visio parameters to configure the room."
|
||||
|
||||
#: core/models.py:402
|
||||
#: core/models.py:410
|
||||
msgid "Room PIN code"
|
||||
msgstr "Room PIN code"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:411
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
||||
|
||||
#: core/models.py:409 core/models.py:563
|
||||
#: core/models.py:417 core/models.py:571
|
||||
msgid "Room"
|
||||
msgstr "Room"
|
||||
|
||||
#: core/models.py:410
|
||||
#: core/models.py:418
|
||||
msgid "Rooms"
|
||||
msgstr "Rooms"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:582
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:576
|
||||
#: core/models.py:584
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -328,151 +336,159 @@ msgstr ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
|
||||
#: core/models.py:584
|
||||
#: core/models.py:592
|
||||
msgid "Recording mode"
|
||||
msgstr "Recording mode"
|
||||
|
||||
#: core/models.py:585
|
||||
#: core/models.py:593
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Defines the mode of recording being called."
|
||||
|
||||
#: core/models.py:590 core/models.py:591
|
||||
#: core/models.py:598 core/models.py:599
|
||||
msgid "Recording options"
|
||||
msgstr "Recording options"
|
||||
|
||||
#: core/models.py:597
|
||||
#: core/models.py:606
|
||||
msgid "External Process ID"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:607
|
||||
msgid "ID of the external process associated with the recording."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:613
|
||||
msgid "Recording"
|
||||
msgstr "Recording"
|
||||
|
||||
#: core/models.py:598
|
||||
#: core/models.py:614
|
||||
msgid "Recordings"
|
||||
msgstr "Recordings"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:724
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Recording/user relation"
|
||||
|
||||
#: core/models.py:707
|
||||
#: core/models.py:725
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Recording/user relations"
|
||||
|
||||
#: core/models.py:713
|
||||
#: core/models.py:731
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "This user is already in this recording."
|
||||
|
||||
#: core/models.py:719
|
||||
#: core/models.py:737
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "This team is already in this recording."
|
||||
|
||||
#: core/models.py:725
|
||||
#: core/models.py:743
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Either user or team must be set, not both."
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:760
|
||||
#, fuzzy
|
||||
#| msgid "created on"
|
||||
msgid "Create rooms"
|
||||
msgstr "Create rooms"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:761
|
||||
msgid "List rooms"
|
||||
msgstr "List rooms"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:762
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Retrieve room details"
|
||||
|
||||
#: core/models.py:745
|
||||
#: core/models.py:763
|
||||
#, fuzzy
|
||||
#| msgid "updated on"
|
||||
msgid "Update rooms"
|
||||
msgstr "Update rooms"
|
||||
|
||||
#: core/models.py:746
|
||||
#: core/models.py:764
|
||||
msgid "Delete rooms"
|
||||
msgstr "Delete rooms"
|
||||
|
||||
#: core/models.py:759
|
||||
#: core/models.py:777
|
||||
msgid "Application name"
|
||||
msgstr "Application name"
|
||||
|
||||
#: core/models.py:760
|
||||
#: core/models.py:778
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Descriptive name for this application."
|
||||
|
||||
#: core/models.py:770
|
||||
#: core/models.py:788
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Hashed on Save. Copy it now if this is a new secret."
|
||||
|
||||
#: core/models.py:781
|
||||
#: core/models.py:799
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:782
|
||||
#: core/models.py:800
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:805
|
||||
#: core/models.py:823
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Enter a valid domain"
|
||||
|
||||
#: core/models.py:808
|
||||
#: core/models.py:826
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:809
|
||||
#: core/models.py:827
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Email domain this application can act on behalf of."
|
||||
|
||||
#: core/models.py:821
|
||||
#: core/models.py:839
|
||||
msgid "Application domain"
|
||||
msgstr "Application domain"
|
||||
|
||||
#: core/models.py:822
|
||||
#: core/models.py:840
|
||||
msgid "Application domains"
|
||||
msgstr "Application domains"
|
||||
|
||||
#: core/models.py:840
|
||||
#: core/models.py:858
|
||||
#, fuzzy
|
||||
#| msgid "Recording"
|
||||
msgid "Pending"
|
||||
msgstr "Pending"
|
||||
|
||||
#: core/models.py:848
|
||||
#: core/models.py:866
|
||||
msgid "Ready"
|
||||
msgstr "Ready"
|
||||
|
||||
#: core/models.py:854
|
||||
#: core/models.py:872
|
||||
msgid "Background image"
|
||||
msgstr "Background image"
|
||||
|
||||
#: core/models.py:866
|
||||
#: core/models.py:884
|
||||
msgid "title"
|
||||
msgstr "title"
|
||||
|
||||
#: core/models.py:890
|
||||
#: core/models.py:908
|
||||
msgid "Malware detection info when the analysis status is unsafe."
|
||||
msgstr "Malware detection info when the analysis status is unsafe."
|
||||
|
||||
#: core/models.py:895
|
||||
#: core/models.py:913
|
||||
msgid "File"
|
||||
msgstr "File"
|
||||
|
||||
#: core/models.py:896
|
||||
#: core/models.py:914
|
||||
msgid "Files"
|
||||
msgstr "Files"
|
||||
|
||||
#: core/models.py:1000
|
||||
#: core/models.py:1018
|
||||
#, fuzzy
|
||||
#| msgid "This user is already in this recording."
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "This file is already hard deleted."
|
||||
|
||||
#: core/models.py:1010
|
||||
#: core/models.py:1028
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgstr "To hard delete a file, it must first be soft deleted."
|
||||
|
||||
#: core/recording/event/notification.py:116
|
||||
#: core/recording/event/notification.py:124
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Your recording is ready"
|
||||
|
||||
@@ -572,7 +588,9 @@ msgstr "To keep this recording permanently:"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:208
|
||||
#: core/templates/mail/text/screen_recording.txt:13
|
||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
#, fuzzy
|
||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
msgid "Click the \"Open\" button below "
|
||||
msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:209
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-21 14:09+0000\n"
|
||||
"POT-Creation-Date: 2026-05-26 16:20+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -89,17 +89,17 @@ msgstr "Scopes"
|
||||
msgid "Creator is me"
|
||||
msgstr "Je suis le créateur"
|
||||
|
||||
#: core/api/serializers.py:88
|
||||
#: core/api/serializers.py:89
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
||||
"des accès."
|
||||
|
||||
#: core/api/serializers.py:509
|
||||
#: core/api/serializers.py:534
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "Cette extension n'est pas autorisée"
|
||||
|
||||
#: core/api/viewsets.py:1090
|
||||
#: core/api/viewsets.py:1228
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "Vous avez atteint le nombre maximum de fichiers de ce type"
|
||||
|
||||
@@ -147,53 +147,61 @@ msgstr "Échec à l'arrêt"
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification réussie"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:65
|
||||
msgid "External process successful"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:67
|
||||
msgid "External process failed"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:96
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "ENREGISTREMENT_ÉCRAN"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:97
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPTION"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:103
|
||||
msgid "Public Access"
|
||||
msgstr "Accès public"
|
||||
|
||||
#: core/models.py:97
|
||||
#: core/models.py:104
|
||||
msgid "Trusted Access"
|
||||
msgstr "Accès de confiance"
|
||||
|
||||
#: core/models.py:98
|
||||
#: core/models.py:105
|
||||
msgid "Restricted Access"
|
||||
msgstr "Accès restreint"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:117
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:118
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:124
|
||||
msgid "created on"
|
||||
msgstr "créé le"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:125
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure auxquelles un enregistrement a été créé"
|
||||
|
||||
#: core/models.py:123
|
||||
#: core/models.py:130
|
||||
msgid "updated on"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:124
|
||||
#: core/models.py:131
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
||||
"fois"
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:151
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -201,11 +209,11 @@ msgstr ""
|
||||
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
|
||||
"chiffres et les caractères @/./+/-/_."
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:157
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:152
|
||||
#: core/models.py:159
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -213,55 +221,55 @@ msgstr ""
|
||||
"Optionnel pour les utilisateurs en attente ; requis lors de l'activation du "
|
||||
"compte. 255 caractères maximum. Lettres, chiffres et @/./+/-/_ uniquement."
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:168
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:173
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail d'administrateur"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:175
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:177
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:183
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:177
|
||||
#: core/models.py:184
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:190
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:193
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:195
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:198
|
||||
msgid "staff status"
|
||||
msgstr "statut du personnel"
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:200
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:203
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: core/models.py:199
|
||||
#: core/models.py:206
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -269,65 +277,65 @@ msgstr ""
|
||||
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
||||
"au lieu de supprimer des comptes."
|
||||
|
||||
#: core/models.py:212
|
||||
#: core/models.py:219
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:213
|
||||
#: core/models.py:220
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:272
|
||||
#: core/models.py:279
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:273
|
||||
#: core/models.py:280
|
||||
msgid "Resources"
|
||||
msgstr "Ressources"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:338
|
||||
msgid "Resource access"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:332
|
||||
#: core/models.py:339
|
||||
msgid "Resource accesses"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:338
|
||||
#: core/models.py:345
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
||||
|
||||
#: core/models.py:394
|
||||
#: core/models.py:402
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Configuration de la salle de visioconférence"
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:403
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
||||
|
||||
#: core/models.py:402
|
||||
#: core/models.py:410
|
||||
msgid "Room PIN code"
|
||||
msgstr "Code PIN de la salle"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:411
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
||||
|
||||
#: core/models.py:409 core/models.py:563
|
||||
#: core/models.py:417 core/models.py:571
|
||||
msgid "Room"
|
||||
msgstr "Salle"
|
||||
|
||||
#: core/models.py:410
|
||||
#: core/models.py:418
|
||||
msgid "Rooms"
|
||||
msgstr "Salles"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:582
|
||||
msgid "Worker ID"
|
||||
msgstr "ID du Worker"
|
||||
|
||||
#: core/models.py:576
|
||||
#: core/models.py:584
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -335,150 +343,158 @@ msgstr ""
|
||||
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
|
||||
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
|
||||
|
||||
#: core/models.py:584
|
||||
#: core/models.py:592
|
||||
msgid "Recording mode"
|
||||
msgstr "Mode d'enregistrement"
|
||||
|
||||
#: core/models.py:585
|
||||
#: core/models.py:593
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Définit le mode d'enregistrement appelé."
|
||||
|
||||
#: core/models.py:590 core/models.py:591
|
||||
#: core/models.py:598 core/models.py:599
|
||||
msgid "Recording options"
|
||||
msgstr "Options d'enregistrement"
|
||||
|
||||
#: core/models.py:597
|
||||
#: core/models.py:606
|
||||
msgid "External Process ID"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:607
|
||||
msgid "ID of the external process associated with the recording."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:613
|
||||
msgid "Recording"
|
||||
msgstr "Enregistrement"
|
||||
|
||||
#: core/models.py:598
|
||||
#: core/models.py:614
|
||||
msgid "Recordings"
|
||||
msgstr "Enregistrements"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:724
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Relation enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:707
|
||||
#: core/models.py:725
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Relations enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:713
|
||||
#: core/models.py:731
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:719
|
||||
#: core/models.py:737
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Cette équipe est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:725
|
||||
#: core/models.py:743
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:760
|
||||
msgid "Create rooms"
|
||||
msgstr "Créer des salles"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:761
|
||||
msgid "List rooms"
|
||||
msgstr "Lister les salles"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:762
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Afficher les détails d’une salle"
|
||||
|
||||
#: core/models.py:745
|
||||
#: core/models.py:763
|
||||
msgid "Update rooms"
|
||||
msgstr "Mettre à jour les salles"
|
||||
|
||||
#: core/models.py:746
|
||||
#: core/models.py:764
|
||||
msgid "Delete rooms"
|
||||
msgstr "Supprimer les salles"
|
||||
|
||||
#: core/models.py:759
|
||||
#: core/models.py:777
|
||||
msgid "Application name"
|
||||
msgstr "Nom de l’application"
|
||||
|
||||
#: core/models.py:760
|
||||
#: core/models.py:778
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Nom descriptif de cette application."
|
||||
|
||||
#: core/models.py:770
|
||||
#: core/models.py:788
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Haché lors de l’enregistrement. Copiez-le maintenant s’il s’agit d’un "
|
||||
"nouveau secret."
|
||||
|
||||
#: core/models.py:781
|
||||
#: core/models.py:799
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:782
|
||||
#: core/models.py:800
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:805
|
||||
#: core/models.py:823
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Saisissez un domaine valide"
|
||||
|
||||
#: core/models.py:808
|
||||
#: core/models.py:826
|
||||
msgid "Domain"
|
||||
msgstr "Domaine"
|
||||
|
||||
#: core/models.py:809
|
||||
#: core/models.py:827
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Domaine de messagerie au nom duquel cette application peut agir."
|
||||
|
||||
#: core/models.py:821
|
||||
#: core/models.py:839
|
||||
msgid "Application domain"
|
||||
msgstr "Domaine d’application"
|
||||
|
||||
#: core/models.py:822
|
||||
#: core/models.py:840
|
||||
msgid "Application domains"
|
||||
msgstr "Domaines d’application"
|
||||
|
||||
#: core/models.py:840
|
||||
#: core/models.py:858
|
||||
msgid "Pending"
|
||||
msgstr "En attente"
|
||||
|
||||
#: core/models.py:848
|
||||
#: core/models.py:866
|
||||
msgid "Ready"
|
||||
msgstr "Prêt"
|
||||
|
||||
#: core/models.py:854
|
||||
#: core/models.py:872
|
||||
msgid "Background image"
|
||||
msgstr "Image de fond"
|
||||
|
||||
#: core/models.py:866
|
||||
#: core/models.py:884
|
||||
msgid "title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: core/models.py:890
|
||||
#: core/models.py:908
|
||||
msgid "Malware detection info when the analysis status is unsafe."
|
||||
msgstr ""
|
||||
"Information concernant la détection de Malware cand le statut n'est pas sain"
|
||||
|
||||
#: core/models.py:895
|
||||
#: core/models.py:913
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
|
||||
#: core/models.py:896
|
||||
#: core/models.py:914
|
||||
msgid "Files"
|
||||
msgstr "Fichiers"
|
||||
|
||||
#: core/models.py:1000
|
||||
#: core/models.py:1018
|
||||
#, fuzzy
|
||||
#| msgid "This user is already in this recording."
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "Ce fichier a été supprimé."
|
||||
|
||||
#: core/models.py:1010
|
||||
#: core/models.py:1028
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgstr ""
|
||||
"Pour supprimer définitivement un fichier il doit d'abord avoir été marqué "
|
||||
"comme supprimé (soft delete)"
|
||||
|
||||
#: core/recording/event/notification.py:116
|
||||
#: core/recording/event/notification.py:124
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Votre enregistrement est prêt"
|
||||
|
||||
@@ -578,7 +594,9 @@ msgstr "Pour conserver cet enregistrement de façon permanente :"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:208
|
||||
#: core/templates/mail/text/screen_recording.txt:13
|
||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
#, fuzzy
|
||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
msgid "Click the \"Open\" button below "
|
||||
msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:209
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-21 14:09+0000\n"
|
||||
"POT-Creation-Date: 2026-05-26 16:20+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -88,16 +88,16 @@ msgstr "Scopes"
|
||||
msgid "Creator is me"
|
||||
msgstr "Maker ben ik"
|
||||
|
||||
#: core/api/serializers.py:88
|
||||
#: core/api/serializers.py:89
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
||||
|
||||
#: core/api/serializers.py:509
|
||||
#: core/api/serializers.py:534
|
||||
msgid "This file extension is not allowed."
|
||||
msgstr "Deze bestandsextensie is niet toegestaan."
|
||||
|
||||
#: core/api/viewsets.py:1090
|
||||
#: core/api/viewsets.py:1228
|
||||
msgid "You have reached the maximum number of files for this type."
|
||||
msgstr "Het maximale aantal bestanden voor dit type is bereikt."
|
||||
|
||||
@@ -145,51 +145,59 @@ msgstr "Stoppen mislukt"
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notificatie geslaagd"
|
||||
|
||||
#: core/models.py:89
|
||||
#: core/models.py:65
|
||||
msgid "External process successful"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:67
|
||||
msgid "External process failed"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:96
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCHERM_OPNAME"
|
||||
|
||||
#: core/models.py:90
|
||||
#: core/models.py:97
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:96
|
||||
#: core/models.py:103
|
||||
msgid "Public Access"
|
||||
msgstr "Openbare toegang"
|
||||
|
||||
#: core/models.py:97
|
||||
#: core/models.py:104
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrouwde toegang"
|
||||
|
||||
#: core/models.py:98
|
||||
#: core/models.py:105
|
||||
msgid "Restricted Access"
|
||||
msgstr "Beperkte toegang"
|
||||
|
||||
#: core/models.py:110
|
||||
#: core/models.py:117
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:111
|
||||
#: core/models.py:118
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primaire sleutel voor het record als UUID"
|
||||
|
||||
#: core/models.py:117
|
||||
#: core/models.py:124
|
||||
msgid "created on"
|
||||
msgstr "aangemaakt op"
|
||||
|
||||
#: core/models.py:118
|
||||
#: core/models.py:125
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd waarop een record werd aangemaakt"
|
||||
|
||||
#: core/models.py:123
|
||||
#: core/models.py:130
|
||||
msgid "updated on"
|
||||
msgstr "bijgewerkt op"
|
||||
|
||||
#: core/models.py:124
|
||||
#: core/models.py:131
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
||||
|
||||
#: core/models.py:144
|
||||
#: core/models.py:151
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -197,11 +205,11 @@ msgstr ""
|
||||
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
|
||||
"_ tekens bevatten."
|
||||
|
||||
#: core/models.py:150
|
||||
#: core/models.py:157
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:152
|
||||
#: core/models.py:159
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
@@ -209,55 +217,55 @@ msgstr ""
|
||||
"Optioneel voor gebruikers in afwachting; vereist bij accountactivering. "
|
||||
"Maximum 255 tekens. Alleen letters, cijfers en @/./+/-/_ toegestaan."
|
||||
|
||||
#: core/models.py:161
|
||||
#: core/models.py:168
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit e-mailadres"
|
||||
|
||||
#: core/models.py:166
|
||||
#: core/models.py:173
|
||||
msgid "admin email address"
|
||||
msgstr "beheerder e-mailadres"
|
||||
|
||||
#: core/models.py:168
|
||||
#: core/models.py:175
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: core/models.py:170
|
||||
#: core/models.py:177
|
||||
msgid "short name"
|
||||
msgstr "korte naam"
|
||||
|
||||
#: core/models.py:176
|
||||
#: core/models.py:183
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: core/models.py:177
|
||||
#: core/models.py:184
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:190
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
||||
|
||||
#: core/models.py:186
|
||||
#: core/models.py:193
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:195
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: core/models.py:191
|
||||
#: core/models.py:198
|
||||
msgid "staff status"
|
||||
msgstr "personeelsstatus"
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:200
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:203
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: core/models.py:199
|
||||
#: core/models.py:206
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -265,64 +273,64 @@ msgstr ""
|
||||
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
||||
"plaats van accounts te verwijderen."
|
||||
|
||||
#: core/models.py:212
|
||||
#: core/models.py:219
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: core/models.py:213
|
||||
#: core/models.py:220
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: core/models.py:272
|
||||
#: core/models.py:279
|
||||
msgid "Resource"
|
||||
msgstr "Bron"
|
||||
|
||||
#: core/models.py:273
|
||||
#: core/models.py:280
|
||||
msgid "Resources"
|
||||
msgstr "Bronnen"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:338
|
||||
msgid "Resource access"
|
||||
msgstr "Brontoegang"
|
||||
|
||||
#: core/models.py:332
|
||||
#: core/models.py:339
|
||||
msgid "Resource accesses"
|
||||
msgstr "Brontoegangsrechten"
|
||||
|
||||
#: core/models.py:338
|
||||
#: core/models.py:345
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
||||
|
||||
#: core/models.py:394
|
||||
#: core/models.py:402
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-ruimteconfiguratie"
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:403
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
||||
|
||||
#: core/models.py:402
|
||||
#: core/models.py:410
|
||||
msgid "Room PIN code"
|
||||
msgstr "Pincode van de kamer"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:411
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
||||
|
||||
#: core/models.py:409 core/models.py:563
|
||||
#: core/models.py:417 core/models.py:571
|
||||
msgid "Room"
|
||||
msgstr "Ruimte"
|
||||
|
||||
#: core/models.py:410
|
||||
#: core/models.py:418
|
||||
msgid "Rooms"
|
||||
msgstr "Ruimtes"
|
||||
|
||||
#: core/models.py:574
|
||||
#: core/models.py:582
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:576
|
||||
#: core/models.py:584
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -330,140 +338,148 @@ msgstr ""
|
||||
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
||||
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
||||
|
||||
#: core/models.py:584
|
||||
#: core/models.py:592
|
||||
msgid "Recording mode"
|
||||
msgstr "Opnamemodus"
|
||||
|
||||
#: core/models.py:585
|
||||
#: core/models.py:593
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
||||
|
||||
#: core/models.py:590 core/models.py:591
|
||||
#: core/models.py:598 core/models.py:599
|
||||
msgid "Recording options"
|
||||
msgstr "Opnameopties"
|
||||
|
||||
#: core/models.py:597
|
||||
#: core/models.py:606
|
||||
msgid "External Process ID"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:607
|
||||
msgid "ID of the external process associated with the recording."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:613
|
||||
msgid "Recording"
|
||||
msgstr "Opname"
|
||||
|
||||
#: core/models.py:598
|
||||
#: core/models.py:614
|
||||
msgid "Recordings"
|
||||
msgstr "Opnames"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:724
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Opname/gebruiker-relatie"
|
||||
|
||||
#: core/models.py:707
|
||||
#: core/models.py:725
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Opname/gebruiker-relaties"
|
||||
|
||||
#: core/models.py:713
|
||||
#: core/models.py:731
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Deze gebruiker is al in deze opname."
|
||||
|
||||
#: core/models.py:719
|
||||
#: core/models.py:737
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dit team is al in deze opname."
|
||||
|
||||
#: core/models.py:725
|
||||
#: core/models.py:743
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
||||
|
||||
#: core/models.py:742
|
||||
#: core/models.py:760
|
||||
msgid "Create rooms"
|
||||
msgstr "Ruimtes aanmaken"
|
||||
|
||||
#: core/models.py:743
|
||||
#: core/models.py:761
|
||||
msgid "List rooms"
|
||||
msgstr "Ruimtes weergeven"
|
||||
|
||||
#: core/models.py:744
|
||||
#: core/models.py:762
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Details van een ruimte ophalen"
|
||||
|
||||
#: core/models.py:745
|
||||
#: core/models.py:763
|
||||
msgid "Update rooms"
|
||||
msgstr "Ruimtes bijwerken"
|
||||
|
||||
#: core/models.py:746
|
||||
#: core/models.py:764
|
||||
msgid "Delete rooms"
|
||||
msgstr "Ruimtes verwijderen"
|
||||
|
||||
#: core/models.py:759
|
||||
#: core/models.py:777
|
||||
msgid "Application name"
|
||||
msgstr "Naam van de applicatie"
|
||||
|
||||
#: core/models.py:760
|
||||
#: core/models.py:778
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschrijvende naam voor deze applicatie."
|
||||
|
||||
#: core/models.py:770
|
||||
#: core/models.py:788
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
|
||||
|
||||
#: core/models.py:781
|
||||
#: core/models.py:799
|
||||
msgid "Application"
|
||||
msgstr "Applicatie"
|
||||
|
||||
#: core/models.py:782
|
||||
#: core/models.py:800
|
||||
msgid "Applications"
|
||||
msgstr "Applicaties"
|
||||
|
||||
#: core/models.py:805
|
||||
#: core/models.py:823
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Voer een geldig domein in"
|
||||
|
||||
#: core/models.py:808
|
||||
#: core/models.py:826
|
||||
msgid "Domain"
|
||||
msgstr "Domein"
|
||||
|
||||
#: core/models.py:809
|
||||
#: core/models.py:827
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-maildomein namens welke deze applicatie kan handelen."
|
||||
|
||||
#: core/models.py:821
|
||||
#: core/models.py:839
|
||||
msgid "Application domain"
|
||||
msgstr "Applicatiedomein"
|
||||
|
||||
#: core/models.py:822
|
||||
#: core/models.py:840
|
||||
msgid "Application domains"
|
||||
msgstr "Applicatiedomeinen"
|
||||
|
||||
#: core/models.py:840
|
||||
#: core/models.py:858
|
||||
msgid "Pending"
|
||||
msgstr "In afwachting"
|
||||
|
||||
#: core/models.py:848
|
||||
#: core/models.py:866
|
||||
msgid "Ready"
|
||||
msgstr "Klaar"
|
||||
|
||||
#: core/models.py:854
|
||||
#: core/models.py:872
|
||||
msgid "Background image"
|
||||
msgstr "Achtergrondafbeelding"
|
||||
|
||||
#: core/models.py:866
|
||||
#: core/models.py:884
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: core/models.py:890
|
||||
#: core/models.py:908
|
||||
msgid "Malware detection info when the analysis status is unsafe."
|
||||
msgstr "Informatie over malwaredetectie wanneer de analysestatus onveilig is."
|
||||
|
||||
#: core/models.py:895
|
||||
#: core/models.py:913
|
||||
msgid "File"
|
||||
msgstr "Bestand"
|
||||
|
||||
#: core/models.py:896
|
||||
#: core/models.py:914
|
||||
msgid "Files"
|
||||
msgstr "Bestanden"
|
||||
|
||||
#: core/models.py:1000
|
||||
#: core/models.py:1018
|
||||
msgid "This file is already hard deleted."
|
||||
msgstr "Dit bestand is al definitief verwijderd."
|
||||
|
||||
#: core/models.py:1010
|
||||
#: core/models.py:1028
|
||||
#, fuzzy
|
||||
#| msgid "To hard delete a file, it must first be soft deleted."
|
||||
msgid "To hard delete a file, it must first be soft deleted."
|
||||
@@ -471,7 +487,7 @@ msgstr ""
|
||||
"Om een bestand definitief te verwijderen, moet het eerst zacht verwijderd "
|
||||
"zijn."
|
||||
|
||||
#: core/recording/event/notification.py:116
|
||||
#: core/recording/event/notification.py:124
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Je opname is klaar"
|
||||
|
||||
@@ -571,7 +587,9 @@ msgstr "Om deze opname permanent te bewaren:"
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:208
|
||||
#: core/templates/mail/text/screen_recording.txt:13
|
||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
#, fuzzy
|
||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||
msgid "Click the \"Open\" button below "
|
||||
msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:209
|
||||
|
||||
@@ -186,9 +186,6 @@ class Base(Configuration):
|
||||
FILE_UPLOAD_PATH = values.Value(
|
||||
"files", environ_name="FILE_UPLOAD_PATH", environ_prefix=None
|
||||
)
|
||||
FILE_UPLOAD_TMP_PATH = values.Value(
|
||||
"tmp/files", environ_name="FILE_UPLOAD_TMP_PATH", environ_prefix=None
|
||||
)
|
||||
|
||||
FILE_UPLOAD_APPLY_RESTRICTIONS = values.BooleanValue(
|
||||
default=True, environ_name="FILE_UPLOAD_APPLY_RESTRICTIONS", environ_prefix=None
|
||||
@@ -747,6 +744,9 @@ class Base(Configuration):
|
||||
SUMMARY_SERVICE_API_TOKEN = SecretFileValue(
|
||||
None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None
|
||||
)
|
||||
SUMMARY_SERVICE_WEBHOOK_API_TOKEN = SecretFileValue(
|
||||
None, environ_name="SUMMARY_SERVICE_WEBHOOK_API_TOKEN", environ_prefix=None
|
||||
)
|
||||
SCREEN_RECORDING_BASE_URL = values.Value(
|
||||
None, environ_name="SCREEN_RECORDING_BASE_URL", environ_prefix=None
|
||||
)
|
||||
@@ -911,18 +911,6 @@ class Base(Configuration):
|
||||
environ_name="APPLICATION_BASE_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
# Warning: EXTERNAL_API_ALLOW_PUBLIC_ACCESS is ignored when
|
||||
# EXTERNAL_API_DEFAULT_ACCESS_LEVEL=public.
|
||||
EXTERNAL_API_ALLOW_PUBLIC_ACCESS = values.BooleanValue(
|
||||
False,
|
||||
environ_name="EXTERNAL_API_ALLOW_PUBLIC_ACCESS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
EXTERNAL_API_DEFAULT_ACCESS_LEVEL = values.Value(
|
||||
"trusted",
|
||||
environ_name="EXTERNAL_API_DEFAULT_ACCESS_LEVEL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
# Allows third-party platforms to create users with email-only identification.
|
||||
# Required for external integrations, but fragile due to deferred user reconciliation
|
||||
# on sub. Enable it with care /!\
|
||||
@@ -1014,44 +1002,6 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "{asctime} {name} {levelname} {message}",
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
},
|
||||
},
|
||||
# Override root logger to send it to console
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None
|
||||
),
|
||||
},
|
||||
"loggers": {
|
||||
"core": {
|
||||
"handlers": ["console"],
|
||||
"level": values.Value(
|
||||
"INFO",
|
||||
environ_name="LOGGING_LEVEL_LOGGERS_APP",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
@@ -1090,11 +1040,6 @@ class Base(Configuration):
|
||||
"""
|
||||
super().post_setup()
|
||||
|
||||
if cls.FILE_UPLOAD_TMP_PATH == cls.FILE_UPLOAD_PATH:
|
||||
raise ValueError(
|
||||
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
||||
)
|
||||
|
||||
# The SENTRY_DSN setting should be available to activate sentry for an environment
|
||||
if cls.SENTRY_DSN is not None:
|
||||
sentry_sdk.init(
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import include, path
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from drf_spectacular.views import (
|
||||
SpectacularJSONAPIView,
|
||||
@@ -29,7 +29,7 @@ if settings.DEBUG:
|
||||
if settings.USE_SWAGGER or settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/swagger.json",
|
||||
f"{settings.API_VERSION}/swagger.json",
|
||||
SpectacularJSONAPIView.as_view(
|
||||
api_version=settings.API_VERSION,
|
||||
urlconf="core.urls",
|
||||
@@ -37,12 +37,12 @@ if settings.USE_SWAGGER or settings.DEBUG:
|
||||
name="client-api-schema",
|
||||
),
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/swagger/",
|
||||
f"{settings.API_VERSION}//swagger/",
|
||||
SpectacularSwaggerView.as_view(url_name="client-api-schema"),
|
||||
name="swagger-ui-schema",
|
||||
),
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/redoc/",
|
||||
re_path(
|
||||
f"{settings.API_VERSION}//redoc/",
|
||||
SpectacularRedocView.as_view(url_name="client-api-schema"),
|
||||
name="redoc-schema",
|
||||
),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
# Meet package
|
||||
#
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.11.16,<0.12.0"]
|
||||
requires = ["uv_build>=0.10.9,<0.11.0"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.20.0"
|
||||
version = "1.16.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -24,45 +24,44 @@ keywords = ["Django", "Contacts", "Templates", "RBAC"]
|
||||
license = "MIT"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"boto3==1.43.14",
|
||||
"boto3==1.42.68",
|
||||
"Brotli==1.2.0",
|
||||
"brevo-python==1.2.0",
|
||||
"celery[redis]==5.6.3",
|
||||
"celery[redis]==5.6.2",
|
||||
"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.26",
|
||||
"django-parler==2.4",
|
||||
"django-parler==2.3",
|
||||
"redis==5.2.1",
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django-pydantic-field==0.5.4",
|
||||
"django==5.2.14",
|
||||
"djangorestframework==3.17.1",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2026.3.4",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==26.0.0",
|
||||
"gunicorn==25.1.0",
|
||||
"jsonschema==4.26.0",
|
||||
"markdown==3.10.2",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.3.4",
|
||||
"pydantic==2.13.4",
|
||||
"PyJWT==2.13.0",
|
||||
"python-frontmatter==1.3.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.34.2",
|
||||
"sentry-sdk==2.60.0",
|
||||
"requests==2.33.0",
|
||||
"sentry-sdk==2.54.0",
|
||||
"whitenoise==6.12.0",
|
||||
"mozilla-django-oidc==5.0.2",
|
||||
"livekit-api==1.1.0",
|
||||
"aiohttp==3.14.0",
|
||||
"aiohttp==3.13.4",
|
||||
"urllib3==2.7.0",
|
||||
"phonenumbers==9.0.31",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -74,21 +73,21 @@ dependencies = [
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"drf-spectacular-sidecar==2026.5.1",
|
||||
"drf-spectacular-sidecar==2026.3.1",
|
||||
"freezegun==1.5.5",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.13.0",
|
||||
"pyfakefs==6.2.0",
|
||||
"ipython==9.11.0",
|
||||
"pyfakefs==6.1.5",
|
||||
"pylint-django==2.7.0",
|
||||
"pylint<4.0.0",
|
||||
"pytest-cov==7.1.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.12.0",
|
||||
"pytest==9.0.3",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.26.1",
|
||||
"ruff==0.15.14",
|
||||
"types-requests==2.33.0.20260518",
|
||||
"responses==0.26.0",
|
||||
"ruff==0.15.6",
|
||||
"types-requests==2.32.4.20260107",
|
||||
]
|
||||
|
||||
[tool.uv.build-backend]
|
||||
|
||||
@@ -13,7 +13,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.14.0"
|
||||
version = "3.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -24,72 +24,59 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -172,30 +159,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.43.14"
|
||||
version = "1.42.68"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/4b/616367e871ce3f1cb3e8545a97736b6331b9fb081497f2d44c5b2aa6959d/boto3-1.43.14.tar.gz", hash = "sha256:5c0a994b3182061ee101812e721100717a4d664f9f4ceaf4a86b6d032ce9fc2d", size = 113142, upload-time = "2026-05-22T19:28:47.861Z" }
|
||||
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/cb/00/59cb9329c18e2d3aa23062ceaa87d065f2e81e7d2931df24d64e9a7815aa/boto3-1.43.14-py3-none-any.whl", hash = "sha256:574335744656cfed0b362a0a0467aaf2eb2bf15526edcd02d31d3c661f4b09e4", size = 140536, upload-time = "2026-05-22T19:28:46.49Z" },
|
||||
{ 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.43.19"
|
||||
version = "1.42.89"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/a7/298986789785b74a954e2347114993be7e6b070417159125a6865f2687b6/botocore-1.43.19.tar.gz", hash = "sha256:18ac2fdd76c89b940707eb10493ff58678adad337d03215caec2d408ccd43cc0", size = 15435441, upload-time = "2026-06-01T19:32:53.126Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/cc/e6be943efa9051bd15c2ee14077c2b10d6e27c9e9385fc43a03a5c4ed8b5/botocore-1.42.89.tar.gz", hash = "sha256:95ac52f472dad29942f3088b278ab493044516c16dbf9133c975af16527baa99", size = 15206290, upload-time = "2026-04-13T19:36:02.321Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/75/fe4d45bdd08afd66f3d5273db58f3d8a29365e52ce3a0851f7f5e5900943/botocore-1.43.19-py3-none-any.whl", hash = "sha256:99dbdccbf748974750601e805cecc9362a85d11fee89d6d58cd3f4ff302e6ff9", size = 15117709, upload-time = "2026-06-01T19:32:47.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/f1/90a7b8eda38b7c3a65ca7ee0075bdf310b6b471cb1b95fab6e8994323a50/botocore-1.42.89-py3-none-any.whl", hash = "sha256:d9b786c8d9db6473063b4cc5be0ba7e6a381082307bd6afb69d4216f9fa95f35", size = 14887287, upload-time = "2026-04-13T19:35:56.677Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -240,7 +227,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "celery"
|
||||
version = "5.6.3"
|
||||
version = "5.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "billiard" },
|
||||
@@ -253,9 +240,9 @@ dependencies = [
|
||||
{ name = "tzlocal" },
|
||||
{ name = "vine" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/9d/3d13596519cfa7207a6f9834f4b082554845eb3cd2684b5f8535d50c7c44/celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b", size = 1718802, upload-time = "2026-01-04T12:35:58.012Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502, upload-time = "2026-01-04T12:35:55.894Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -686,14 +673,14 @@ all = [
|
||||
|
||||
[[package]]
|
||||
name = "django-parler"
|
||||
version = "2.4"
|
||||
version = "2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/96/e336bd885f185c5d4336217b26dfdd1a8a295ed6f5ea8d8b24a2e0f4d870/django_parler-2.4.tar.gz", hash = "sha256:6c70f853cf19c09fd72d9a10aa784ae57da9094178c650eab0c5d30c0a48be40", size = 91503, upload-time = "2026-05-14T08:58:08.128Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/2b/2423d31620efe8ab0d0390e60afab4f9cc2e62d4bf39fe0e05df0eef1b93/django-parler-2.3.tar.gz", hash = "sha256:2c8f5012ceb5e49af93b16ea3fe4d0c83d70b91b2d0f470c05d7d742b6f3083d", size = 69167, upload-time = "2021-11-18T09:32:28.959Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/87/6d6129e14a7815bb383b61e7f86ffa85a4f43132eafa15de4a3a30ff9424/django_parler-2.4-py3-none-any.whl", hash = "sha256:0fff7185d581790d7063cbe0f7c57a399517d5ec976536b6c6bc06cf8ad4b956", size = 112605, upload-time = "2026-05-14T08:58:06.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/38/11f1a7e3d56f3a6b74cf99e307f2554b741cadebc9b1c45b05e2ec1f35a2/django_parler-2.3-py3-none-any.whl", hash = "sha256:8f6c8061e4b5690f1ee2d8e5760940ef06bf78a5bfa033d11178377559c749cf", size = 83288, upload-time = "2021-11-18T09:32:27.193Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -754,14 +741,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.17.1"
|
||||
version = "3.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -792,14 +779,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "drf-spectacular-sidecar"
|
||||
version = "2026.5.1"
|
||||
version = "2026.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/e9/600a7806111c6d1ba49d7e31bfc978a745682724310ad29b0d2c068f1f73/drf_spectacular_sidecar-2026.5.1.tar.gz", hash = "sha256:cdeca03e32859318a563b5733d5fc196c8b563a178a85fd380e227ed642c19ca", size = 2466161, upload-time = "2026-05-01T12:04:01.118Z" }
|
||||
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/9c/2e/29e8676c87201a174491d0e1104df99d27258b3b7e0dc15a0e9b11652d86/drf_spectacular_sidecar-2026.5.1-py3-none-any.whl", hash = "sha256:2af264e5b125fedc5d382be1349f7f736f128bc8fa05c3be3fc7f3e5b282d3c4", size = 2488545, upload-time = "2026-05-01T12:03:59.269Z" },
|
||||
{ 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]]
|
||||
@@ -944,14 +931,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "26.0.0"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -965,11 +952,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.18"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1005,7 +992,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "9.13.0"
|
||||
version = "9.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -1015,14 +1002,13 @@ dependencies = [
|
||||
{ name = "matplotlib-inline" },
|
||||
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pygments" },
|
||||
{ name = "stack-data" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" }
|
||||
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/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" },
|
||||
{ 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]]
|
||||
@@ -1187,7 +1173,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.20.0"
|
||||
version = "1.16.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1218,7 +1204,6 @@ dependencies = [
|
||||
{ name = "markdown" },
|
||||
{ name = "mozilla-django-oidc" },
|
||||
{ name = "nested-multipart-parser" },
|
||||
{ name = "phonenumbers" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyjwt" },
|
||||
@@ -1253,11 +1238,11 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = "==3.14.0" },
|
||||
{ name = "boto3", specifier = "==1.43.14" },
|
||||
{ name = "aiohttp", specifier = "==3.13.4" },
|
||||
{ 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.3" },
|
||||
{ name = "celery", extras = ["redis"], specifier = "==5.6.2" },
|
||||
{ name = "dj-database-url", specifier = "==3.1.2" },
|
||||
{ name = "django", specifier = "==5.2.14" },
|
||||
{ name = "django-configurations", specifier = "==2.5.1" },
|
||||
@@ -1265,31 +1250,30 @@ requires-dist = [
|
||||
{ name = "django-countries", specifier = "==8.2.0" },
|
||||
{ name = "django-filter", specifier = "==25.2" },
|
||||
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.26" },
|
||||
{ name = "django-parler", specifier = "==2.4" },
|
||||
{ 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.17.1" },
|
||||
{ name = "djangorestframework", specifier = "==3.16.1" },
|
||||
{ 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" },
|
||||
{ name = "gunicorn", specifier = "==26.0.0" },
|
||||
{ name = "gunicorn", specifier = "==25.1.0" },
|
||||
{ name = "jsonschema", specifier = "==4.26.0" },
|
||||
{ name = "livekit-api", specifier = "==1.1.0" },
|
||||
{ name = "markdown", specifier = "==3.10.2" },
|
||||
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
|
||||
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
|
||||
{ name = "phonenumbers", specifier = "==9.0.31" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.4" },
|
||||
{ name = "pydantic", specifier = "==2.13.4" },
|
||||
{ name = "pyjwt", specifier = "==2.13.0" },
|
||||
{ name = "python-frontmatter", specifier = "==1.3.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.34.2" },
|
||||
{ name = "sentry-sdk", specifier = "==2.60.0" },
|
||||
{ name = "requests", specifier = "==2.33.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.54.0" },
|
||||
{ name = "urllib3", specifier = "==2.7.0" },
|
||||
{ name = "whitenoise", specifier = "==6.12.0" },
|
||||
]
|
||||
@@ -1297,21 +1281,21 @@ requires-dist = [
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "django-extensions", specifier = "==4.1" },
|
||||
{ name = "drf-spectacular-sidecar", specifier = "==2026.5.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.13.0" },
|
||||
{ name = "pyfakefs", specifier = "==6.2.0" },
|
||||
{ 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.3" },
|
||||
{ name = "pytest-cov", specifier = "==7.1.0" },
|
||||
{ name = "pytest-cov", specifier = "==7.0.0" },
|
||||
{ name = "pytest-django", specifier = "==4.12.0" },
|
||||
{ name = "pytest-icdiff", specifier = "==0.9" },
|
||||
{ name = "pytest-xdist", specifier = "==3.8.0" },
|
||||
{ name = "responses", specifier = "==0.26.1" },
|
||||
{ name = "ruff", specifier = "==0.15.14" },
|
||||
{ name = "types-requests", specifier = "==2.33.0.20260518" },
|
||||
{ name = "responses", specifier = "==0.26.0" },
|
||||
{ name = "ruff", specifier = "==0.15.6" },
|
||||
{ name = "types-requests", specifier = "==2.32.4.20260107" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1446,15 +1430,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phonenumbers"
|
||||
version = "9.0.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/20/51f0eafd5819e923d09c6f508a764b84d00e7e7d96c4087627a1adab8d30/phonenumbers-9.0.31.tar.gz", hash = "sha256:287dbd012b12cf9bcc4803b55a29e0d204db5a0df747d38c9130fee1b1b6b0aa", size = 2306679, upload-time = "2026-05-23T06:09:06.338Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/d3/17268202ea3fa5a597c98ca5dd178ca013f96b42373c54604e59b3bb8d02/phonenumbers-9.0.31-py2.py3-none-any.whl", hash = "sha256:f308c1f425e2637b92ff34a174f0888ddf47aacfa6199531f7097e00ad90f57c", size = 2595455, upload-time = "2026-05-23T06:09:03.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
@@ -1636,44 +1611,16 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.4"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
|
||||
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/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
|
||||
{ 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]
|
||||
@@ -1683,31 +1630,31 @@ binary = [
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.3.4"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
|
||||
{ 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]]
|
||||
@@ -1739,7 +1686,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.4"
|
||||
version = "2.12.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
@@ -1747,74 +1694,71 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||
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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||
{ 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]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.4"
|
||||
version = "2.41.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyfakefs"
|
||||
version = "6.2.0"
|
||||
version = "6.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/0d/c80012ee6e885c293ad63c5f5b049d3ef3fd2b32bbe6fa8739145f392ec6/pyfakefs-6.2.0.tar.gz", hash = "sha256:e59a36db447bf509ce9c97ab3d1510c08cc51895c5311325a560a5e5b5dc1940", size = 228273, upload-time = "2026-04-12T13:38:50.411Z" }
|
||||
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/b2/80/97571ac8295289c267367b7b60aadeae1a9a841e83f0a96ad9b65d1dd3c0/pyfakefs-6.2.0-py3-none-any.whl", hash = "sha256:0968a49db692694ffed420e54a9f1cbae4636637b880e8ab09c8ccc0f11bd7ae", size = 241113, upload-time = "2026-04-12T13:38:48.927Z" },
|
||||
{ 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]]
|
||||
@@ -1828,11 +1772,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.13.0"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
|
||||
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/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
|
||||
{ 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]]
|
||||
@@ -1895,16 +1839,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.1.0"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1960,14 +1904,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-frontmatter"
|
||||
version = "1.3.0"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/e8/79cbe69864d44f3b48e70ebee0a872a7d5a4e7150c9f8577ed7a5beefff0/python_frontmatter-1.3.0.tar.gz", hash = "sha256:acc73e477a568dc2a25c9e130c6c68ae8daa8c204c8f7e813db47d6a7280dcf2", size = 8322, upload-time = "2026-05-20T19:21:44.164Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/de/910fa208120314a12f9a88ea63e03707261692af782c99283f1a2c8a5e6f/python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d", size = 16256, upload-time = "2024-01-16T18:50:04.052Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/a3/17c284b4f4d8ad50f0f9ba70ad8fcc35c777aeafcdbbffdd91bbdc5ab379/python_frontmatter-1.3.0-py3-none-any.whl", hash = "sha256:9f7dd9260bec99044219159a329f64f039087f9d1a2124c9442556f2fe6f82ec", size = 10562, upload-time = "2026-05-20T19:21:43.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834, upload-time = "2024-01-16T18:50:00.911Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2039,7 +1983,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
version = "2.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -2047,9 +1991,9 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2066,16 +2010,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "responses"
|
||||
version = "0.26.1"
|
||||
version = "0.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" }
|
||||
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/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" },
|
||||
{ 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]]
|
||||
@@ -2146,52 +2090,52 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.14"
|
||||
version = "0.15.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
||||
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/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
||||
{ 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]]
|
||||
name = "s3transfer"
|
||||
version = "0.17.1"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/b3/bcdc2f58fa92592db511beda154c2c08d28f21f6c4637f06a42a24b10c21/s3transfer-0.17.1.tar.gz", hash = "sha256:042dd5e3b1b512355e35a23f0223e426b7042e80b97830ea2680ddce327fc45e", size = 159439, upload-time = "2026-05-26T19:45:01.714Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl", hash = "sha256:5b9827d1044159bbb01b86ef8902760ea39281927f5de31de75e1d657177bf4c", size = 88264, upload-time = "2026-05-26T19:45:00.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.60.0"
|
||||
version = "2.54.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/a2/2e6c090db384cc515069f4f85542bd5baf6786852073020ea73d4a76d3ea/sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978", size = 452946, upload-time = "2026-05-13T13:34:52.516Z" }
|
||||
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/29/41/f2b800b7f12a05dd48c2a6280d4dd812d1425fc66ed3fe3fd99420c41d1a/sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803", size = 475616, upload-time = "2026-05-13T13:34:50.259Z" },
|
||||
{ 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]]
|
||||
@@ -2255,14 +2199,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.33.0.20260518"
|
||||
version = "2.32.4.20260107"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'prettier'
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs', 'styled-system'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -45,8 +45,6 @@ USER root
|
||||
|
||||
# Security patches for known CVEs
|
||||
RUN apk update && apk upgrade \
|
||||
libcrypto3>=3.5.7-r0 \
|
||||
libssl3>=3.5.7-r0 \
|
||||
musl \
|
||||
musl-utils \
|
||||
zlib>=1.3.2-r0 \
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
// src/frontend/eslint.config.js
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import reactPlugin from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||
import globals from 'globals'
|
||||
|
||||
export default tseslint.config(
|
||||
// Global ignores (replaces .eslintignore)
|
||||
{
|
||||
ignores: [
|
||||
'dist/**',
|
||||
'node_modules/**',
|
||||
'coverage/**',
|
||||
'src/styled-system/**',
|
||||
],
|
||||
},
|
||||
|
||||
// Base JS recommended rules
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript recommended rules (parser + rules bundled together)
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// React-specific config
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
'jsx-a11y': jsxA11y,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
rules: {
|
||||
// React rules
|
||||
...reactPlugin.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/react-in-jsx-scope': 'off', // not needed with React 17+
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
|
||||
// React Compiler rules (v7+) — disable until migrated to React 19
|
||||
'react-hooks/react-compiler': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/preserve-manual-memoization': 'off',
|
||||
'react-hooks/immutability': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
|
||||
// jsx-a11y recommended rules
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
|
||||
// TypeScript rules you may want to adjust
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// CJS and root-level JS config files (postcss.config.js, etc.)
|
||||
{
|
||||
files: ['**/*.cjs', '*.config.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -6,6 +6,22 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<!-- Font URLs are resolved and replaced by Vite during the build process. Font loading failures will not break the application. -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin="anonymous"
|
||||
href="/node_modules/@fontsource/material-icons-outlined/files/material-icons-outlined-latin-400-normal.woff2"
|
||||
type="font/woff2"
|
||||
/>
|
||||
<!-- Font URLs are resolved and replaced by Vite during the build process. Font loading failures will not break the application. -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin="anonymous"
|
||||
href="/node_modules/@fontsource-variable/material-symbols-outlined/files/material-symbols-outlined-latin-wght-normal.woff2"
|
||||
type="font/woff2"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
</head>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.20.0",
|
||||
"version": "1.16.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
"build": "panda codegen && tsc -b && vite build",
|
||||
"build:debug": "VITE_ANALYZE=true npm run build -- --debug",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"preview": "vite preview",
|
||||
"i18n:extract": "npx i18next -c i18next-parser.config.json",
|
||||
"format": "prettier --write ./src",
|
||||
@@ -17,56 +15,57 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.34",
|
||||
"@fontsource/material-icons-outlined": "5.2.6",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@livekit/components-react": "2.9.21",
|
||||
"@livekit/components-react": "2.9.19",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
"@livekit/track-processors": "0.7.2",
|
||||
"@pandacss/preset-panda": "1.11.1",
|
||||
"@livekit/track-processors": "0.7.0",
|
||||
"@pandacss/preset-panda": "1.8.2",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.10.0",
|
||||
"@remixicon/react": "4.9.0",
|
||||
"@tanstack/react-query": "5.100.14",
|
||||
"@react-types/overlays": "3.9.3",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.90.21",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"crisp-sdk-web": "1.0.27",
|
||||
"derive-valtio": "0.2.0",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "26.2.0",
|
||||
"i18next": "25.8.8",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.19.0",
|
||||
"posthog-js": "1.376.0",
|
||||
"libphonenumber-js": "1.12.10",
|
||||
"livekit-client": "2.17.1",
|
||||
"posthog-js": "1.342.1",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.14.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "17.0.8",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
"wouter": "3.10.0"
|
||||
"valtio": "2.3.0",
|
||||
"wouter": "3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.4",
|
||||
"@pandacss/dev": "1.11.1",
|
||||
"@tanstack/eslint-plugin-query": "5.100.14",
|
||||
"@tanstack/react-query-devtools": "5.100.14",
|
||||
"@tanstack/eslint-plugin-query": "5.91.4",
|
||||
"@tanstack/react-query-devtools": "5.91.3",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/node": "22.16.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@vitejs/plugin-react": "6.0.2",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.35.1",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"eslint-plugin-react-refresh": "0.5.2",
|
||||
"globals": "17.6.0",
|
||||
"postcss": "8.5.15",
|
||||
"prettier": "3.8.3",
|
||||
"rollup-plugin-visualizer": "7.0.1",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.60.1",
|
||||
"vite": "8.0.14",
|
||||
"vite-plugin-svgr": "5.2.0"
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.14",
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.3.2",
|
||||
"vite-tsconfig-paths": "6.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,3 +1,4 @@
|
||||
import '@livekit/components-styles'
|
||||
import '@/styles/index.css'
|
||||
import { Suspense } from 'react'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
|
||||
@@ -2,8 +2,8 @@ import { fetchApi } from './fetchApi'
|
||||
import { keys } from './queryKeys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { RecordingMode } from '@/features/recording'
|
||||
import type { Track } from 'livekit-client'
|
||||
type Source = Track.Source
|
||||
import { Track } from 'livekit-client'
|
||||
import Source = Track.Source
|
||||
|
||||
export interface ApiConfig {
|
||||
analytics?: {
|
||||
@@ -44,7 +44,7 @@ export interface ApiConfig {
|
||||
}
|
||||
telephony: {
|
||||
enabled: boolean
|
||||
international_phone_number?: string
|
||||
phone_number?: string
|
||||
default_country?: string
|
||||
}
|
||||
manifest_link?: string
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-280h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm-80 480q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 350 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 178 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q17-72 85-137t145-65q33 0 56.5 23.5T520-716v242l64-62 56 56-160 160-160-160 56-56 64 62v-242q-76 14-118 73.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h480q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-48-22-89.5T600-680v-93q74 35 117 103.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H260Zm220-358Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 488 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M325-111.5q-73-31.5-127.5-86t-86-127.5Q80-398 80-480.5t31.5-155q31.5-72.5 86-127t127.5-86Q398-880 480.5-880t155 31.5q72.5 31.5 127 86t86 127Q880-563 880-480.5T848.5-325q-31.5 73-86 127.5t-127 86Q563-80 480.5-80T325-111.5ZM480-162q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 996 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 278 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 328 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M565-395q35-35 35-85t-35-85q-35-35-85-35t-85 35q-35 35-35 85t35 85q35 35 85 35t85-35ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 495 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M80-40v-80h800v80H80Zm80-120v-240q-33-54-51-114.5T91-638q0-61 15.5-120T143-874q8-21 26-33.5t40-12.5q31 0 53 21t18 50l-11 91q-6 48 8.5 91t43.5 75.5q29 32.5 70 52t89 19.5q60 0 120.5 12.5T706-472q45 23 69.5 58.5T800-326v166H160Zm80-80h480v-86q0-24-12-42.5T674-398q-41-20-95-31t-99-11q-66 0-122.5-27t-96-72.5Q222-585 202-644.5T190-768q-10 30-14.5 64t-4.5 66q0 58 20.5 111.5T240-422v182Zm127-367q-47-47-47-113t47-113q47-47 113-47t113 47q47 47 47 113t-47 113q-47 47-113 47t-113-47Zm169.5-56.5Q560-687 560-720t-23.5-56.5Q513-800 480-800t-56.5 23.5Q400-753 400-720t23.5 56.5Q447-640 480-640t56.5-23.5ZM320-160v-37q0-67 46.5-115T480-360h160v80H480q-34 0-57 24.5T400-197v37h-80Zm160-80Zm0-480Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 808 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M158-242q-37-50-57.5-110.5T80-480q0-67 20-127t57-110l58 57q-26 38-40.5 83.5T160-480q0 51 14.5 97t40.5 84l-57 57ZM480-80q-67 0-127-20t-110-57l57-58q38 26 83.5 40.5T480-160q51 0 96.5-14.5T660-215l57 58q-50 37-110 57T480-80Zm322-162-57-57q26-38 40.5-84t14.5-97q0-51-14.5-96.5T745-660l58-57q37 50 57 110t20 127q0 67-20.5 127.5T802-242ZM299-745l-57-57q50-37 110.5-57.5T480-880q68 0 128 20.5T718-802l-57 57q-38-26-84-40.5T480-800q-51 0-97 14.5T299-745Zm181 465q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 677 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M680-560q-33 0-56.5-23T600-640v-160q0-34 23.5-57t56.5-23q34 0 57 23t23 57v160q0 34-23 57t-57 23ZM200-80q-33 0-56.5-23.5T120-160v-640q0-33 23.5-56.5T200-880h320v80H200v640h440v-80h80v80q0 33-23.5 56.5T640-80H200Zm80-160v-80h280v80H280Zm0-120v-80h200v80H280Zm440 40h-80v-104q-77-14-128.5-74.5T460-640h80q0 58 41 99t99 41q59 0 99.5-41t40.5-99h80q0 81-51 141.5T720-424v104Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 494 B |
@@ -1,14 +1,20 @@
|
||||
import { silenceLiveKitLogs } from '@/utils/livekit'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
||||
import { useSupport } from '@/features/support/hooks/useSupport'
|
||||
import { useSyncUserPreferencesWithBackend } from '@/features/auth/api/useSyncUserPreferencesWithBackend'
|
||||
import { useSyncUserPreferencesWithBackend } from '@/features/auth'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const AppInitialization = () => {
|
||||
const { data } = useConfig()
|
||||
useSyncUserPreferencesWithBackend()
|
||||
|
||||
const { analytics = {}, support = {}, custom_css_url = '' } = data ?? {}
|
||||
const {
|
||||
analytics = {},
|
||||
support = {},
|
||||
silence_livekit_debug_logs = false,
|
||||
custom_css_url = '',
|
||||
} = data ?? {}
|
||||
|
||||
useAnalytics(analytics)
|
||||
useSupport(support)
|
||||
@@ -23,5 +29,7 @@ export const AppInitialization = () => {
|
||||
}
|
||||
}, [custom_css_url])
|
||||
|
||||
silenceLiveKitLogs(silence_livekit_debug_logs)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LinkButton } from '@/primitives'
|
||||
import { authUrl } from '@/features/auth'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { ProConnectButton } from './ProConnectButton'
|
||||
import { authUrl } from '@/features/auth/utils/authUrl'
|
||||
|
||||
type LoginButtonProps = {
|
||||
proConnectHint?: boolean // Hide hint in layouts where space doesn't allow it.
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'wouter'
|
||||
import { type PostHog } from 'posthog-js'
|
||||
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
||||
import { useUser } from '@/features/auth/api/useUser'
|
||||
|
||||
let posthog: PostHog | null = null
|
||||
|
||||
const getPosthog = async () => {
|
||||
if (!posthog) posthog = (await import('posthog-js')).default
|
||||
return posthog
|
||||
}
|
||||
import posthog from 'posthog-js'
|
||||
import { ApiUser } from '@/features/auth/api/ApiUser'
|
||||
|
||||
export const startAnalyticsSession = (data: ApiUser) => {
|
||||
getPosthog().then((ph) => {
|
||||
if (ph._isIdentified()) return
|
||||
const { id, email } = data
|
||||
ph.identify(id, { email })
|
||||
})
|
||||
if (posthog._isIdentified()) return
|
||||
const { id, email } = data
|
||||
posthog.identify(id, { email })
|
||||
}
|
||||
|
||||
export const terminateAnalyticsSession = async () => {
|
||||
const ph = await getPosthog()
|
||||
if (!ph._isIdentified()) return
|
||||
ph.reset()
|
||||
export const terminateAnalyticsSession = () => {
|
||||
if (!posthog._isIdentified()) return
|
||||
posthog.reset()
|
||||
}
|
||||
|
||||
export type useAnalyticsProps = {
|
||||
@@ -33,26 +22,18 @@ export type useAnalyticsProps = {
|
||||
|
||||
export const useAnalytics = ({ id, host, isDisabled }: useAnalyticsProps) => {
|
||||
const [location] = useLocation()
|
||||
const { user } = useUser()
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !host || isDisabled) return
|
||||
getPosthog().then((ph) => {
|
||||
if (ph.__loaded) return
|
||||
ph.init(id, { api_host: host, person_profiles: 'always' })
|
||||
if (posthog.__loaded) return
|
||||
posthog.init(id, {
|
||||
api_host: host,
|
||||
person_profiles: 'always',
|
||||
})
|
||||
}, [id, host, isDisabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
startAnalyticsSession(user)
|
||||
}, [user])
|
||||
|
||||
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
|
||||
useEffect(() => {
|
||||
getPosthog().then((ph) => {
|
||||
ph.capture('$pageview')
|
||||
})
|
||||
posthog.capture('$pageview')
|
||||
}, [location])
|
||||
|
||||
return null
|
||||
|
||||
@@ -2,7 +2,16 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { keys } from '@/api/queryKeys'
|
||||
import { fetchUser } from './fetchUser'
|
||||
import { type ApiUser } from './ApiUser'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import {
|
||||
startAnalyticsSession,
|
||||
terminateAnalyticsSession,
|
||||
} from '@/features/analytics/hooks/useAnalytics'
|
||||
import {
|
||||
initializeSupportSession,
|
||||
terminateSupportSession,
|
||||
} from '@/features/support/hooks/useSupport'
|
||||
import { logoutUrl } from '../utils/logoutUrl'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
/**
|
||||
@@ -29,12 +38,26 @@ export const useUser = (
|
||||
}, [data, opts, isConfigLoading])
|
||||
|
||||
const query = useQuery({
|
||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||
queryKey: [keys.user],
|
||||
queryFn: () => fetchUser(options),
|
||||
staleTime: Infinity,
|
||||
enabled: !isConfigLoading,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (query?.data) {
|
||||
startAnalyticsSession(query.data)
|
||||
initializeSupportSession(query.data)
|
||||
}
|
||||
}, [query.data])
|
||||
|
||||
const logout = () => {
|
||||
terminateAnalyticsSession()
|
||||
terminateSupportSession()
|
||||
window.location.href = logoutUrl()
|
||||
}
|
||||
|
||||
const isLoggedIn =
|
||||
query.status === 'success' ? query.data !== false : undefined
|
||||
const isLoggedOut = isLoggedIn === false
|
||||
@@ -44,5 +67,6 @@ export const useUser = (
|
||||
user: isLoggedOut ? undefined : (query.data as ApiUser | undefined),
|
||||
isLoggedIn,
|
||||
isLoading: query.isLoading,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useUser } from '../api/useUser'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { useUser } from './api/useUser'
|
||||
export { useSyncUserPreferencesWithBackend } from './api/useSyncUserPreferencesWithBackend'
|
||||
export { authUrl } from './utils/authUrl'
|
||||
export { UserAware } from './components/UserAware'
|
||||