mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-01 14:42:15 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5790bb1e47 | |||
| ca80540f7e | |||
| 80f3af0690 | |||
| 399617e247 |
@@ -14,6 +14,7 @@ and this project adheres to
|
|||||||
- ✨(frontend) add configurable documentation menu item
|
- ✨(frontend) add configurable documentation menu item
|
||||||
- ✨(frontend) allow promoting authenticated participants
|
- ✨(frontend) allow promoting authenticated participants
|
||||||
- ✨(frontend) introduce an "unauthenticated" participant badge
|
- ✨(frontend) introduce an "unauthenticated" participant badge
|
||||||
|
- ✨(backend) add roomkit viewset to start a room without WebRTC join
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ and this project adheres to
|
|||||||
- 📝(legal) update terms of service
|
- 📝(legal) update terms of service
|
||||||
- 💄(frontend) render Avatar initials in uppercase
|
- 💄(frontend) render Avatar initials in uppercase
|
||||||
- 💄(frontend) improve participant name rendering in the list
|
- 💄(frontend) improve participant name rendering in the list
|
||||||
|
- 🚚(backend) rename TelephonyService to SIPManagement
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -389,6 +389,12 @@ build-k8s-cluster: \
|
|||||||
./bin/start-kind.sh
|
./bin/start-kind.sh
|
||||||
.PHONY: build-k8s-cluster
|
.PHONY: build-k8s-cluster
|
||||||
|
|
||||||
|
build-k8s-cluster-orbstack: ## setup the kubernetes environment on OrbStack's built-in cluster (macOS)
|
||||||
|
build-k8s-cluster-orbstack: \
|
||||||
|
env.d/development/kube-secret
|
||||||
|
./bin/start-orbstack.sh
|
||||||
|
.PHONY: build-k8s-cluster-orbstack
|
||||||
|
|
||||||
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
|
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
|
||||||
DEV_ENV=dev-keycloak tilt up --namespace=meet -f ./bin/Tiltfile
|
DEV_ENV=dev-keycloak tilt up --namespace=meet -f ./bin/Tiltfile
|
||||||
.PHONY: build-k8s-cluster
|
.PHONY: build-k8s-cluster
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
|
load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
|
||||||
load('ext://namespace', 'namespace_create', 'namespace_inject')
|
load('ext://namespace', 'namespace_create', 'namespace_inject')
|
||||||
|
|
||||||
|
# OrbStack's built-in cluster (macOS) is a supported alternative to kind.
|
||||||
|
# Recent Tilt versions (>= 0.33) detect it as a local dev cluster; this is
|
||||||
|
# a no-op for kind and a safety net for older Tilt versions.
|
||||||
|
allow_k8s_contexts('orbstack')
|
||||||
|
|
||||||
namespace_create('meet')
|
namespace_create('meet')
|
||||||
|
|
||||||
DEV_ENV = os.getenv('DEV_ENV', 'dev-keycloak')
|
DEV_ENV = os.getenv('DEV_ENV', 'dev-keycloak')
|
||||||
|
|||||||
Executable
+182
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Bootstrap the local dev environment on OrbStack's built-in Kubernetes
|
||||||
|
# cluster (macOS) instead of kind.
|
||||||
|
#
|
||||||
|
# This replicates what bin/start-kind.sh (numerique-gouv/tools
|
||||||
|
# kind/create_cluster.sh) provides, minus what OrbStack makes unnecessary:
|
||||||
|
# - no kind cluster: OrbStack ships a lightweight single-node cluster
|
||||||
|
# - no local registry (kind-registry): OrbStack's cluster shares the
|
||||||
|
# Docker image store, so images built by Tilt are directly visible
|
||||||
|
# to pods. Tilt detects the "orbstack" context as a local cluster
|
||||||
|
# and skips pushing images entirely.
|
||||||
|
#
|
||||||
|
# Requirements: OrbStack (with Kubernetes enabled), kubectl, mkcert, curl.
|
||||||
|
set -o errexit
|
||||||
|
|
||||||
|
APPLICATION=${1:-meet}
|
||||||
|
CONTEXT="orbstack"
|
||||||
|
|
||||||
|
echo "0. Check OrbStack Kubernetes is available"
|
||||||
|
if ! command -v mkcert >/dev/null 2>&1; then
|
||||||
|
echo "❌ mkcert is not installed. Install it first: brew install mkcert"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! kubectl config get-contexts -o name | grep -qx "${CONTEXT}"; then
|
||||||
|
echo "Context '${CONTEXT}' not found. Trying to start OrbStack Kubernetes..."
|
||||||
|
if command -v orb >/dev/null 2>&1; then
|
||||||
|
orb start k8s
|
||||||
|
else
|
||||||
|
echo "❌ Enable Kubernetes in OrbStack (Settings > Kubernetes) and retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
kubectl config use-context "${CONTEXT}"
|
||||||
|
|
||||||
|
echo "0b. Check ports 80/443 are free on localhost"
|
||||||
|
# OrbStack forwards LoadBalancer service ports to 127.0.0.1. If the kind
|
||||||
|
# cluster is still running, its docker proxy already holds 80/443.
|
||||||
|
# Skip the check if ingress-nginx is already installed here: in that case
|
||||||
|
# the listener on 80/443 is our own LoadBalancer.
|
||||||
|
if ! kubectl -n ingress-nginx get deployment ingress-nginx-controller >/dev/null 2>&1; then
|
||||||
|
for port in 80 443; do
|
||||||
|
if lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||||
|
echo "❌ Port ${port} is already in use on the host."
|
||||||
|
echo " If the kind cluster is running, delete it first:"
|
||||||
|
echo " kind delete cluster --name suite"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "1. Create ca"
|
||||||
|
CURRENT_DIR=$(pwd)
|
||||||
|
mkcert -install
|
||||||
|
cd /tmp
|
||||||
|
mkcert "127.0.0.1.nip.io" "*.127.0.0.1.nip.io"
|
||||||
|
cd "${CURRENT_DIR}"
|
||||||
|
|
||||||
|
echo "2. Install ingress-nginx (cloud provider: LoadBalancer service)"
|
||||||
|
# OrbStack exposes LoadBalancer services on 127.0.0.1, so the cloud
|
||||||
|
# manifest replaces kind's hostPort-based deploy. Every sub-step below is
|
||||||
|
# guarded individually so the script is safe to re-run after a partial
|
||||||
|
# failure (unlike the upstream kind script, which guards the whole block
|
||||||
|
# on namespace existence).
|
||||||
|
|
||||||
|
# Make sure no stale registry configmap tells Tilt to push to localhost:5001
|
||||||
|
# (there is no registry on OrbStack).
|
||||||
|
kubectl -n kube-public delete configmap local-registry-hosting --ignore-not-found
|
||||||
|
|
||||||
|
if ! kubectl -n ingress-nginx get deployment ingress-nginx-controller >/dev/null 2>&1; then
|
||||||
|
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
|
||||||
|
fi
|
||||||
|
if ! kubectl -n ingress-nginx get deployment nginx-errors >/dev/null 2>&1; then
|
||||||
|
kubectl apply -n ingress-nginx -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/refs/heads/main/docs/examples/customization/custom-errors/custom-default-backend.yaml
|
||||||
|
fi
|
||||||
|
kubectl -n ingress-nginx create secret tls mkcert --key /tmp/127.0.0.1.nip.io+1-key.pem --cert /tmp/127.0.0.1.nip.io+1.pem || echo ok
|
||||||
|
|
||||||
|
# The meet charts render Ingresses without ingressClassName. The kind
|
||||||
|
# provider manifest handles this via --watch-ingress-without-class=true;
|
||||||
|
# the cloud manifest does not, so add it here (otherwise: 404 everywhere).
|
||||||
|
if ! kubectl -n ingress-nginx get deployment ingress-nginx-controller -o jsonpath='{.spec.template.spec.containers[0].args}' | grep -q 'watch-ingress-without-class'; then
|
||||||
|
kubectl -n ingress-nginx patch deployments.apps ingress-nginx-controller --type 'json' -p '[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--watch-ingress-without-class=true"},{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--default-ssl-certificate=ingress-nginx/mkcert"},{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--default-backend-service=ingress-nginx/nginx-errors"}
|
||||||
|
]'
|
||||||
|
fi
|
||||||
|
if ! kubectl -n ingress-nginx get deployment nginx-errors -o jsonpath='{.spec.template.spec.containers[0].image}' | grep -q 'error-pages'; then
|
||||||
|
kubectl -n ingress-nginx patch deployment nginx-errors --type=json -p='[
|
||||||
|
{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "ghcr.io/tarampampam/error-pages:3.3.0"},
|
||||||
|
{"op": "add", "path": "/spec/template/spec/containers/0/env", "value": [{"name": "TEMPLATE_NAME", "value": "ghost"}, {"name": "SHOW_DETAILS", "value": "false"}, {"name": "SEND_SAME_HTTP_CODE", "value": "true"}]}
|
||||||
|
]'
|
||||||
|
fi
|
||||||
|
cat <<EOF | kubectl apply -n ingress-nginx -f -
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
allow-snippet-annotations: "true"
|
||||||
|
annotations-risk-level: Critical
|
||||||
|
custom-http-errors: 500,501,502,503,504
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: ingress-nginx-controller
|
||||||
|
namespace: ingress-nginx
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "2b. Wait for the ingress controller to be ready"
|
||||||
|
kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller --timeout=180s
|
||||||
|
|
||||||
|
echo "3. Patch CoreDNS so in-cluster pods resolve *.127.0.0.1.nip.io to the ingress"
|
||||||
|
# nip.io resolves to 127.0.0.1, which inside a pod is the pod itself.
|
||||||
|
# Rewrite these names to the ingress-nginx service, like the kind setup does.
|
||||||
|
# Unlike kind, we amend OrbStack's existing Corefile instead of replacing it.
|
||||||
|
if ! kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}' | grep -q '127\.0\.0\.1\.nip\.io'; then
|
||||||
|
kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}' \
|
||||||
|
| awk '/forward \./ && !done { print " rewrite stop {"; print " name regex (.*).127.0.0.1.nip.io ingress-nginx-controller.ingress-nginx.svc.cluster.local answer auto"; print " }"; done=1 } { print }' \
|
||||||
|
>/tmp/Corefile.orbstack
|
||||||
|
kubectl -n kube-system create configmap coredns --from-file=Corefile=/tmp/Corefile.orbstack --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl -n kube-system rollout restart deployments/coredns
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! kubectl get ns "${APPLICATION}" >/dev/null 2>&1; then
|
||||||
|
echo "4. Setup namespace"
|
||||||
|
kubectl create ns "${APPLICATION}"
|
||||||
|
fi
|
||||||
|
kubectl config set-context --current --namespace="${APPLICATION}"
|
||||||
|
kubectl -n "${APPLICATION}" create secret generic mkcert --from-file=rootCA.pem="$(mkcert -CAROOT)/rootCA.pem" || echo ok
|
||||||
|
|
||||||
|
if ! kubectl get configmap certifi -n "${APPLICATION}" >/dev/null 2>&1; then
|
||||||
|
echo "5. Inject our custom CA in a configmap for certifi"
|
||||||
|
curl https://raw.githubusercontent.com/certifi/python-certifi/refs/heads/master/certifi/cacert.pem -o /tmp/cacert.pem
|
||||||
|
cat "$(mkcert -CAROOT)/rootCA.pem" >>/tmp/cacert.pem
|
||||||
|
kubectl -n "${APPLICATION}" create configmap certifi --from-file=cacert.pem=/tmp/cacert.pem
|
||||||
|
kubectl -n "${APPLICATION}" create secret generic certifi --from-file=/tmp/cacert.pem || echo ok
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "5b. Smoke test: the ingress chain answers on https://127.0.0.1"
|
||||||
|
# Before Tilt deploys the app this returns the styled 404 from the default
|
||||||
|
# backend — that still proves LB -> controller works. 000 means the
|
||||||
|
# LoadBalancer is not bound to localhost.
|
||||||
|
HTTP_CODE=$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 https://127.0.0.1/ || true)
|
||||||
|
if [ "${HTTP_CODE}" = "000" ]; then
|
||||||
|
echo "⚠️ Nothing answered on https://127.0.0.1 — check the LoadBalancer:"
|
||||||
|
echo " kubectl -n ingress-nginx get svc ingress-nginx-controller"
|
||||||
|
else
|
||||||
|
echo "✅ Ingress reachable (HTTP ${HTTP_CODE})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "6. Check pod readiness across all namespaces..."
|
||||||
|
|
||||||
|
sleep_interval=10
|
||||||
|
|
||||||
|
echo "Initial wait time: $((sleep_interval * 2)) seconds…"
|
||||||
|
sleep $((sleep_interval * 2))
|
||||||
|
|
||||||
|
check_pods_ready() {
|
||||||
|
local max_attempts=60 # Maximum number of attempts (10 minutes with 10s intervals)
|
||||||
|
local attempt=1
|
||||||
|
|
||||||
|
while [ $attempt -le $max_attempts ]; do
|
||||||
|
echo "Attempt $attempt/$max_attempts - Checking pod status..."
|
||||||
|
|
||||||
|
not_ready_count=$( kubectl get po -A --no-headers | grep -v -E "Running|Completed"| wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$not_ready_count" -eq 0 ]; then
|
||||||
|
echo "✅ All pods are ready!"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "⏳ $not_ready_count pod(s) still not ready. Waiting $sleep_interval seconds…"
|
||||||
|
sleep $sleep_interval
|
||||||
|
((attempt++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ Timeout: Some pods are still not ready after 10 minutes"
|
||||||
|
echo "Final pod status:"
|
||||||
|
kubectl get po -A
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if check_pods_ready; then
|
||||||
|
echo "🎉 Cluster is fully ready!"
|
||||||
|
else
|
||||||
|
echo "⚠️ Some pods may need manual intervention"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -143,3 +143,24 @@ $ make start-tilt-keycloak
|
|||||||
```
|
```
|
||||||
|
|
||||||
Monitor Tilt’s progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://meet.127.0.0.1.nip.io/](https://meet.127.0.0.1.nip.io/).
|
Monitor Tilt’s progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://meet.127.0.0.1.nip.io/](https://meet.127.0.0.1.nip.io/).
|
||||||
|
|
||||||
|
### Alternative: OrbStack's built-in Kubernetes (macOS)
|
||||||
|
|
||||||
|
If you use [OrbStack](https://orbstack.dev/) on macOS, you can run the stack on its built-in Kubernetes cluster instead of kind. It uses noticeably less RAM (no nested kubeadm node container) and no local registry is needed: OrbStack's cluster shares the Docker image store, so Tilt uses images directly without pushing.
|
||||||
|
|
||||||
|
Enable Kubernetes in OrbStack (Settings > Kubernetes), then:
|
||||||
|
|
||||||
|
```shellscript
|
||||||
|
$ make build-k8s-cluster-orbstack
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs ingress-nginx (exposed by OrbStack on `127.0.0.1:80/443`), the mkcert TLS certificates, and the CoreDNS rewrite for `*.127.0.0.1.nip.io`, then you start Tilt as usual:
|
||||||
|
|
||||||
|
```shellscript
|
||||||
|
$ make start-tilt-keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Ports 80/443 must be free: delete the kind cluster first if you used it (`kind delete cluster --name suite`).
|
||||||
|
- If you "Reset Kubernetes" in OrbStack, re-run `make build-k8s-cluster-orbstack`.
|
||||||
|
- kind remains the reference setup (matches CI and lets you pin the Kubernetes version).
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ name = "agents"
|
|||||||
version = "1.24.0"
|
version = "1.24.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.6.7",
|
"livekit-agents==1.6.4",
|
||||||
"livekit-plugins-deepgram==1.6.7",
|
"livekit-plugins-deepgram==1.6.4",
|
||||||
"livekit-plugins-silero==1.6.7",
|
"livekit-plugins-silero==1.6.4",
|
||||||
"livekit-plugins-kyutai-lasuite==0.0.6",
|
"livekit-plugins-kyutai-lasuite==0.0.6",
|
||||||
"python-dotenv==1.2.2",
|
"python-dotenv==1.2.2",
|
||||||
"protobuf==7.35.1",
|
"protobuf==6.33.6",
|
||||||
"minio==7.2.20",
|
"minio==7.2.20",
|
||||||
"sentry-sdk==2.66.1",
|
"sentry-sdk==2.60.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff==0.16.0",
|
"ruff==0.15.19",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ class AnalyticsEvent(StrEnum):
|
|||||||
|
|
||||||
# Rooms
|
# Rooms
|
||||||
ROOM_CREATED = "room_created"
|
ROOM_CREATED = "room_created"
|
||||||
|
|
||||||
|
# Roomkit (meeting-room SIP devices)
|
||||||
|
ROOMKIT_JOINED = "roomkit_joined"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class FeatureFlag:
|
|||||||
"file_upload": "FILE_UPLOAD_ENABLED",
|
"file_upload": "FILE_UPLOAD_ENABLED",
|
||||||
"addons": "ADDONS_ENABLED",
|
"addons": "ADDONS_ENABLED",
|
||||||
"application": "APPLICATION_ENABLED",
|
"application": "APPLICATION_ENABLED",
|
||||||
|
"roomkit": "ROOMKIT_ENABLED",
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -73,3 +73,15 @@ class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle):
|
|||||||
"""Throttle Anonymous user requesting room generation callback"""
|
"""Throttle Anonymous user requesting room generation callback"""
|
||||||
|
|
||||||
scope = "creation_callback"
|
scope = "creation_callback"
|
||||||
|
|
||||||
|
|
||||||
|
class RoomKitJoinRateThrottle(MonitoredUserRateThrottle):
|
||||||
|
"""Throttle the LiveKit SIP module requesting roomkit joins.
|
||||||
|
|
||||||
|
The roomkit endpoints are authenticated as a machine user, so all requests
|
||||||
|
share a single throttle bucket. This is not a security measure against
|
||||||
|
brute-force attacks but a guard against accidental hammering from a buggy
|
||||||
|
SIP module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
scope = "roomkit_join"
|
||||||
|
|||||||
@@ -429,7 +429,14 @@ class Room(Resource):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Generate a unique n-digit pin code for new rooms."""
|
"""Generate a unique n-digit pin code for new rooms."""
|
||||||
if settings.ROOM_TELEPHONY_ENABLED and not self.pk and not self.pin_code:
|
|
||||||
|
# Roomkit devices also join by PIN, so a PIN is needed as soon as
|
||||||
|
# either integration is enabled.
|
||||||
|
if (
|
||||||
|
(settings.ROOM_TELEPHONY_ENABLED or settings.ROOMKIT_ENABLED)
|
||||||
|
and not self.pk
|
||||||
|
and not self.pin_code
|
||||||
|
):
|
||||||
self.pin_code = self.generate_unique_pin_code(
|
self.pin_code = self.generate_unique_pin_code(
|
||||||
length=settings.ROOM_TELEPHONY_PIN_LENGTH
|
length=settings.ROOM_TELEPHONY_PIN_LENGTH
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Meet core roomkit API endpoints for meeting-room (SIP) device integration."""
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""Authentication for the roomkit API of the Meet core app."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from rest_framework.authentication import BaseAuthentication
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
|
from core.recording.event.authentication import MachineUser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerToServerAuthentication(BaseAuthentication):
|
||||||
|
"""Custom authentication class for roomkit server-to-server requests.
|
||||||
|
|
||||||
|
Validates the Authorization header against the roomkit server-to-server
|
||||||
|
token. A valid PIN code is intentionally not enough to authenticate: the
|
||||||
|
endpoints are restricted to the LiveKit SIP module's credentials.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AUTH_HEADER = "Authorization"
|
||||||
|
TOKEN_TYPE = "Bearer" # noqa S105
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
"""Validate the Bearer token from the Authorization header.
|
||||||
|
|
||||||
|
Returns a (MachineUser, token) pair on success, and raises
|
||||||
|
AuthenticationFailed if the header is missing, malformed, or contains
|
||||||
|
an invalid token.
|
||||||
|
"""
|
||||||
|
required_token = settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN
|
||||||
|
if not required_token:
|
||||||
|
raise AuthenticationFailed("Server-to-server token is not configured.")
|
||||||
|
|
||||||
|
auth_header = request.headers.get(self.AUTH_HEADER)
|
||||||
|
if not auth_header:
|
||||||
|
logger.warning(
|
||||||
|
"Roomkit authentication failed: missing Authorization header (ip: %s)",
|
||||||
|
request.META.get("REMOTE_ADDR"),
|
||||||
|
)
|
||||||
|
raise AuthenticationFailed("Authorization header is missing.")
|
||||||
|
|
||||||
|
# Validate token format and existence
|
||||||
|
auth_parts = auth_header.split(" ")
|
||||||
|
if len(auth_parts) != 2 or auth_parts[0] != self.TOKEN_TYPE:
|
||||||
|
raise AuthenticationFailed("Invalid authorization header.")
|
||||||
|
|
||||||
|
token = auth_parts[1]
|
||||||
|
|
||||||
|
# Use constant-time comparison to prevent timing attacks
|
||||||
|
if not secrets.compare_digest(token.encode(), required_token.encode()):
|
||||||
|
logger.warning(
|
||||||
|
"Roomkit authentication failed: invalid token (ip: %s)",
|
||||||
|
request.META.get("REMOTE_ADDR"),
|
||||||
|
)
|
||||||
|
raise AuthenticationFailed("Invalid server-to-server token.")
|
||||||
|
|
||||||
|
return MachineUser(username="roomkit"), token
|
||||||
|
|
||||||
|
def authenticate_header(self, request):
|
||||||
|
"""Return the WWW-Authenticate header value."""
|
||||||
|
return f"{self.TOKEN_TYPE} realm='Roomkit server to server'"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""Serializers for the roomkit API of the Meet core app."""
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from core.api.serializers import BaseValidationOnlySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class RoomKitJoinSerializer(BaseValidationOnlySerializer):
|
||||||
|
"""Validate roomkit join requests from the LiveKit SIP module."""
|
||||||
|
|
||||||
|
pin_code = serializers.CharField(required=True)
|
||||||
|
|
||||||
|
def validate_pin_code(self, value):
|
||||||
|
"""Ensure the PIN code matches the configured length."""
|
||||||
|
if len(value) != settings.ROOM_TELEPHONY_PIN_LENGTH:
|
||||||
|
raise serializers.ValidationError("PIN code length is invalid.")
|
||||||
|
return value
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""Roomkit API endpoints for meeting-room (SIP) device integration."""
|
||||||
|
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from rest_framework import decorators, viewsets
|
||||||
|
from rest_framework import (
|
||||||
|
exceptions as drf_exceptions,
|
||||||
|
)
|
||||||
|
from rest_framework import (
|
||||||
|
response as drf_response,
|
||||||
|
)
|
||||||
|
from rest_framework import (
|
||||||
|
status as drf_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
from core import analytics, models
|
||||||
|
from core.api import permissions, throttling
|
||||||
|
from core.api.feature_flag import FeatureFlag
|
||||||
|
from core.services.sip_management import SIPException, SIPManagement
|
||||||
|
|
||||||
|
from . import authentication, serializers
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomKitViewSet(viewsets.ViewSet):
|
||||||
|
"""Server-to-server API endpoints for the roomkit integration.
|
||||||
|
|
||||||
|
Groups all interactions between roomkit (SIP) devices and the backend,
|
||||||
|
brokered by the LiveKit SIP module. All endpoints are authenticated
|
||||||
|
with the roomkit server-to-server tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
authentication_classes = [authentication.ServerToServerAuthentication]
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
@decorators.action(
|
||||||
|
detail=False,
|
||||||
|
methods=["post"],
|
||||||
|
url_path="join",
|
||||||
|
throttle_classes=[throttling.RoomKitJoinRateThrottle],
|
||||||
|
)
|
||||||
|
@FeatureFlag.require("roomkit")
|
||||||
|
def join(self, request):
|
||||||
|
"""Prepare a room for a meeting-room (SIP) device joining by PIN code.
|
||||||
|
|
||||||
|
Called by the LiveKit SIP module when a meeting-room device dials in
|
||||||
|
with a PIN code before any WebRTC participant has joined. Resolves the
|
||||||
|
room by PIN and creates its SIP dispatch rule, so the device can enter
|
||||||
|
without waiting for a WebRTC user.
|
||||||
|
|
||||||
|
The webhook-based creation path is kept: both converge on the same rule
|
||||||
|
through the shared SIPManagement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer = serializers.RoomKitJoinSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
room = models.Room.objects.get(
|
||||||
|
pin_code=serializer.validated_data["pin_code"]
|
||||||
|
)
|
||||||
|
except models.Room.DoesNotExist as e:
|
||||||
|
raise drf_exceptions.NotFound("No room found for this PIN code.") from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
created = SIPManagement().ensure_dispatch_rule(room)
|
||||||
|
except SIPException as e:
|
||||||
|
raise drf_exceptions.APIException("Could not create dispatch rule.") from e
|
||||||
|
|
||||||
|
analytics.capture(
|
||||||
|
request.user,
|
||||||
|
analytics.AnalyticsEvent.ROOMKIT_JOINED,
|
||||||
|
{
|
||||||
|
"room_id": str(room.pk),
|
||||||
|
"dispatch_rule_created": created,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Roomkit join requested: room_id=%s, dispatch_rule_created=%s",
|
||||||
|
room.id,
|
||||||
|
created,
|
||||||
|
)
|
||||||
|
|
||||||
|
return drf_response.Response(
|
||||||
|
{"status": "success"},
|
||||||
|
status=drf_status.HTTP_200_OK,
|
||||||
|
)
|
||||||
@@ -28,7 +28,7 @@ from .room_management import (
|
|||||||
RoomManagementException,
|
RoomManagementException,
|
||||||
RoomNotFoundException,
|
RoomNotFoundException,
|
||||||
)
|
)
|
||||||
from .telephony import TelephonyException, TelephonyService
|
from .sip_management import SIPException, SIPManagement
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ class LiveKitEventsService:
|
|||||||
)
|
)
|
||||||
self.webhook_receiver = api.WebhookReceiver(token_verifier)
|
self.webhook_receiver = api.WebhookReceiver(token_verifier)
|
||||||
self.lobby_service = LobbyService()
|
self.lobby_service = LobbyService()
|
||||||
self.telephony_service = TelephonyService()
|
self.sip_management = SIPManagement()
|
||||||
self.recording_events = RecordingEventsService()
|
self.recording_events = RecordingEventsService()
|
||||||
|
|
||||||
self._filter_regex = None
|
self._filter_regex = None
|
||||||
@@ -245,12 +245,12 @@ class LiveKitEventsService:
|
|||||||
except models.Room.DoesNotExist as err:
|
except models.Room.DoesNotExist as err:
|
||||||
raise ActionFailedError(f"Room with ID {room_id} does not exist") from err
|
raise ActionFailedError(f"Room with ID {room_id} does not exist") from err
|
||||||
|
|
||||||
if settings.ROOM_TELEPHONY_ENABLED:
|
if settings.ROOM_TELEPHONY_ENABLED or settings.ROOMKIT_ENABLED:
|
||||||
try:
|
try:
|
||||||
self.telephony_service.create_dispatch_rule(room)
|
self.sip_management.ensure_dispatch_rule(room)
|
||||||
except TelephonyException as e:
|
except SIPException as e:
|
||||||
raise ActionFailedError(
|
raise ActionFailedError(
|
||||||
f"Failed to create telephony dispatch rule for room {room_id}"
|
f"Failed to create sip dispatch rule for room {room_id}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def _handle_room_finished(self, data):
|
def _handle_room_finished(self, data):
|
||||||
@@ -265,12 +265,12 @@ class LiveKitEventsService:
|
|||||||
)
|
)
|
||||||
raise ActionFailedError("Failed to process room finished event") from e
|
raise ActionFailedError("Failed to process room finished event") from e
|
||||||
|
|
||||||
if settings.ROOM_TELEPHONY_ENABLED:
|
if settings.ROOM_TELEPHONY_ENABLED or settings.ROOMKIT_ENABLED:
|
||||||
try:
|
try:
|
||||||
self.telephony_service.delete_dispatch_rule(room_id)
|
self.sip_management.delete_dispatch_rule(room_id)
|
||||||
except TelephonyException as e:
|
except SIPException as e:
|
||||||
raise ActionFailedError(
|
raise ActionFailedError(
|
||||||
f"Failed to delete telephony dispatch rule for room {room_id}"
|
f"Failed to delete sip dispatch rule for room {room_id}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
+38
-10
@@ -1,9 +1,9 @@
|
|||||||
"""Telephony service for managing SIP dispatch rules for room access."""
|
"""SIP management service for managing SIP dispatch rules for room access."""
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from livekit.api import TwirpError
|
from livekit.api import TwirpError, TwirpErrorCode
|
||||||
from livekit.protocol.sip import (
|
from livekit.protocol.sip import (
|
||||||
CreateSIPDispatchRuleRequest,
|
CreateSIPDispatchRuleRequest,
|
||||||
DeleteSIPDispatchRuleRequest,
|
DeleteSIPDispatchRuleRequest,
|
||||||
@@ -17,12 +17,16 @@ from core import utils
|
|||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TelephonyException(Exception):
|
class SIPException(Exception):
|
||||||
"""Exception raised when telephony operations fail."""
|
"""Exception raised when SIP operations fail."""
|
||||||
|
|
||||||
|
|
||||||
class TelephonyService:
|
class DispatchRuleConflictError(SIPException):
|
||||||
"""Service for managing participant access through the telephony system (SIP)."""
|
"""Raised when a dispatch rule already exists for the same routing criteria."""
|
||||||
|
|
||||||
|
|
||||||
|
class SIPManagement:
|
||||||
|
"""Service for managing SIP access through the telephony or roomkit system (SIP)."""
|
||||||
|
|
||||||
def _rule_name(self, room_id):
|
def _rule_name(self, room_id):
|
||||||
"""Generate the rule name for a room based on its ID."""
|
"""Generate the rule name for a room based on its ID."""
|
||||||
@@ -32,7 +36,7 @@ class TelephonyService:
|
|||||||
async def create_dispatch_rule(self, room):
|
async def create_dispatch_rule(self, room):
|
||||||
"""Create a SIP inbound dispatch rule for direct room routing.
|
"""Create a SIP inbound dispatch rule for direct room routing.
|
||||||
|
|
||||||
Configures telephony to route incoming SIP calls directly to the specified room
|
Configures livekit-sip to route incoming SIP calls directly to the specified room
|
||||||
using the room's ID and PIN code for authentication.
|
using the room's ID and PIN code for authentication.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -51,10 +55,12 @@ class TelephonyService:
|
|||||||
try:
|
try:
|
||||||
await lkapi.sip.create_sip_dispatch_rule(create=request)
|
await lkapi.sip.create_sip_dispatch_rule(create=request)
|
||||||
except TwirpError as e:
|
except TwirpError as e:
|
||||||
|
if e.code == TwirpErrorCode.ALREADY_EXISTS:
|
||||||
|
raise DispatchRuleConflictError("Dispatch rule already exists") from e
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Unexpected error creating dispatch rule for room %s", room.id
|
"Unexpected error creating dispatch rule for room %s", room.id
|
||||||
)
|
)
|
||||||
raise TelephonyException("Could not create dispatch rule") from e
|
raise SIPException("Could not create dispatch rule") from e
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await lkapi.aclose()
|
await lkapi.aclose()
|
||||||
@@ -79,7 +85,7 @@ class TelephonyService:
|
|||||||
)
|
)
|
||||||
except TwirpError as e:
|
except TwirpError as e:
|
||||||
logger.exception("Failed to list dispatch rules for room %s", room_id)
|
logger.exception("Failed to list dispatch rules for room %s", room_id)
|
||||||
raise TelephonyException("Could not list dispatch rules") from e
|
raise SIPException("Could not list dispatch rules") from e
|
||||||
finally:
|
finally:
|
||||||
await lkapi.aclose()
|
await lkapi.aclose()
|
||||||
|
|
||||||
@@ -94,6 +100,28 @@ class TelephonyService:
|
|||||||
if existing_rule.name == rule_name
|
if existing_rule.name == rule_name
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def has_dispatch_rule(self, room_id):
|
||||||
|
"""Check whether at least one dispatch rule exists for a specific room."""
|
||||||
|
return bool(await self._list_dispatch_rules_ids(room_id))
|
||||||
|
|
||||||
|
def ensure_dispatch_rule(self, room):
|
||||||
|
"""Create the SIP dispatch rule for a room if it does not already exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if a rule was created, False if it already existed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.has_dispatch_rule(room.pk):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.create_dispatch_rule(room)
|
||||||
|
except DispatchRuleConflictError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
@async_to_sync
|
@async_to_sync
|
||||||
async def delete_dispatch_rule(self, room_id):
|
async def delete_dispatch_rule(self, room_id):
|
||||||
"""Delete all SIP inbound dispatch rules associated with a specific room."""
|
"""Delete all SIP inbound dispatch rules associated with a specific room."""
|
||||||
@@ -118,7 +146,7 @@ class TelephonyService:
|
|||||||
|
|
||||||
except TwirpError as e:
|
except TwirpError as e:
|
||||||
logger.exception("Failed to delete dispatch rules for room %s", room_id)
|
logger.exception("Failed to delete dispatch rules for room %s", room_id)
|
||||||
raise TelephonyException("Could not delete dispatch rules") from e
|
raise SIPException("Could not delete dispatch rules") from e
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await lkapi.aclose()
|
await lkapi.aclose()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the roomkit API of the Meet core app."""
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
"""
|
||||||
|
Test the roomkit join server-to-server API endpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=redefined-outer-name,unused-argument
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ...factories import RoomFactory
|
||||||
|
from ...services.sip_management import SIPException
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_sip_management():
|
||||||
|
"""Mock the SIPManagement used by the roomkit viewset."""
|
||||||
|
with mock.patch("core.roomkit.viewsets.SIPManagement") as mock_service_class:
|
||||||
|
yield mock_service_class.return_value
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_anonymous(settings, mock_sip_management, client):
|
||||||
|
"""Requests without an Authorization header should be rejected."""
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
|
||||||
|
response = client.post("/api/v1.0/roomkit/join/", {"pin_code": room.pin_code})
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {"detail": "Authorization header is missing."}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_malformed_authorization_header(settings, mock_sip_management, client):
|
||||||
|
"""Requests with a malformed Authorization header should be rejected."""
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {"detail": "Invalid authorization header."}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_wrong_bearer(settings, mock_sip_management, client):
|
||||||
|
"""Requests with an incorrect bearer token should be rejected."""
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer wrongAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json() == {"detail": "Invalid server-to-server token."}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_token_not_configured(settings, mock_sip_management, client):
|
||||||
|
"""Requests should be rejected when no server-to-server token is configured."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = None
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_roomkit_disabled(settings, mock_sip_management, client):
|
||||||
|
"""The endpoint should not be exposed when the roomkit integration is disabled."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = False
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_missing_pin(settings, mock_sip_management, client):
|
||||||
|
"""Requests without a PIN code should be rejected."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {"pin_code": ["This field is required."]}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_blank_pin(settings, mock_sip_management, client):
|
||||||
|
"""Requests with a blank PIN code should be rejected."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": ""},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {"pin_code": ["This field may not be blank."]}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_wrong_pin_length(settings, mock_sip_management, client):
|
||||||
|
"""Requests with a PIN code of unexpected length should be rejected."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
settings.ROOM_TELEPHONY_PIN_LENGTH = 10
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": "123"},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {"pin_code": ["PIN code length is invalid."]}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_unknown_pin(settings, mock_sip_management, client):
|
||||||
|
"""Requests with a PIN matching no room should return 404 and create no rule."""
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
RoomFactory(pin_code="1234567890")
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": "0987654321"},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert response.json() == {"detail": "No room found for this PIN code."}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_success(settings, mock_sip_management, client):
|
||||||
|
"""Requests with a valid PIN should create the dispatch rule."""
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
mock_sip_management.ensure_dispatch_rule.return_value = True
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"status": "success"}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_called_once_with(room)
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_dispatch_rule_already_exists(settings, mock_sip_management, client):
|
||||||
|
"""Requests should succeed when the dispatch rule already exists (idempotency)."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
mock_sip_management.ensure_dispatch_rule.return_value = False
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"status": "success"}
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_called_once_with(room)
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_tracks_analytics_event(settings, mock_sip_management, client):
|
||||||
|
"""Successful joins should be tracked with an analytics event."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
mock_sip_management.ensure_dispatch_rule.return_value = True
|
||||||
|
|
||||||
|
with mock.patch("core.roomkit.viewsets.analytics.capture") as mock_capture:
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
mock_capture.assert_called_once()
|
||||||
|
_user, event, properties = mock_capture.call_args[0]
|
||||||
|
assert str(event) == "roomkit_joined"
|
||||||
|
assert properties == {
|
||||||
|
"room_id": str(room.pk),
|
||||||
|
"dispatch_rule_created": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_join_sip_failure(settings, mock_sip_management, client):
|
||||||
|
"""Requests should fail with a server error when the sip management service fails."""
|
||||||
|
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
settings.ROOMKIT_SERVER_TO_SERVER_API_TOKEN = "testAuthToken"
|
||||||
|
|
||||||
|
room = RoomFactory(pin_code="1234567890")
|
||||||
|
mock_sip_management.ensure_dispatch_rule.side_effect = SIPException(
|
||||||
|
"Could not create dispatch rule"
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch("core.roomkit.viewsets.analytics.capture") as mock_capture:
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1.0/roomkit/join/",
|
||||||
|
{"pin_code": room.pin_code},
|
||||||
|
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||||
|
raise_request_exception=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
mock_sip_management.ensure_dispatch_rule.assert_called_once_with(room)
|
||||||
|
mock_capture.assert_not_called()
|
||||||
@@ -21,7 +21,10 @@ from core.services.livekit_events import (
|
|||||||
)
|
)
|
||||||
from core.services.lobby import LobbyService
|
from core.services.lobby import LobbyService
|
||||||
from core.services.room_management import RoomManagementException
|
from core.services.room_management import RoomManagementException
|
||||||
from core.services.telephony import TelephonyException, TelephonyService
|
from core.services.sip_management import (
|
||||||
|
SIPException,
|
||||||
|
SIPManagement,
|
||||||
|
)
|
||||||
from core.utils import NotificationError
|
from core.utils import NotificationError
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
@@ -59,7 +62,7 @@ def test_initialization(
|
|||||||
mock_token_verifier.assert_called_once_with(api_key, api_secret)
|
mock_token_verifier.assert_called_once_with(api_key, api_secret)
|
||||||
mock_webhook_receiver.assert_called_once_with(mock_token_verifier.return_value)
|
mock_webhook_receiver.assert_called_once_with(mock_token_verifier.return_value)
|
||||||
assert isinstance(service.lobby_service, LobbyService)
|
assert isinstance(service.lobby_service, LobbyService)
|
||||||
assert isinstance(service.telephony_service, TelephonyService)
|
assert isinstance(service.sip_management, SIPManagement)
|
||||||
assert isinstance(service.recording_events, RecordingEventsService)
|
assert isinstance(service.recording_events, RecordingEventsService)
|
||||||
|
|
||||||
|
|
||||||
@@ -469,11 +472,11 @@ def test_handle_egress_ended_ignores_non_savable_recording(
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch.object(LobbyService, "clear_room_cache")
|
@mock.patch.object(LobbyService, "clear_room_cache")
|
||||||
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
|
@mock.patch.object(SIPManagement, "delete_dispatch_rule")
|
||||||
def test_handle_room_finished_clears_cache_and_deletes_dispatch_rule(
|
def test_handle_room_finished_clears_cache_and_deletes_dispatch_rule(
|
||||||
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
||||||
):
|
):
|
||||||
"""Should clear lobby cache and delete telephony dispatch rule when room finishes."""
|
"""Should clear lobby cache and delete SIP dispatch rule when room finishes."""
|
||||||
settings.ROOM_TELEPHONY_ENABLED = True
|
settings.ROOM_TELEPHONY_ENABLED = True
|
||||||
mock_room_name = uuid.uuid4()
|
mock_room_name = uuid.uuid4()
|
||||||
mock_data = mock.MagicMock()
|
mock_data = mock.MagicMock()
|
||||||
@@ -486,12 +489,31 @@ def test_handle_room_finished_clears_cache_and_deletes_dispatch_rule(
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch.object(LobbyService, "clear_room_cache")
|
@mock.patch.object(LobbyService, "clear_room_cache")
|
||||||
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
|
@mock.patch.object(SIPManagement, "delete_dispatch_rule")
|
||||||
|
def test_handle_room_finished_deletes_dispatch_rule_when_only_roomkit_enabled(
|
||||||
|
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
||||||
|
):
|
||||||
|
"""Should delete dispatch rule when only roomkit is enabled when room finishes."""
|
||||||
|
settings.ROOM_TELEPHONY_ENABLED = False
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
mock_room_name = uuid.uuid4()
|
||||||
|
mock_data = mock.MagicMock()
|
||||||
|
mock_data.room.name = str(mock_room_name)
|
||||||
|
|
||||||
|
service._handle_room_finished(mock_data)
|
||||||
|
|
||||||
|
mock_delete_dispatch_rule.assert_called_once_with(mock_room_name)
|
||||||
|
mock_clear_cache.assert_called_once_with(mock_room_name)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(LobbyService, "clear_room_cache")
|
||||||
|
@mock.patch.object(SIPManagement, "delete_dispatch_rule")
|
||||||
def test_handle_room_finished_skips_telephony_when_disabled(
|
def test_handle_room_finished_skips_telephony_when_disabled(
|
||||||
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
||||||
):
|
):
|
||||||
"""Should clear lobby cache but skip dispatch rule deletion when telephony is disabled."""
|
"""Should clear lobby cache but skip dispatch rule deletion when telephony is disabled."""
|
||||||
settings.ROOM_TELEPHONY_ENABLED = False
|
settings.ROOM_TELEPHONY_ENABLED = False
|
||||||
|
settings.ROOMKIT_ENABLED = False
|
||||||
mock_room_name = uuid.uuid4()
|
mock_room_name = uuid.uuid4()
|
||||||
mock_data = mock.MagicMock()
|
mock_data = mock.MagicMock()
|
||||||
mock_data.room.name = str(mock_room_name)
|
mock_data.room.name = str(mock_room_name)
|
||||||
@@ -505,7 +527,7 @@ def test_handle_room_finished_skips_telephony_when_disabled(
|
|||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
LobbyService, "clear_room_cache", side_effect=Exception("Test error")
|
LobbyService, "clear_room_cache", side_effect=Exception("Test error")
|
||||||
)
|
)
|
||||||
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
|
@mock.patch.object(SIPManagement, "delete_dispatch_rule")
|
||||||
def test_handle_room_finished_raises_error_when_cache_clearing_fails(
|
def test_handle_room_finished_raises_error_when_cache_clearing_fails(
|
||||||
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
||||||
):
|
):
|
||||||
@@ -528,9 +550,9 @@ def test_handle_room_finished_raises_error_when_cache_clearing_fails(
|
|||||||
|
|
||||||
@mock.patch.object(LobbyService, "clear_room_cache")
|
@mock.patch.object(LobbyService, "clear_room_cache")
|
||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
TelephonyService,
|
SIPManagement,
|
||||||
"delete_dispatch_rule",
|
"delete_dispatch_rule",
|
||||||
side_effect=TelephonyException("Test error"),
|
side_effect=SIPException("Test error"),
|
||||||
)
|
)
|
||||||
def test_handle_room_finished_raises_error_when_telephony_deletion_fails(
|
def test_handle_room_finished_raises_error_when_telephony_deletion_fails(
|
||||||
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
mock_delete_dispatch_rule, mock_clear_cache, service, settings
|
||||||
@@ -541,7 +563,7 @@ def test_handle_room_finished_raises_error_when_telephony_deletion_fails(
|
|||||||
mock_data.room.name = "00000000-0000-0000-0000-000000000000"
|
mock_data.room.name = "00000000-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
expected_error = (
|
expected_error = (
|
||||||
"Failed to delete telephony dispatch rule for room "
|
"Failed to delete sip dispatch rule for room "
|
||||||
"00000000-0000-0000-0000-000000000000"
|
"00000000-0000-0000-0000-000000000000"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -562,11 +584,11 @@ def test_handle_room_finished_raises_error_for_invalid_room_name(service):
|
|||||||
service._handle_room_finished(mock_data)
|
service._handle_room_finished(mock_data)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(TelephonyService, "create_dispatch_rule")
|
@mock.patch.object(SIPManagement, "ensure_dispatch_rule")
|
||||||
def test_handle_room_started_creates_dispatch_rule_successfully(
|
def test_handle_room_started_creates_dispatch_rule_successfully(
|
||||||
mock_create_dispatch_rule, service, settings
|
mock_ensure_dispatch_rule, service, settings
|
||||||
):
|
):
|
||||||
"""Should create telephony dispatch rule when room starts successfully."""
|
"""Should ensure the SIP dispatch rule exists when room starts successfully."""
|
||||||
settings.ROOM_TELEPHONY_ENABLED = True
|
settings.ROOM_TELEPHONY_ENABLED = True
|
||||||
room = RoomFactory()
|
room = RoomFactory()
|
||||||
mock_data = mock.MagicMock()
|
mock_data = mock.MagicMock()
|
||||||
@@ -574,22 +596,75 @@ def test_handle_room_started_creates_dispatch_rule_successfully(
|
|||||||
|
|
||||||
service._handle_room_started(mock_data)
|
service._handle_room_started(mock_data)
|
||||||
|
|
||||||
mock_create_dispatch_rule.assert_called_once_with(room)
|
mock_ensure_dispatch_rule.assert_called_once_with(room)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(TelephonyService, "create_dispatch_rule")
|
@mock.patch.object(SIPManagement, "ensure_dispatch_rule")
|
||||||
def test_handle_room_started_skips_dispatch_rule_when_telephony_disabled(
|
def test_handle_room_started_creates_dispatch_rule_when_only_roomkit_enabled(
|
||||||
mock_create_dispatch_rule, service, settings
|
mock_ensure_dispatch_rule, service, settings
|
||||||
):
|
):
|
||||||
"""Should skip creating telephony dispatch rule when telephony is disabled during room start."""
|
"""Should ensure the dispatch rule exists when only roomkit is enabled during room start."""
|
||||||
settings.ROOM_TELEPHONY_ENABLED = False
|
settings.ROOM_TELEPHONY_ENABLED = False
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
room = RoomFactory()
|
room = RoomFactory()
|
||||||
mock_data = mock.MagicMock()
|
mock_data = mock.MagicMock()
|
||||||
mock_data.room.name = str(room.id)
|
mock_data.room.name = str(room.id)
|
||||||
|
|
||||||
service._handle_room_started(mock_data)
|
service._handle_room_started(mock_data)
|
||||||
|
|
||||||
mock_create_dispatch_rule.assert_not_called()
|
mock_ensure_dispatch_rule.assert_called_once_with(room)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(SIPManagement, "ensure_dispatch_rule", return_value=False)
|
||||||
|
def test_handle_room_started_ignores_existing_dispatch_rule(
|
||||||
|
mock_ensure_dispatch_rule, service, settings
|
||||||
|
):
|
||||||
|
"""Should proceed silently when the dispatch rule already exists when room starts."""
|
||||||
|
settings.ROOM_TELEPHONY_ENABLED = True
|
||||||
|
room = RoomFactory()
|
||||||
|
mock_data = mock.MagicMock()
|
||||||
|
mock_data.room.name = str(room.id)
|
||||||
|
|
||||||
|
# ensure_dispatch_rule reports the rule as pre-existing: nothing to raise
|
||||||
|
service._handle_room_started(mock_data)
|
||||||
|
|
||||||
|
mock_ensure_dispatch_rule.assert_called_once_with(room)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
SIPManagement,
|
||||||
|
"ensure_dispatch_rule",
|
||||||
|
side_effect=SIPException("Test error"),
|
||||||
|
)
|
||||||
|
def test_handle_room_started_raises_error_when_dispatch_rule_creation_fails(
|
||||||
|
mock_ensure_dispatch_rule, service, settings
|
||||||
|
):
|
||||||
|
"""Should raise ActionFailedError when ensuring the dispatch rule fails when room starts."""
|
||||||
|
settings.ROOM_TELEPHONY_ENABLED = True
|
||||||
|
room = RoomFactory()
|
||||||
|
mock_data = mock.MagicMock()
|
||||||
|
mock_data.room.name = str(room.id)
|
||||||
|
|
||||||
|
expected_error = f"Failed to create sip dispatch rule for room {room.id}"
|
||||||
|
|
||||||
|
with pytest.raises(ActionFailedError, match=expected_error):
|
||||||
|
service._handle_room_started(mock_data)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(SIPManagement, "ensure_dispatch_rule")
|
||||||
|
def test_handle_room_started_skips_dispatch_rule_when_telephony_disabled(
|
||||||
|
mock_ensure_dispatch_rule, service, settings
|
||||||
|
):
|
||||||
|
"""Should skip ensuring the SIP dispatch rule when telephony is disabled during room start."""
|
||||||
|
settings.ROOM_TELEPHONY_ENABLED = False
|
||||||
|
settings.ROOMKIT_ENABLED = False
|
||||||
|
room = RoomFactory()
|
||||||
|
mock_data = mock.MagicMock()
|
||||||
|
mock_data.room.name = str(room.id)
|
||||||
|
|
||||||
|
service._handle_room_started(mock_data)
|
||||||
|
|
||||||
|
mock_ensure_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_handle_room_started_raises_error_for_invalid_room_name(service):
|
def test_handle_room_started_raises_error_for_invalid_room_name(service):
|
||||||
|
|||||||
+162
-35
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Test telephony service.
|
Test SIP mamagement service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=W0212
|
# pylint: disable=W0212
|
||||||
@@ -20,7 +20,11 @@ from livekit.protocol.sip import (
|
|||||||
|
|
||||||
from core.factories import RoomFactory
|
from core.factories import RoomFactory
|
||||||
from core.models import RoomAccessLevel
|
from core.models import RoomAccessLevel
|
||||||
from core.services.telephony import TelephonyException, TelephonyService
|
from core.services.sip_management import (
|
||||||
|
DispatchRuleConflictError,
|
||||||
|
SIPException,
|
||||||
|
SIPManagement,
|
||||||
|
)
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -35,9 +39,9 @@ def create_mock_livekit_client():
|
|||||||
|
|
||||||
def test_rule_name():
|
def test_rule_name():
|
||||||
"""Test rule name generation."""
|
"""Test rule name generation."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
rule_name = telephony_service._rule_name(room.id)
|
rule_name = sip_management._rule_name(room.id)
|
||||||
|
|
||||||
assert rule_name == f"SIP_{str(room.id)}"
|
assert rule_name == f"SIP_{str(room.id)}"
|
||||||
|
|
||||||
@@ -45,14 +49,14 @@ def test_rule_name():
|
|||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_create_dispatch_rule_success(mock_client_factory):
|
def test_create_dispatch_rule_success(mock_client_factory):
|
||||||
"""Test successful dispatch rule creation."""
|
"""Test successful dispatch rule creation."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_api = create_mock_livekit_client()
|
mock_api = create_mock_livekit_client()
|
||||||
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock()
|
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock()
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
telephony_service.create_dispatch_rule(room)
|
sip_management.create_dispatch_rule(room)
|
||||||
|
|
||||||
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
|
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
|
||||||
create_request = mock_api.sip.create_sip_dispatch_rule.call_args[1]["create"]
|
create_request = mock_api.sip.create_sip_dispatch_rule.call_args[1]["create"]
|
||||||
@@ -67,7 +71,7 @@ def test_create_dispatch_rule_success(mock_client_factory):
|
|||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_create_dispatch_rule_api_failure(mock_client_factory):
|
def test_create_dispatch_rule_api_failure(mock_client_factory):
|
||||||
"""Test dispatch rule creation when API fails."""
|
"""Test dispatch rule creation when API fails."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_api = create_mock_livekit_client()
|
mock_api = create_mock_livekit_client()
|
||||||
@@ -76,8 +80,8 @@ def test_create_dispatch_rule_api_failure(mock_client_factory):
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
with pytest.raises(TelephonyException, match="Could not create dispatch rule"):
|
with pytest.raises(SIPException, match="Could not create dispatch rule"):
|
||||||
telephony_service.create_dispatch_rule(room)
|
sip_management.create_dispatch_rule(room)
|
||||||
|
|
||||||
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
|
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
|
||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
@@ -86,7 +90,7 @@ def test_create_dispatch_rule_api_failure(mock_client_factory):
|
|||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_list_dispatch_rules_ids_success(mock_client_factory):
|
def test_list_dispatch_rules_ids_success(mock_client_factory):
|
||||||
"""Test successful listing of dispatch rule IDs."""
|
"""Test successful listing of dispatch rule IDs."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_rules = [
|
mock_rules = [
|
||||||
@@ -111,7 +115,7 @@ def test_list_dispatch_rules_ids_success(mock_client_factory):
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
result = async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
|
result = async_to_sync(sip_management._list_dispatch_rules_ids)(room.id)
|
||||||
|
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
assert "rule-1" in result
|
assert "rule-1" in result
|
||||||
@@ -127,7 +131,7 @@ def test_list_dispatch_rules_ids_success(mock_client_factory):
|
|||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_list_dispatch_rules_ids_empty_response(mock_client_factory):
|
def test_list_dispatch_rules_ids_empty_response(mock_client_factory):
|
||||||
"""Test listing dispatch rule IDs when no rules exist."""
|
"""Test listing dispatch rule IDs when no rules exist."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_api = create_mock_livekit_client()
|
mock_api = create_mock_livekit_client()
|
||||||
@@ -136,7 +140,7 @@ def test_list_dispatch_rules_ids_empty_response(mock_client_factory):
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
result = async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
|
result = async_to_sync(sip_management._list_dispatch_rules_ids)(room.id)
|
||||||
|
|
||||||
assert result == []
|
assert result == []
|
||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
@@ -145,7 +149,7 @@ def test_list_dispatch_rules_ids_empty_response(mock_client_factory):
|
|||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_list_dispatch_rules_ids_no_matching_rules(mock_client_factory):
|
def test_list_dispatch_rules_ids_no_matching_rules(mock_client_factory):
|
||||||
"""Test listing dispatch rule IDs when no rules match the room."""
|
"""Test listing dispatch rule IDs when no rules match the room."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_rules = [
|
mock_rules = [
|
||||||
@@ -163,7 +167,7 @@ def test_list_dispatch_rules_ids_no_matching_rules(mock_client_factory):
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
result = async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
|
result = async_to_sync(sip_management._list_dispatch_rules_ids)(room.id)
|
||||||
|
|
||||||
assert result == []
|
assert result == []
|
||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
@@ -172,7 +176,7 @@ def test_list_dispatch_rules_ids_no_matching_rules(mock_client_factory):
|
|||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_list_dispatch_rules_ids_api_failure(mock_client_factory):
|
def test_list_dispatch_rules_ids_api_failure(mock_client_factory):
|
||||||
"""Test listing dispatch rule IDs when API fails."""
|
"""Test listing dispatch rule IDs when API fails."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_api = create_mock_livekit_client()
|
mock_api = create_mock_livekit_client()
|
||||||
@@ -181,34 +185,34 @@ def test_list_dispatch_rules_ids_api_failure(mock_client_factory):
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
with pytest.raises(TelephonyException, match="Could not list dispatch rules"):
|
with pytest.raises(SIPException, match="Could not list dispatch rules"):
|
||||||
async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
|
async_to_sync(sip_management._list_dispatch_rules_ids)(room.id)
|
||||||
|
|
||||||
mock_api.sip.list_sip_dispatch_rule.assert_called_once()
|
mock_api.sip.list_sip_dispatch_rule.assert_called_once()
|
||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
|
@mock.patch("core.services.sip_management.SIPManagement._list_dispatch_rules_ids")
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_delete_dispatch_rule_no_rules(mock_client_factory, mock_list_rules):
|
def test_delete_dispatch_rule_no_rules(mock_client_factory, mock_list_rules):
|
||||||
"""Test deleting dispatch rules when no rules exist."""
|
"""Test deleting dispatch rules when no rules exist."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_list_rules.return_value = []
|
mock_list_rules.return_value = []
|
||||||
|
|
||||||
result = telephony_service.delete_dispatch_rule(room.id)
|
result = sip_management.delete_dispatch_rule(room.id)
|
||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
mock_list_rules.assert_called_once_with(room.id)
|
mock_list_rules.assert_called_once_with(room.id)
|
||||||
mock_client_factory.assert_not_called()
|
mock_client_factory.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
|
@mock.patch("core.services.sip_management.SIPManagement._list_dispatch_rules_ids")
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_delete_dispatch_rule_single_rule(mock_client_factory, mock_list_rules):
|
def test_delete_dispatch_rule_single_rule(mock_client_factory, mock_list_rules):
|
||||||
"""Test deleting a single dispatch rule."""
|
"""Test deleting a single dispatch rule."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_list_rules.return_value = ["rule-1"]
|
mock_list_rules.return_value = ["rule-1"]
|
||||||
@@ -216,7 +220,7 @@ def test_delete_dispatch_rule_single_rule(mock_client_factory, mock_list_rules):
|
|||||||
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock()
|
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock()
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
result = telephony_service.delete_dispatch_rule(room.id)
|
result = sip_management.delete_dispatch_rule(room.id)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
mock_api.sip.delete_sip_dispatch_rule.assert_called_once()
|
mock_api.sip.delete_sip_dispatch_rule.assert_called_once()
|
||||||
@@ -226,11 +230,11 @@ def test_delete_dispatch_rule_single_rule(mock_client_factory, mock_list_rules):
|
|||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
|
@mock.patch("core.services.sip_management.SIPManagement._list_dispatch_rules_ids")
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_delete_dispatch_rule_multiple_rules(mock_client_factory, mock_list_rules):
|
def test_delete_dispatch_rule_multiple_rules(mock_client_factory, mock_list_rules):
|
||||||
"""Test deleting multiple dispatch rules."""
|
"""Test deleting multiple dispatch rules."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_list_rules.return_value = ["rule-1", "rule-2", "rule-3"]
|
mock_list_rules.return_value = ["rule-1", "rule-2", "rule-3"]
|
||||||
@@ -238,7 +242,7 @@ def test_delete_dispatch_rule_multiple_rules(mock_client_factory, mock_list_rule
|
|||||||
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock()
|
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock()
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
result = telephony_service.delete_dispatch_rule(room.id)
|
result = sip_management.delete_dispatch_rule(room.id)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
assert mock_api.sip.delete_sip_dispatch_rule.call_count == 3
|
assert mock_api.sip.delete_sip_dispatch_rule.call_count == 3
|
||||||
@@ -253,11 +257,11 @@ def test_delete_dispatch_rule_multiple_rules(mock_client_factory, mock_list_rule
|
|||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
|
@mock.patch("core.services.sip_management.SIPManagement._list_dispatch_rules_ids")
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_delete_dispatch_rule_partial_failure(mock_client_factory, mock_list_rules):
|
def test_delete_dispatch_rule_partial_failure(mock_client_factory, mock_list_rules):
|
||||||
"""Test deleting multiple dispatch rules when one deletion fails."""
|
"""Test deleting multiple dispatch rules when one deletion fails."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_list_rules.return_value = ["rule-1", "rule-2", "rule-3"]
|
mock_list_rules.return_value = ["rule-1", "rule-2", "rule-3"]
|
||||||
@@ -277,18 +281,18 @@ def test_delete_dispatch_rule_partial_failure(mock_client_factory, mock_list_rul
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
with pytest.raises(TelephonyException, match="Could not delete dispatch rules"):
|
with pytest.raises(SIPException, match="Could not delete dispatch rules"):
|
||||||
telephony_service.delete_dispatch_rule(room.id)
|
sip_management.delete_dispatch_rule(room.id)
|
||||||
|
|
||||||
assert mock_api.sip.delete_sip_dispatch_rule.call_count == 2
|
assert mock_api.sip.delete_sip_dispatch_rule.call_count == 2
|
||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
|
@mock.patch("core.services.sip_management.SIPManagement._list_dispatch_rules_ids")
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
def test_delete_dispatch_rule_api_failure(mock_client_factory, mock_list_rules):
|
def test_delete_dispatch_rule_api_failure(mock_client_factory, mock_list_rules):
|
||||||
"""Test deleting dispatch rules when API fails immediately."""
|
"""Test deleting dispatch rules when API fails immediately."""
|
||||||
telephony_service = TelephonyService()
|
sip_management = SIPManagement()
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
mock_list_rules.return_value = ["rule-1"]
|
mock_list_rules.return_value = ["rule-1"]
|
||||||
@@ -298,8 +302,131 @@ def test_delete_dispatch_rule_api_failure(mock_client_factory, mock_list_rules):
|
|||||||
)
|
)
|
||||||
mock_client_factory.return_value = mock_api
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
with pytest.raises(TelephonyException, match="Could not delete dispatch rules"):
|
with pytest.raises(SIPException, match="Could not delete dispatch rules"):
|
||||||
telephony_service.delete_dispatch_rule(room.id)
|
sip_management.delete_dispatch_rule(room.id)
|
||||||
|
|
||||||
mock_api.sip.delete_sip_dispatch_rule.assert_called_once()
|
mock_api.sip.delete_sip_dispatch_rule.assert_called_once()
|
||||||
mock_api.aclose.assert_called_once()
|
mock_api.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_create_dispatch_rule_conflict_raises_dedicated_error(mock_client_factory):
|
||||||
|
"""Test that a LiveKit conflict error raises DispatchRuleConflictError."""
|
||||||
|
sip_management = SIPManagement()
|
||||||
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
|
mock_api = create_mock_livekit_client()
|
||||||
|
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
side_effect=TwirpError(
|
||||||
|
msg=(
|
||||||
|
"Dispatch rule for the same trunk, inbound number, number, and "
|
||||||
|
"PIN combination already exists in dispatch rule"
|
||||||
|
),
|
||||||
|
code="already_exists",
|
||||||
|
status=409,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
|
with pytest.raises(DispatchRuleConflictError):
|
||||||
|
sip_management.create_dispatch_rule(room)
|
||||||
|
|
||||||
|
mock_api.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_ensure_dispatch_rule_creates_when_missing(mock_client_factory):
|
||||||
|
"""Test that ensure_dispatch_rule creates the rule when none exists."""
|
||||||
|
sip_management = SIPManagement()
|
||||||
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
|
mock_api = create_mock_livekit_client()
|
||||||
|
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
return_value=ListSIPDispatchRuleResponse(items=[])
|
||||||
|
)
|
||||||
|
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock()
|
||||||
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
|
created = sip_management.ensure_dispatch_rule(room)
|
||||||
|
|
||||||
|
assert created is True
|
||||||
|
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
|
||||||
|
create_request = mock_api.sip.create_sip_dispatch_rule.call_args[1]["create"]
|
||||||
|
|
||||||
|
assert isinstance(create_request, CreateSIPDispatchRuleRequest)
|
||||||
|
assert create_request.name == f"SIP_{str(room.id)}"
|
||||||
|
assert create_request.rule.dispatch_rule_direct.room_name == str(room.id)
|
||||||
|
assert create_request.rule.dispatch_rule_direct.pin == str(room.pin_code)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_ensure_dispatch_rule_skips_when_existing(mock_client_factory):
|
||||||
|
"""Test that ensure_dispatch_rule is idempotent when the rule already exists."""
|
||||||
|
sip_management = SIPManagement()
|
||||||
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
|
existing_rule = SIPDispatchRuleInfo(
|
||||||
|
sip_dispatch_rule_id="rule-1", name=f"SIP_{str(room.id)}"
|
||||||
|
)
|
||||||
|
mock_api = create_mock_livekit_client()
|
||||||
|
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
return_value=ListSIPDispatchRuleResponse(items=[existing_rule])
|
||||||
|
)
|
||||||
|
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock()
|
||||||
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
|
created = sip_management.ensure_dispatch_rule(room)
|
||||||
|
|
||||||
|
assert created is False
|
||||||
|
mock_api.sip.create_sip_dispatch_rule.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_ensure_dispatch_rule_returns_false_on_conflict(mock_client_factory):
|
||||||
|
"""Test that ensure_dispatch_rule tolerates a concurrent rule creation.
|
||||||
|
|
||||||
|
If the rule is created by a concurrent caller (e.g. the LiveKit webhook)
|
||||||
|
between the existence check and the creation, LiveKit rejects the
|
||||||
|
duplicate and ensure_dispatch_rule reports the rule as already existing.
|
||||||
|
"""
|
||||||
|
sip_management = SIPManagement()
|
||||||
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
|
mock_api = create_mock_livekit_client()
|
||||||
|
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
return_value=ListSIPDispatchRuleResponse(items=[])
|
||||||
|
)
|
||||||
|
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
side_effect=TwirpError(
|
||||||
|
msg=(
|
||||||
|
"Dispatch rule for the same trunk, inbound number, number, and "
|
||||||
|
"PIN combination already exists in dispatch rule"
|
||||||
|
),
|
||||||
|
code="already_exists",
|
||||||
|
status=409,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
|
created = sip_management.ensure_dispatch_rule(room)
|
||||||
|
|
||||||
|
assert created is False
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_ensure_dispatch_rule_raises_on_other_failures(mock_client_factory):
|
||||||
|
"""Test that ensure_dispatch_rule propagates unexpected LiveKit failures."""
|
||||||
|
sip_management = SIPManagement()
|
||||||
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
|
||||||
|
|
||||||
|
mock_api = create_mock_livekit_client()
|
||||||
|
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
return_value=ListSIPDispatchRuleResponse(items=[])
|
||||||
|
)
|
||||||
|
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock(
|
||||||
|
side_effect=TwirpError(msg="Internal server error", code="unknown", status=500)
|
||||||
|
)
|
||||||
|
mock_client_factory.return_value = mock_api
|
||||||
|
|
||||||
|
with pytest.raises(SIPException, match="Could not create dispatch rule"):
|
||||||
|
sip_management.ensure_dispatch_rule(room)
|
||||||
@@ -184,12 +184,13 @@ def test_models_rooms_is_public_property():
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch.object(Room, "generate_unique_pin_code")
|
@mock.patch.object(Room, "generate_unique_pin_code")
|
||||||
def test_telephony_disabled_skips_pin_generation(
|
def test_telephony_and_roomkit_disabled_skips_pin_generation(
|
||||||
mock_generate_unique_pin_code, settings
|
mock_generate_unique_pin_code, settings
|
||||||
):
|
):
|
||||||
"""Telephony disabled should not generate pin codes."""
|
"""Telephony and roomkit both disabled should not generate pin codes."""
|
||||||
|
|
||||||
settings.ROOM_TELEPHONY_ENABLED = False
|
settings.ROOM_TELEPHONY_ENABLED = False
|
||||||
|
settings.ROOMKIT_ENABLED = False
|
||||||
|
|
||||||
room = RoomFactory()
|
room = RoomFactory()
|
||||||
|
|
||||||
@@ -197,6 +198,18 @@ def test_telephony_disabled_skips_pin_generation(
|
|||||||
assert room.pin_code is None
|
assert room.pin_code is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_roomkit_enabled_generates_pin_code(settings):
|
||||||
|
"""Roomkit enabled alone should generate pin codes, even without telephony."""
|
||||||
|
|
||||||
|
settings.ROOM_TELEPHONY_ENABLED = False
|
||||||
|
settings.ROOMKIT_ENABLED = True
|
||||||
|
|
||||||
|
room = RoomFactory()
|
||||||
|
|
||||||
|
assert room.pin_code is not None
|
||||||
|
assert len(room.pin_code) == settings.ROOM_TELEPHONY_PIN_LENGTH
|
||||||
|
|
||||||
|
|
||||||
def test_default_and_custom_pin_length(settings):
|
def test_default_and_custom_pin_length(settings):
|
||||||
"""Pin codes should be created with correct configured length."""
|
"""Pin codes should be created with correct configured length."""
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from rest_framework.routers import DefaultRouter, SimpleRouter
|
|||||||
from core.addons import viewsets as addons_viewsets
|
from core.addons import viewsets as addons_viewsets
|
||||||
from core.api import get_frontend_configuration, viewsets
|
from core.api import get_frontend_configuration, viewsets
|
||||||
from core.external_api import viewsets as external_viewsets
|
from core.external_api import viewsets as external_viewsets
|
||||||
|
from core.roomkit import viewsets as roomkit_viewsets
|
||||||
|
|
||||||
# - Main endpoints
|
# - Main endpoints
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
@@ -19,6 +20,11 @@ router.register("files", viewsets.FileViewSet, basename="files")
|
|||||||
router.register(
|
router.register(
|
||||||
"resource-accesses", viewsets.ResourceAccessViewSet, basename="resource_accesses"
|
"resource-accesses", viewsets.ResourceAccessViewSet, basename="resource_accesses"
|
||||||
)
|
)
|
||||||
|
router.register(
|
||||||
|
"roomkit",
|
||||||
|
roomkit_viewsets.RoomKitViewSet,
|
||||||
|
basename="roomkit",
|
||||||
|
)
|
||||||
router.register(
|
router.register(
|
||||||
"addons/sessions",
|
"addons/sessions",
|
||||||
addons_viewsets.SessionViewSet,
|
addons_viewsets.SessionViewSet,
|
||||||
|
|||||||
@@ -349,6 +349,11 @@ class Base(Configuration):
|
|||||||
environ_name="CREATION_CALLBACK_THROTTLE_RATES",
|
environ_name="CREATION_CALLBACK_THROTTLE_RATES",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
),
|
),
|
||||||
|
"roomkit_join": values.Value(
|
||||||
|
default="300/minute",
|
||||||
|
environ_name="ROOMKIT_JOIN_THROTTLE_RATES",
|
||||||
|
environ_prefix=None,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
MONITORED_THROTTLE_FAILURE_CALLBACK = (
|
MONITORED_THROTTLE_FAILURE_CALLBACK = (
|
||||||
@@ -881,6 +886,21 @@ class Base(Configuration):
|
|||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Roomkit (meeting-room SIP devices) integration
|
||||||
|
ROOMKIT_ENABLED = values.BooleanValue(
|
||||||
|
False,
|
||||||
|
environ_name="ROOMKIT_ENABLED",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
# Server-to-server API token allowing the LiveKit SIP module to call the
|
||||||
|
# roomkit endpoints (e.g. join a room on behalf of a meeting-room device
|
||||||
|
# dialing in before any WebRTC participant).
|
||||||
|
ROOMKIT_SERVER_TO_SERVER_API_TOKEN = SecretFileValue(
|
||||||
|
None,
|
||||||
|
environ_name="ROOMKIT_SERVER_TO_SERVER_API_TOKEN",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
# Subtitles settings
|
# Subtitles settings
|
||||||
ROOM_SUBTITLE_ENABLED = values.BooleanValue(
|
ROOM_SUBTITLE_ENABLED = values.BooleanValue(
|
||||||
False, environ_name="ROOM_SUBTITLE_ENABLED", environ_prefix=None
|
False, environ_name="ROOM_SUBTITLE_ENABLED", environ_prefix=None
|
||||||
|
|||||||
+14
-14
@@ -24,7 +24,7 @@ keywords = ["Django", "Contacts", "Templates", "RBAC"]
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boto3==1.43.56",
|
"boto3==1.43.36",
|
||||||
"Brotli==1.2.0",
|
"Brotli==1.2.0",
|
||||||
"brevo-python==1.2.0",
|
"brevo-python==1.2.0",
|
||||||
"celery[redis]==5.6.3",
|
"celery[redis]==5.6.3",
|
||||||
@@ -32,7 +32,7 @@ dependencies = [
|
|||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
"django-cors-headers==4.9.0",
|
"django-cors-headers==4.9.0",
|
||||||
"django-countries==9.0.0",
|
"django-countries==9.0.0",
|
||||||
"django-filter==26.1",
|
"django-filter==25.2",
|
||||||
"django-lasuite[all]==0.0.27",
|
"django-lasuite[all]==0.0.27",
|
||||||
"django-parler==2.4",
|
"django-parler==2.4",
|
||||||
"redis==5.2.1",
|
"redis==5.2.1",
|
||||||
@@ -42,7 +42,7 @@ dependencies = [
|
|||||||
"django-pydantic-field==0.5.4",
|
"django-pydantic-field==0.5.4",
|
||||||
"django==5.2.14",
|
"django==5.2.14",
|
||||||
"djangorestframework==3.17.1",
|
"djangorestframework==3.17.1",
|
||||||
"drf_spectacular==0.30.0",
|
"drf_spectacular==0.29.0",
|
||||||
"dockerflow==2026.3.4",
|
"dockerflow==2026.3.4",
|
||||||
"easy_thumbnails==2.10.1",
|
"easy_thumbnails==2.10.1",
|
||||||
"factory_boy==3.3.3",
|
"factory_boy==3.3.3",
|
||||||
@@ -50,20 +50,20 @@ dependencies = [
|
|||||||
"jsonschema==4.26.0",
|
"jsonschema==4.26.0",
|
||||||
"markdown==3.10.2",
|
"markdown==3.10.2",
|
||||||
"nested-multipart-parser==1.6.0",
|
"nested-multipart-parser==1.6.0",
|
||||||
"posthog==7.29.0",
|
"posthog==7.16.1",
|
||||||
"psycopg[binary]==3.3.4",
|
"psycopg[binary]==3.3.4",
|
||||||
"pydantic==2.13.4",
|
"pydantic==2.13.4",
|
||||||
"PyJWT==2.13.0",
|
"PyJWT==2.13.0",
|
||||||
"python-frontmatter==1.3.0",
|
"python-frontmatter==1.3.0",
|
||||||
"python-magic==0.4.27",
|
"python-magic==0.4.27",
|
||||||
"requests==2.34.2",
|
"requests==2.34.2",
|
||||||
"sentry-sdk==2.66.1",
|
"sentry-sdk==2.63.0",
|
||||||
"whitenoise==6.12.0",
|
"whitenoise==6.12.0",
|
||||||
"mozilla-django-oidc==5.0.2",
|
"mozilla-django-oidc==5.0.2",
|
||||||
"livekit-api==1.2.0",
|
"livekit-api==1.1.1",
|
||||||
"aiohttp==3.14.3",
|
"aiohttp==3.14.1",
|
||||||
"urllib3==2.7.0",
|
"urllib3==2.7.0",
|
||||||
"phonenumbers==9.0.34",
|
"phonenumbers==9.0.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -75,21 +75,21 @@ dependencies = [
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"django-extensions==4.1",
|
"django-extensions==4.1",
|
||||||
"drf-spectacular-sidecar==2026.7.1",
|
"drf-spectacular-sidecar==2026.6.1",
|
||||||
"freezegun==1.5.5",
|
"freezegun==1.5.5",
|
||||||
"ipdb==0.13.13",
|
"ipdb==0.13.13",
|
||||||
"ipython==9.15.0",
|
"ipython==9.14.1",
|
||||||
"pyfakefs==6.2.0",
|
"pyfakefs==6.2.0",
|
||||||
"pylint-django==2.8.0",
|
"pylint-django==2.7.0",
|
||||||
"pylint<4.0.0",
|
"pylint<4.0.0",
|
||||||
"pytest-cov==7.1.0",
|
"pytest-cov==7.1.0",
|
||||||
"pytest-django==4.12.0",
|
"pytest-django==4.12.0",
|
||||||
"pytest==9.1.1",
|
"pytest==9.1.1",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.8.0",
|
"pytest-xdist==3.8.0",
|
||||||
"responses==0.26.2",
|
"responses==0.26.1",
|
||||||
"ruff==0.16.0",
|
"ruff==0.15.19",
|
||||||
"types-requests==2.33.0.20260712",
|
"types-requests==2.33.0.20260518",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.build-backend]
|
[tool.uv.build-backend]
|
||||||
|
|||||||
Generated
+141
-142
@@ -13,7 +13,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
version = "3.14.3"
|
version = "3.14.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohappyeyeballs" },
|
{ name = "aiohappyeyeballs" },
|
||||||
@@ -24,72 +24,72 @@ dependencies = [
|
|||||||
{ name = "propcache" },
|
{ name = "propcache" },
|
||||||
{ name = "yarl" },
|
{ name = "yarl" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
|
{ url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
|
{ url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
|
{ url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
|
{ url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
|
{ url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
|
{ url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
|
{ url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
|
{ url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
|
{ url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
|
{ url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -181,30 +181,30 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.43.56"
|
version = "1.43.36"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "s3transfer" },
|
{ name = "s3transfer" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/53/05/23e1aa8c9e4b0399a61e7fd65c4f9cc0625121f24760e37471f776404abb/boto3-1.43.56.tar.gz", hash = "sha256:57c90df9fb026f2e6ae22530861198130203733c5c9ec4e5cca3a4037f5a8db4", size = 112673, upload-time = "2026-07-24T19:31:48.606Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/897287e955db0f50b12fd69ef45956e4fd2c7ddb48c736872f7ea2314443/boto3-1.43.36.tar.gz", hash = "sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28", size = 112690, upload-time = "2026-06-23T02:47:14.561Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/57/3a960c9f581c00f2a591901b46e035ff79ab3956d16607f12306b3b8d483/boto3-1.43.56-py3-none-any.whl", hash = "sha256:feb699d4ab241ef5c1b80bb58277be2aaad365cd4b672d7817e0bc59ee45131b", size = 140026, upload-time = "2026-07-24T19:31:47.155Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/f1/274303f52483ecf199eae6f8d9b6f5951670397ee4d72c06cfd4eb644612/boto3-1.43.36-py3-none-any.whl", hash = "sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f", size = 140031, upload-time = "2026-06-23T02:47:13.178Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.43.62"
|
version = "1.43.40"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/36af6d99269a701f83809b87a01f4728699eb825ebdedee3a3d515b18f61/botocore-1.43.62.tar.gz", hash = "sha256:94efc419c9f0f41dc2415e4b6b62f04ae21b3ce3930fac47214c4d3f361ea8b8", size = 15818261, upload-time = "2026-07-31T19:35:06.235Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/50/269986277f852cc83029bccbdcdc0b343a685cfd570599e58029792808d8/botocore-1.43.40.tar.gz", hash = "sha256:2085a4314cfd2c8bc1d08ab8039f76c92e99278db0d2a0e2437010526d5d5d70", size = 15639899, upload-time = "2026-07-03T00:28:16.125Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/65/d5dae96de68ffc55acf87c3bae76e9dabeeca92aadf1223f20e9a7860aef/botocore-1.43.62-py3-none-any.whl", hash = "sha256:76de153de1ba3e242b2e6df6a13ab8a3fb35d17db562462969e661457b63166e", size = 15502622, upload-time = "2026-07-31T19:35:02.697Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/8c/5f7e73fd66b28f0705bc55d7060d41ef72328b656b86ca53e75765b3ba2c/botocore-1.43.40-py3-none-any.whl", hash = "sha256:0bc9d352267c9e48415c5d7bb61ff05c3f193eac2fc7e69cfd229a05fbab67d6", size = 15323870, upload-time = "2026-07-03T00:28:12.56Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -650,14 +650,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-filter"
|
name = "django-filter"
|
||||||
version = "26.1"
|
version = "25.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/563965173d4cbb5fc308087e7b3d11a115b7b67273d093622480b1e31f78/django_filter-26.1.tar.gz", hash = "sha256:66ea04031b068c77c86e1ac26ced7a3f8f13ce797f5795751707e3deefc58054", size = 144299, upload-time = "2026-07-11T09:27:02.767Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -775,7 +775,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "drf-spectacular"
|
name = "drf-spectacular"
|
||||||
version = "0.30.0"
|
version = "0.29.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
@@ -785,21 +785,21 @@ dependencies = [
|
|||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "uritemplate" },
|
{ name = "uritemplate" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/43/41d25039a6a53545420ebc98eb9f877ec9fe30c7bd03fefabcaf9b953af7/drf_spectacular-0.30.0.tar.gz", hash = "sha256:53e79e7ba00e240441b63c32273754a5368e4c2ab44a19f2595277cc1cd559c9", size = 252311, upload-time = "2026-07-06T11:29:46.264Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/56/74dd7b45bbde6d24494220b98d6961cb1200b63a1800332b430daa2c4551/drf_spectacular-0.30.0-py3-none-any.whl", hash = "sha256:006cf5921ebe20a9bd24f7c846261ebbf78780be5961b0d6e87afaa82afd62ff", size = 111150, upload-time = "2026-07-06T11:29:45.12Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "drf-spectacular-sidecar"
|
name = "drf-spectacular-sidecar"
|
||||||
version = "2026.7.1"
|
version = "2026.6.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/51/9e038d14bf51a0bd051e8bcb690287349c908fa7ba69021d9f3e5d5ac51f/drf_spectacular_sidecar-2026.7.1.tar.gz", hash = "sha256:40113c4066c7bc3ef15a7ce1c40cda227a907a9986748024a813a9e0595eba25", size = 2593211, upload-time = "2026-07-01T13:39:06.084Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7f/d8/735b129e7d55c4f6c682ecedfcc0438816e1d859e5e1837f914ac4544f6d/drf_spectacular_sidecar-2026.6.1.tar.gz", hash = "sha256:e159874fa85ccee39b801e260f2a3585fbe36a0c79bf811824eef9010ab98ea9", size = 2589761, upload-time = "2026-06-01T16:45:30.851Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/76/d08f5c79f7643dbff4512605c28b75481966ed6c8cc9b397c9dd2ee91cd1/drf_spectacular_sidecar-2026.7.1-py3-none-any.whl", hash = "sha256:bc6d50c9b64660e45e09296d39553b3e759eedd825fc41d631ee5b3f88e0c5de", size = 2617384, upload-time = "2026-07-01T13:39:03.787Z" },
|
{ url = "https://files.pythonhosted.org/packages/4f/46/10bcaf965edcb70e7647e62b88b9e0de266f8bea7a7a3224e8f977f77eab/drf_spectacular_sidecar-2026.6.1-py3-none-any.whl", hash = "sha256:4560572773c7e5f636d36cd2903204e2c59560af0548da254e311f94458c51a2", size = 2613235, upload-time = "2026-06-01T16:45:29.031Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1005,7 +1005,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython"
|
name = "ipython"
|
||||||
version = "9.15.0"
|
version = "9.14.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
@@ -1015,14 +1015,14 @@ dependencies = [
|
|||||||
{ name = "matplotlib-inline" },
|
{ name = "matplotlib-inline" },
|
||||||
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||||
{ name = "prompt-toolkit" },
|
{ name = "prompt-toolkit" },
|
||||||
{ name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" },
|
{ name = "psutil", marker = "sys_platform != 'emscripten'" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "stack-data" },
|
{ name = "stack-data" },
|
||||||
{ name = "traitlets" },
|
{ name = "traitlets" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e2/23/3a27530575643c8bb7bfc757a28e2e7ef80092afbf59a2bc5716320b6602/ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b", size = 4433457, upload-time = "2026-06-05T08:12:34.921Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/22/58818a63eaf8982b67632b1bc20585c811611b15a8da19d6012323dc76a5/ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c", size = 627770, upload-time = "2026-06-05T08:12:33.045Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1128,7 +1128,7 @@ redis = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-api"
|
name = "livekit-api"
|
||||||
version = "1.2.0"
|
version = "1.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
@@ -1137,22 +1137,22 @@ dependencies = [
|
|||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
{ name = "types-protobuf" },
|
{ name = "types-protobuf" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/19/36ff6712ec638a4b7dad4d8f03795952e401dc31db0b04cddec7892650da/livekit_api-1.2.0.tar.gz", hash = "sha256:a89817b3bca9584873786ff07209839308217537a42f95ecb2609aafaa109ddc", size = 20778, upload-time = "2026-07-11T23:20:54.781Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/03/00e0ec173f247e1f7ea63cb5591d5680a64c7a74ea4d5d558e5aed6cc399/livekit_api-1.1.1.tar.gz", hash = "sha256:70c7b80eecbc297b40756ebd76e4f52d00b0348fb7d212a21c1f69cc57fd9c83", size = 15196, upload-time = "2026-06-24T01:36:19.686Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/e7/8926f16d4bc1b2e0ae46d4a507321bb899396d263a757f1adaabcd3b3867/livekit_api-1.2.0-py3-none-any.whl", hash = "sha256:307f8e5cfb0358c3ca091814ab768af55896022151bcd7f951954ccefa036a24", size = 26499, upload-time = "2026-07-11T23:20:53.736Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/c0/d5f3ff74ab5db2d06f173801ec934885d11a754b9fb9ad768c8ede0a6c89/livekit_api-1.1.1-py3-none-any.whl", hash = "sha256:ce8c327676c366e66cf68782934368dd0ba92b9d48f578275227e255c890fe88", size = 19471, upload-time = "2026-06-24T01:36:18.42Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-protocol"
|
name = "livekit-protocol"
|
||||||
version = "1.1.21"
|
version = "1.1.18"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "protobuf" },
|
{ name = "protobuf" },
|
||||||
{ name = "types-protobuf" },
|
{ name = "types-protobuf" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/ae/9d60fe37d85623e68a2e36ae31d18c671949db67d2a13a6438410d930466/livekit_protocol-1.1.21.tar.gz", hash = "sha256:8bb1ac1aba5d37d0af43e9d56d129a5d16295cbd91518b00fd157e258f20a6ef", size = 122363, upload-time = "2026-07-21T18:28:26.372Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/88/64f2be01a630e249f1dbd0d51876f109b53b7899ae41246d2ca5b647086d/livekit_protocol-1.1.18.tar.gz", hash = "sha256:187af32ebf75333a62117b0db9e551c99060bd4e1f57cfc0fce73bcd7a671da8", size = 115802, upload-time = "2026-06-27T15:31:04.102Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/d2/ec10b1cdf912235c2b07898be6ba2535e50f23e8980bb719f29decbd8034/livekit_protocol-1.1.21-py3-none-any.whl", hash = "sha256:ce0bb763327c91349ee8831843c4f8bd72132c4d06ac572171f2ae5c0217211d", size = 149245, upload-time = "2026-07-21T18:28:24.985Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/80/9cc33e4d0280132538850aaf9559d6b8aa9e670c5917f75dab996400ab84/livekit_protocol-1.1.18-py3-none-any.whl", hash = "sha256:30c539410fd3cfc2e551ca3a193aaaaacaaec6dd57dabe2c9be7c7c7d15f0e01", size = 143134, upload-time = "2026-06-27T15:31:02.686Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1254,8 +1254,8 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiohttp", specifier = "==3.14.3" },
|
{ name = "aiohttp", specifier = "==3.14.1" },
|
||||||
{ name = "boto3", specifier = "==1.43.56" },
|
{ name = "boto3", specifier = "==1.43.36" },
|
||||||
{ name = "brevo-python", specifier = "==1.2.0" },
|
{ name = "brevo-python", specifier = "==1.2.0" },
|
||||||
{ name = "brotli", 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.3" },
|
||||||
@@ -1264,7 +1264,7 @@ requires-dist = [
|
|||||||
{ name = "django-configurations", specifier = "==2.5.1" },
|
{ name = "django-configurations", specifier = "==2.5.1" },
|
||||||
{ name = "django-cors-headers", specifier = "==4.9.0" },
|
{ name = "django-cors-headers", specifier = "==4.9.0" },
|
||||||
{ name = "django-countries", specifier = "==9.0.0" },
|
{ name = "django-countries", specifier = "==9.0.0" },
|
||||||
{ name = "django-filter", specifier = "==26.1" },
|
{ name = "django-filter", specifier = "==25.2" },
|
||||||
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.27" },
|
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.27" },
|
||||||
{ name = "django-parler", specifier = "==2.4" },
|
{ name = "django-parler", specifier = "==2.4" },
|
||||||
{ name = "django-pydantic-field", specifier = "==0.5.4" },
|
{ name = "django-pydantic-field", specifier = "==0.5.4" },
|
||||||
@@ -1273,17 +1273,17 @@ requires-dist = [
|
|||||||
{ name = "django-timezone-field", specifier = ">=5.1" },
|
{ name = "django-timezone-field", specifier = ">=5.1" },
|
||||||
{ name = "djangorestframework", specifier = "==3.17.1" },
|
{ name = "djangorestframework", specifier = "==3.17.1" },
|
||||||
{ name = "dockerflow", specifier = "==2026.3.4" },
|
{ name = "dockerflow", specifier = "==2026.3.4" },
|
||||||
{ name = "drf-spectacular", specifier = "==0.30.0" },
|
{ name = "drf-spectacular", specifier = "==0.29.0" },
|
||||||
{ name = "easy-thumbnails", specifier = "==2.10.1" },
|
{ name = "easy-thumbnails", specifier = "==2.10.1" },
|
||||||
{ name = "factory-boy", specifier = "==3.3.3" },
|
{ name = "factory-boy", specifier = "==3.3.3" },
|
||||||
{ name = "gunicorn", specifier = "==26.0.0" },
|
{ name = "gunicorn", specifier = "==26.0.0" },
|
||||||
{ name = "jsonschema", specifier = "==4.26.0" },
|
{ name = "jsonschema", specifier = "==4.26.0" },
|
||||||
{ name = "livekit-api", specifier = "==1.2.0" },
|
{ name = "livekit-api", specifier = "==1.1.1" },
|
||||||
{ name = "markdown", specifier = "==3.10.2" },
|
{ name = "markdown", specifier = "==3.10.2" },
|
||||||
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
|
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
|
||||||
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
|
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
|
||||||
{ name = "phonenumbers", specifier = "==9.0.34" },
|
{ name = "phonenumbers", specifier = "==9.0.33" },
|
||||||
{ name = "posthog", specifier = "==7.29.0" },
|
{ name = "posthog", specifier = "==7.16.1" },
|
||||||
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.4" },
|
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.4" },
|
||||||
{ name = "pydantic", specifier = "==2.13.4" },
|
{ name = "pydantic", specifier = "==2.13.4" },
|
||||||
{ name = "pyjwt", specifier = "==2.13.0" },
|
{ name = "pyjwt", specifier = "==2.13.0" },
|
||||||
@@ -1291,7 +1291,7 @@ requires-dist = [
|
|||||||
{ name = "python-magic", specifier = "==0.4.27" },
|
{ name = "python-magic", specifier = "==0.4.27" },
|
||||||
{ name = "redis", specifier = "==5.2.1" },
|
{ name = "redis", specifier = "==5.2.1" },
|
||||||
{ name = "requests", specifier = "==2.34.2" },
|
{ name = "requests", specifier = "==2.34.2" },
|
||||||
{ name = "sentry-sdk", specifier = "==2.66.1" },
|
{ name = "sentry-sdk", specifier = "==2.63.0" },
|
||||||
{ name = "urllib3", specifier = "==2.7.0" },
|
{ name = "urllib3", specifier = "==2.7.0" },
|
||||||
{ name = "whitenoise", specifier = "==6.12.0" },
|
{ name = "whitenoise", specifier = "==6.12.0" },
|
||||||
]
|
]
|
||||||
@@ -1299,21 +1299,21 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "django-extensions", specifier = "==4.1" },
|
{ name = "django-extensions", specifier = "==4.1" },
|
||||||
{ name = "drf-spectacular-sidecar", specifier = "==2026.7.1" },
|
{ name = "drf-spectacular-sidecar", specifier = "==2026.6.1" },
|
||||||
{ name = "freezegun", specifier = "==1.5.5" },
|
{ name = "freezegun", specifier = "==1.5.5" },
|
||||||
{ name = "ipdb", specifier = "==0.13.13" },
|
{ name = "ipdb", specifier = "==0.13.13" },
|
||||||
{ name = "ipython", specifier = "==9.15.0" },
|
{ name = "ipython", specifier = "==9.14.1" },
|
||||||
{ name = "pyfakefs", specifier = "==6.2.0" },
|
{ name = "pyfakefs", specifier = "==6.2.0" },
|
||||||
{ name = "pylint", specifier = "<4.0.0" },
|
{ name = "pylint", specifier = "<4.0.0" },
|
||||||
{ name = "pylint-django", specifier = "==2.8.0" },
|
{ name = "pylint-django", specifier = "==2.7.0" },
|
||||||
{ name = "pytest", specifier = "==9.1.1" },
|
{ name = "pytest", specifier = "==9.1.1" },
|
||||||
{ name = "pytest-cov", specifier = "==7.1.0" },
|
{ name = "pytest-cov", specifier = "==7.1.0" },
|
||||||
{ name = "pytest-django", specifier = "==4.12.0" },
|
{ name = "pytest-django", specifier = "==4.12.0" },
|
||||||
{ name = "pytest-icdiff", specifier = "==0.9" },
|
{ name = "pytest-icdiff", specifier = "==0.9" },
|
||||||
{ name = "pytest-xdist", specifier = "==3.8.0" },
|
{ name = "pytest-xdist", specifier = "==3.8.0" },
|
||||||
{ name = "responses", specifier = "==0.26.2" },
|
{ name = "responses", specifier = "==0.26.1" },
|
||||||
{ name = "ruff", specifier = "==0.16.0" },
|
{ name = "ruff", specifier = "==0.15.19" },
|
||||||
{ name = "types-requests", specifier = "==2.33.0.20260712" },
|
{ name = "types-requests", specifier = "==2.33.0.20260518" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1450,11 +1450,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phonenumbers"
|
name = "phonenumbers"
|
||||||
version = "9.0.34"
|
version = "9.0.33"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/86/c3/e154829a50679c38ae28ec9c4f151f2c425db5e70fd445266e76f6d6cd65/phonenumbers-9.0.34.tar.gz", hash = "sha256:00751c75d1166485ca80ce02ec15b6a61a2628e9b313381579330bc70c934075", size = 2306776, upload-time = "2026-07-03T06:30:37.358Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/75/37/dfc4cf24169f1a7169ebaedaf896c818f0add8603409d1e748e3085ccdc0/phonenumbers-9.0.33.tar.gz", hash = "sha256:9ab8a02b940b90c64f3866c0b25a30e567ddf7bb9836a3e11efdb0478f65fc1c", size = 2306756, upload-time = "2026-06-22T10:23:33.428Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/0a/3a7980f3b071dde9a297d85cf6b18ba4bc2e5024e1c453b3da8a1dc14268/phonenumbers-9.0.34-py2.py3-none-any.whl", hash = "sha256:1221bf8e65bd2c02770226488af806d4636814bc997104d3a1f7de6ed6410bd2", size = 2595344, upload-time = "2026-07-03T06:30:33.913Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/29/f7e30e3dbd3c7e3d9c4a55006112c04ee62b4765a31f21bcc28c253ac3f1/phonenumbers-9.0.33-py2.py3-none-any.whl", hash = "sha256:ba1d0da52711d5fdda6b2b673b2621fe80774fc5d1b2e5a6ef783396b0343186", size = 2595422, upload-time = "2026-06-22T10:23:29.925Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1539,7 +1539,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "posthog"
|
name = "posthog"
|
||||||
version = "7.29.0"
|
version = "7.16.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "backoff" },
|
{ name = "backoff" },
|
||||||
@@ -1547,9 +1547,9 @@ dependencies = [
|
|||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/09/43ae0e27bafe031d307921cdeadcd3af8a7370c887177df7c43cfd903adf/posthog-7.29.0.tar.gz", hash = "sha256:673f201e2d204f0664bb0cec86f8adfca97ba3a94488fb7857bb11c4a35fb017", size = 360483, upload-time = "2026-07-23T15:26:28.225Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/b4/4f/a954175c862a3565d02c3f627874d85f18313472a0c4b08f45d84aaf3315/posthog-7.16.1.tar.gz", hash = "sha256:3619d3c619ad01f36c6d465e084950882417c63021eb3cfacacb23f900ec52d4", size = 226343, upload-time = "2026-05-27T18:46:20.129Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/4a/01c3e9f44a2f167977b5fafc9ba5b4ed5a027881da4b2ee1f4987a621758/posthog-7.29.0-py3-none-any.whl", hash = "sha256:8269afec8439e0177fd9d660b88d0a580497b617ee6aa213850ae9244ad46111", size = 429592, upload-time = "2026-07-23T15:26:26.343Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/28/0f840699a1d0db3c1e5483c6208f0804a51f21ccfa34e6aa356161606adc/posthog-7.16.1-py3-none-any.whl", hash = "sha256:fd5aa4510033f3b039fda2fbfce45f493d140d4782f681e69639793dda317d67", size = 264231, upload-time = "2026-05-27T18:46:17.933Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1884,15 +1884,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pylint-django"
|
name = "pylint-django"
|
||||||
version = "2.8.0"
|
version = "2.7.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pylint" },
|
{ name = "pylint" },
|
||||||
{ name = "pylint-plugin-utils" },
|
{ name = "pylint-plugin-utils" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/71/a1/b92e5d5cf320b603c9bcc5174da7e9ba4c6ce71087354322a2b83536df13/pylint_django-2.8.0.tar.gz", hash = "sha256:42accea9098e4a3298b4bfbae0e4da81f909f8bff0deda9485efbd6035a86d6a", size = 32038, upload-time = "2026-07-11T10:19:14.844Z" }
|
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/4a/3dae8a09e12a28ccf3d7204cc4fb69f28dfb30d2b19567eb5ff094fe1265/pylint_django-2.8.0-py3-none-any.whl", hash = "sha256:706eb2cc8d7692236be9fd033a341042afe3bbbf99df9234a659db931016ef5d", size = 44672, upload-time = "2026-07-11T10:05:29.281Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/0d/d775fec0dde8ca5d20e9170a2ca332dfa21b77f7e7e47fc3ab9b2261773c/pylint_django-2.7.0-py3-none-any.whl", hash = "sha256:76ef7e7bbbcf7ee86adbb2beac0ffaa7232509a17bf4a488d81467a1bbaa215b", size = 42892, upload-time = "2026-01-01T11:17:04.292Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2096,16 +2095,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "responses"
|
name = "responses"
|
||||||
version = "0.26.2"
|
version = "0.26.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/1a/4af3e6d659394b809838490b144e4ab8d7ed3b9fecc7ca78f5d2f79b1a3d/responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8", size = 84030, upload-time = "2026-07-03T16:44:50.325Z" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/28/693e1d9ebf72baa062ded80d837a035b86ce75eda5a269379e9e2b1008a8/responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364", size = 35609, upload-time = "2026-07-03T16:44:49.1Z" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2191,27 +2190,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.16.0"
|
version = "0.15.19"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
|
{ url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
|
{ url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2228,15 +2227,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.66.1"
|
version = "2.63.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2300,14 +2299,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-requests"
|
name = "types-requests"
|
||||||
version = "2.33.0.20260712"
|
version = "2.33.0.20260518"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" }
|
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" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" },
|
{ 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" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -59,10 +59,9 @@ export const Avatar = React.memo(
|
|||||||
<text
|
<text
|
||||||
x="50"
|
x="50"
|
||||||
y="50"
|
y="50"
|
||||||
dy="-0.08em"
|
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fontSize="52"
|
fontSize={initials.length > 1 ? 48 : 52}
|
||||||
fontWeight="500"
|
fontWeight="500"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ dependencies = [
|
|||||||
"celery==5.6.3",
|
"celery==5.6.3",
|
||||||
"redis==5.2.1",
|
"redis==5.2.1",
|
||||||
"minio==7.2.20",
|
"minio==7.2.20",
|
||||||
"openai==2.48.0",
|
"openai==2.44.0",
|
||||||
"posthog==7.29.0",
|
"posthog==7.20.5",
|
||||||
"requests==2.34.2",
|
"requests==2.34.2",
|
||||||
"sentry-sdk[fastapi, celery]==2.66.1",
|
"sentry-sdk[fastapi, celery]==2.63.0",
|
||||||
"langfuse==4.14.1"
|
"langfuse==4.11.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff==0.16.0",
|
"ruff==0.15.19",
|
||||||
"pytest==9.1.1",
|
"pytest==9.1.1",
|
||||||
"responses>=0.25.8",
|
"responses>=0.25.8",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user