Compare commits
2 Commits
8a1da13383
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96de2d2573 | |||
| 30e7d1b65f |
@@ -3,7 +3,5 @@ kind: Kustomization
|
|||||||
|
|
||||||
resources:
|
resources:
|
||||||
- userbots.yaml
|
- userbots.yaml
|
||||||
- common-secret.yaml
|
- panel.yaml
|
||||||
- common-config.yaml
|
- common-config.yaml
|
||||||
- forust-secrets.yaml
|
|
||||||
- anna-secrets.yaml
|
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: userbot
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: userbot-runtime
|
||||||
|
namespace: userbot
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: userbot-common-config
|
||||||
|
namespace: userbot
|
||||||
|
data:
|
||||||
|
DATABASE_TYPE: ""
|
||||||
|
DATABASE_NAME: ""
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- deployments
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- deployments/scale
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods/log
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- persistentvolumeclaims
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- metrics.k8s.io
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: userbot-panel
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel-legacy
|
||||||
|
namespace: default
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- deployments
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- deployments/scale
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods/log
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- persistentvolumeclaims
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- apiGroups:
|
||||||
|
- metrics.k8s.io
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel-legacy
|
||||||
|
namespace: default
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: userbot-panel-legacy
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
labels:
|
||||||
|
app: userbot-panel
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: userbot-panel
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: userbot-panel
|
||||||
|
spec:
|
||||||
|
serviceAccountName: userbot-panel
|
||||||
|
containers:
|
||||||
|
- name: userbot-panel
|
||||||
|
image: gcr.forust.xyz/forust/userbot-panel:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: USERBOT_NAMESPACE
|
||||||
|
value: userbot
|
||||||
|
- name: USERBOT_LEGACY_NAMESPACES
|
||||||
|
value: default
|
||||||
|
- name: USERBOT_IMAGE
|
||||||
|
value: gcr.forust.xyz/forust/userbot:latest
|
||||||
|
- name: USERBOT_STORAGE_CLASS
|
||||||
|
value: local-path-retain
|
||||||
|
- name: USERBOT_DOWNLOADS_HOST_PATH
|
||||||
|
value: /srv/homelab/userbot/Downloads
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 20m
|
||||||
|
memory: 96Mi
|
||||||
|
limits:
|
||||||
|
cpu: 300m
|
||||||
|
memory: 384Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel
|
||||||
|
namespace: userbot
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: userbot-panel
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
targetPort: http
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: userbot-panel-route
|
||||||
|
namespace: userbot
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`userbot.workstation.internal`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: userbot-panel
|
||||||
|
port: 8080
|
||||||
+30
-2
@@ -4,8 +4,14 @@ metadata:
|
|||||||
name: forust-userbot-deployment
|
name: forust-userbot-deployment
|
||||||
labels:
|
labels:
|
||||||
app: forust-userbot
|
app: forust-userbot
|
||||||
|
app.kubernetes.io/name: userbot
|
||||||
|
app.kubernetes.io/instance: forust
|
||||||
|
annotations:
|
||||||
|
userbot.forust.xyz/display-name: forust
|
||||||
|
userbot.forust.xyz/legacy: "true"
|
||||||
|
userbot.forust.xyz/credentials-secret: userbot-forust-secrets
|
||||||
|
userbot.forust.xyz/pvc: forust-pvc
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: forust-userbot
|
app: forust-userbot
|
||||||
@@ -13,6 +19,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: forust-userbot
|
app: forust-userbot
|
||||||
|
annotations:
|
||||||
|
kubectl.kubernetes.io/restartedAt: "2026-07-25T20:24:51+02:00"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: forust-userbot
|
- name: forust-userbot
|
||||||
@@ -33,6 +41,8 @@ spec:
|
|||||||
- secretRef:
|
- secretRef:
|
||||||
name: userbot-forust-secrets
|
name: userbot-forust-secrets
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /app/downloads
|
||||||
- name: forust-storage
|
- name: forust-storage
|
||||||
mountPath: /app/data
|
mountPath: /app/data
|
||||||
subPath: data
|
subPath: data
|
||||||
@@ -40,6 +50,10 @@ spec:
|
|||||||
mountPath: /app/logs
|
mountPath: /app/logs
|
||||||
subPath: logs
|
subPath: logs
|
||||||
volumes:
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
hostPath:
|
||||||
|
path: /srv/homelab/userbot/Downloads
|
||||||
|
type: DirectoryOrCreate
|
||||||
- name: forust-storage
|
- name: forust-storage
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: forust-pvc
|
claimName: forust-pvc
|
||||||
@@ -62,8 +76,14 @@ metadata:
|
|||||||
name: anna-userbot-deployment
|
name: anna-userbot-deployment
|
||||||
labels:
|
labels:
|
||||||
app: anna-userbot
|
app: anna-userbot
|
||||||
|
app.kubernetes.io/name: userbot
|
||||||
|
app.kubernetes.io/instance: anna
|
||||||
|
annotations:
|
||||||
|
userbot.forust.xyz/display-name: anna
|
||||||
|
userbot.forust.xyz/legacy: "true"
|
||||||
|
userbot.forust.xyz/credentials-secret: userbot-anna-secrets
|
||||||
|
userbot.forust.xyz/pvc: anna-pvc
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: anna-userbot
|
app: anna-userbot
|
||||||
@@ -71,6 +91,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: anna-userbot
|
app: anna-userbot
|
||||||
|
annotations:
|
||||||
|
kubectl.kubernetes.io/restartedAt: "2026-07-25T20:24:51+02:00"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: anna-userbot
|
- name: anna-userbot
|
||||||
@@ -91,6 +113,8 @@ spec:
|
|||||||
- secretRef:
|
- secretRef:
|
||||||
name: userbot-anna-secrets
|
name: userbot-anna-secrets
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
- name: downloads
|
||||||
|
mountPath: /app/downloads
|
||||||
- name: anna-storage
|
- name: anna-storage
|
||||||
mountPath: /app/data
|
mountPath: /app/data
|
||||||
subPath: data
|
subPath: data
|
||||||
@@ -98,6 +122,10 @@ spec:
|
|||||||
mountPath: /app/logs
|
mountPath: /app/logs
|
||||||
subPath: logs
|
subPath: logs
|
||||||
volumes:
|
volumes:
|
||||||
|
- name: downloads
|
||||||
|
hostPath:
|
||||||
|
path: /srv/homelab/userbot/Downloads
|
||||||
|
type: DirectoryOrCreate
|
||||||
- name: anna-storage
|
- name: anna-storage
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: anna-pvc
|
claimName: anna-pvc
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
backend/.venv
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
.ruff_cache
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
backend/.venv/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
FROM node:22-alpine AS frontend
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM python:3.13-slim AS python-builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY backend/requirements.txt ./
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential \
|
||||||
|
&& pip install --no-cache-dir --prefix=/install -r requirements.txt \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PANEL_STATIC_DIR=/app/static
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=python-builder /install /usr/local
|
||||||
|
COPY backend/app ./app
|
||||||
|
COPY --from=frontend /src/dist ./static
|
||||||
|
|
||||||
|
USER 65532:65532
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Userbot Control Panel
|
||||||
|
|
||||||
|
Локальная панель управляет userbot-инстансами напрямую через Kubernetes API.
|
||||||
|
Новые аккаунты создаются в namespace `userbot`; существующие `forust` и `anna`
|
||||||
|
остаются в `default` и принимаются под управление только через metadata labels.
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
Скопировать общий Secret в новый namespace, не печатая его значения:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create namespace userbot --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl get secret userbot-common-secrets -n default -o json \
|
||||||
|
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
|
||||||
|
| kubectl apply -f -
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем применить манифесты:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f userbot/k8s/base/panel.yaml
|
||||||
|
kubectl apply -f userbot/k8s/base/userbots.yaml
|
||||||
|
kubectl rollout status deployment/userbot-panel -n userbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Панель доступна только через внутренний Traefik route:
|
||||||
|
`https://userbot.workstation.internal`.
|
||||||
|
|
||||||
|
## Локальная разработка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd userbot/panel/frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend использует in-cluster config, а вне кластера — текущий kubeconfig:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd userbot/panel/backend
|
||||||
|
uv run --with-requirements requirements-dev.txt \
|
||||||
|
uvicorn app.main:app --reload --port 8080
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Userbot Kubernetes control panel backend."""
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import secrets
|
||||||
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
from pyrogram import Client
|
||||||
|
from pyrogram.errors import (
|
||||||
|
ApiIdInvalid,
|
||||||
|
FloodWait,
|
||||||
|
PasswordHashInvalid,
|
||||||
|
PhoneCodeExpired,
|
||||||
|
PhoneCodeInvalid,
|
||||||
|
PhoneNumberInvalid,
|
||||||
|
RPCError,
|
||||||
|
SessionPasswordNeeded,
|
||||||
|
Unauthorized,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .errors import PanelError
|
||||||
|
from .models import AccountBase, PhoneStart, StringSessionStart
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuthFlow:
|
||||||
|
flow_id: str
|
||||||
|
account: PhoneStart
|
||||||
|
client: Client
|
||||||
|
phone_code_hash: str
|
||||||
|
expires_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuthorizedAccount:
|
||||||
|
account: AccountBase
|
||||||
|
session_string: str
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramAuthService:
|
||||||
|
def __init__(self, ttl_seconds: int = 600) -> None:
|
||||||
|
self.ttl = timedelta(seconds=ttl_seconds)
|
||||||
|
self.flows: dict[str, AuthFlow] = {}
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def start_phone(self, account: PhoneStart) -> str:
|
||||||
|
await self._cleanup_expired()
|
||||||
|
flow_id = secrets.token_urlsafe(24)
|
||||||
|
telegram = Client(
|
||||||
|
f"auth-{flow_id}",
|
||||||
|
api_id=account.api_id,
|
||||||
|
api_hash=account.api_hash,
|
||||||
|
in_memory=True,
|
||||||
|
no_updates=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await telegram.connect()
|
||||||
|
sent = await telegram.send_code(account.phone)
|
||||||
|
except Exception as exc:
|
||||||
|
await self._disconnect(telegram)
|
||||||
|
raise self._translate(exc) from exc
|
||||||
|
|
||||||
|
flow = AuthFlow(
|
||||||
|
flow_id=flow_id,
|
||||||
|
account=account,
|
||||||
|
client=telegram,
|
||||||
|
phone_code_hash=sent.phone_code_hash,
|
||||||
|
expires_at=datetime.now(UTC) + self.ttl,
|
||||||
|
)
|
||||||
|
async with self._lock:
|
||||||
|
self.flows[flow_id] = flow
|
||||||
|
return flow_id
|
||||||
|
|
||||||
|
async def submit_code(
|
||||||
|
self,
|
||||||
|
flow_id: str,
|
||||||
|
code: str,
|
||||||
|
) -> AuthorizedAccount | None:
|
||||||
|
flow = await self._get(flow_id)
|
||||||
|
try:
|
||||||
|
await flow.client.sign_in(
|
||||||
|
flow.account.phone,
|
||||||
|
flow.phone_code_hash,
|
||||||
|
code,
|
||||||
|
)
|
||||||
|
except SessionPasswordNeeded:
|
||||||
|
return None
|
||||||
|
except PhoneCodeExpired as exc:
|
||||||
|
await self._discard(flow)
|
||||||
|
raise self._translate(exc) from exc
|
||||||
|
except Exception as exc:
|
||||||
|
raise self._translate(exc) from exc
|
||||||
|
return await self._finish(flow)
|
||||||
|
|
||||||
|
async def submit_password(
|
||||||
|
self,
|
||||||
|
flow_id: str,
|
||||||
|
password: str,
|
||||||
|
) -> AuthorizedAccount:
|
||||||
|
flow = await self._get(flow_id)
|
||||||
|
try:
|
||||||
|
await flow.client.check_password(password)
|
||||||
|
except Exception as exc:
|
||||||
|
raise self._translate(exc) from exc
|
||||||
|
return await self._finish(flow)
|
||||||
|
|
||||||
|
async def validate_string_session(
|
||||||
|
self,
|
||||||
|
account: StringSessionStart,
|
||||||
|
) -> AuthorizedAccount:
|
||||||
|
telegram = Client(
|
||||||
|
f"validate-{secrets.token_urlsafe(12)}",
|
||||||
|
api_id=account.api_id,
|
||||||
|
api_hash=account.api_hash,
|
||||||
|
session_string=account.session_string,
|
||||||
|
in_memory=True,
|
||||||
|
no_updates=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await telegram.connect()
|
||||||
|
await telegram.get_me()
|
||||||
|
session_string = await telegram.export_session_string()
|
||||||
|
except Exception as exc:
|
||||||
|
raise self._translate(exc, session=True) from exc
|
||||||
|
finally:
|
||||||
|
await self._disconnect(telegram)
|
||||||
|
return AuthorizedAccount(account=account, session_string=session_string)
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
async with self._lock:
|
||||||
|
flows = list(self.flows.values())
|
||||||
|
self.flows.clear()
|
||||||
|
await asyncio.gather(
|
||||||
|
*(self._disconnect(flow.client) for flow in flows),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _get(self, flow_id: str) -> AuthFlow:
|
||||||
|
await self._cleanup_expired()
|
||||||
|
async with self._lock:
|
||||||
|
flow = self.flows.get(flow_id)
|
||||||
|
if flow is None:
|
||||||
|
raise PanelError(410, "Authorization flow expired; start again")
|
||||||
|
return flow
|
||||||
|
|
||||||
|
async def _finish(self, flow: AuthFlow) -> AuthorizedAccount:
|
||||||
|
try:
|
||||||
|
await flow.client.get_me()
|
||||||
|
session_string = await flow.client.export_session_string()
|
||||||
|
except Exception as exc:
|
||||||
|
raise self._translate(exc) from exc
|
||||||
|
finally:
|
||||||
|
async with self._lock:
|
||||||
|
self.flows.pop(flow.flow_id, None)
|
||||||
|
await self._disconnect(flow.client)
|
||||||
|
return AuthorizedAccount(account=flow.account, session_string=session_string)
|
||||||
|
|
||||||
|
async def _discard(self, flow: AuthFlow) -> None:
|
||||||
|
async with self._lock:
|
||||||
|
self.flows.pop(flow.flow_id, None)
|
||||||
|
await self._disconnect(flow.client)
|
||||||
|
|
||||||
|
async def _cleanup_expired(self) -> None:
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
async with self._lock:
|
||||||
|
expired = [
|
||||||
|
self.flows.pop(flow_id)
|
||||||
|
for flow_id, flow in list(self.flows.items())
|
||||||
|
if flow.expires_at <= now
|
||||||
|
]
|
||||||
|
if expired:
|
||||||
|
await asyncio.gather(
|
||||||
|
*(self._disconnect(flow.client) for flow in expired),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _disconnect(telegram: Client) -> None:
|
||||||
|
with suppress(Exception):
|
||||||
|
if telegram.is_connected:
|
||||||
|
await telegram.disconnect()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _translate(exc: Exception, *, session: bool = False) -> PanelError:
|
||||||
|
if isinstance(exc, FloodWait):
|
||||||
|
return PanelError(429, f"Telegram rate limit; retry in {exc.value} seconds")
|
||||||
|
if isinstance(exc, ApiIdInvalid):
|
||||||
|
return PanelError(422, "Telegram API ID or API Hash is invalid")
|
||||||
|
if isinstance(exc, PhoneNumberInvalid):
|
||||||
|
return PanelError(422, "Phone number is invalid")
|
||||||
|
if isinstance(exc, PhoneCodeInvalid):
|
||||||
|
return PanelError(422, "Telegram code is invalid")
|
||||||
|
if isinstance(exc, PhoneCodeExpired):
|
||||||
|
return PanelError(410, "Telegram code expired; start again")
|
||||||
|
if isinstance(exc, PasswordHashInvalid):
|
||||||
|
return PanelError(422, "2FA password is invalid")
|
||||||
|
if session and isinstance(exc, (Unauthorized, RPCError)):
|
||||||
|
return PanelError(422, "StringSession is invalid or expired")
|
||||||
|
if isinstance(exc, RPCError):
|
||||||
|
return PanelError(422, "Telegram rejected the authorization request")
|
||||||
|
return PanelError(503, "Telegram authorization is unavailable")
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Settings:
|
||||||
|
namespace: str = os.environ.get("USERBOT_NAMESPACE", "userbot")
|
||||||
|
legacy_namespaces: tuple[str, ...] = tuple(
|
||||||
|
value.strip()
|
||||||
|
for value in os.environ.get("USERBOT_LEGACY_NAMESPACES", "default").split(",")
|
||||||
|
if value.strip()
|
||||||
|
)
|
||||||
|
image: str = os.environ.get(
|
||||||
|
"USERBOT_IMAGE",
|
||||||
|
"gcr.forust.xyz/forust/userbot:latest",
|
||||||
|
)
|
||||||
|
common_secret: str = os.environ.get(
|
||||||
|
"USERBOT_COMMON_SECRET",
|
||||||
|
"userbot-common-secrets",
|
||||||
|
)
|
||||||
|
common_config: str = os.environ.get(
|
||||||
|
"USERBOT_COMMON_CONFIG",
|
||||||
|
"userbot-common-config",
|
||||||
|
)
|
||||||
|
storage_class: str = os.environ.get(
|
||||||
|
"USERBOT_STORAGE_CLASS",
|
||||||
|
"local-path-retain",
|
||||||
|
)
|
||||||
|
downloads_host_path: str = os.environ.get(
|
||||||
|
"USERBOT_DOWNLOADS_HOST_PATH",
|
||||||
|
"/srv/homelab/userbot/Downloads",
|
||||||
|
)
|
||||||
|
static_dir: Path = Path(os.environ.get("PANEL_STATIC_DIR", "/app/static"))
|
||||||
|
auth_ttl_seconds: int = int(os.environ.get("PANEL_AUTH_TTL_SECONDS", "600"))
|
||||||
|
default_storage: str = os.environ.get("USERBOT_DEFAULT_STORAGE", "1Gi")
|
||||||
|
default_cpu_limit: str = os.environ.get("USERBOT_DEFAULT_CPU_LIMIT", "300m")
|
||||||
|
default_memory_limit: str = os.environ.get(
|
||||||
|
"USERBOT_DEFAULT_MEMORY_LIMIT",
|
||||||
|
"1536Mi",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class PanelError(Exception):
|
||||||
|
def __init__(self, status_code: int, detail: str) -> None:
|
||||||
|
super().__init__(detail)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.detail = detail
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from kubernetes import client, config
|
||||||
|
from kubernetes.client.exceptions import ApiException
|
||||||
|
|
||||||
|
from .config import Settings
|
||||||
|
from .errors import PanelError
|
||||||
|
from .models import AccountBase, InstanceSummary
|
||||||
|
|
||||||
|
MANAGED_LABEL = "app.kubernetes.io/name=userbot"
|
||||||
|
INSTANCE_LABEL = "app.kubernetes.io/instance"
|
||||||
|
MANAGED_BY_LABEL = "app.kubernetes.io/managed-by"
|
||||||
|
DISPLAY_ANNOTATION = "userbot.forust.xyz/display-name"
|
||||||
|
LEGACY_ANNOTATION = "userbot.forust.xyz/legacy"
|
||||||
|
CREDENTIALS_ANNOTATION = "userbot.forust.xyz/credentials-secret"
|
||||||
|
PVC_ANNOTATION = "userbot.forust.xyz/pvc"
|
||||||
|
RESTART_ANNOTATION = "userbot.forust.xyz/restarted-at"
|
||||||
|
|
||||||
|
|
||||||
|
def _selector(labels: dict[str, str] | None) -> str:
|
||||||
|
return ",".join(f"{key}={value}" for key, value in (labels or {}).items())
|
||||||
|
|
||||||
|
|
||||||
|
def _as_datetime(value: Any) -> datetime | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value
|
||||||
|
return getattr(value, "replace", lambda **_: None)(tzinfo=UTC)
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesService:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
settings: Settings,
|
||||||
|
*,
|
||||||
|
core: client.CoreV1Api | None = None,
|
||||||
|
apps: client.AppsV1Api | None = None,
|
||||||
|
custom: client.CustomObjectsApi | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.settings = settings
|
||||||
|
if core is None or apps is None:
|
||||||
|
try:
|
||||||
|
config.load_incluster_config()
|
||||||
|
except config.ConfigException:
|
||||||
|
config.load_kube_config()
|
||||||
|
self.core = core or client.CoreV1Api()
|
||||||
|
self.apps = apps or client.AppsV1Api()
|
||||||
|
self.custom = custom or client.CustomObjectsApi()
|
||||||
|
|
||||||
|
def ensure_prerequisites(self) -> None:
|
||||||
|
try:
|
||||||
|
self.core.read_namespaced_secret(
|
||||||
|
self.settings.common_secret,
|
||||||
|
self.settings.namespace,
|
||||||
|
)
|
||||||
|
self.core.read_namespaced_config_map(
|
||||||
|
self.settings.common_config,
|
||||||
|
self.settings.namespace,
|
||||||
|
)
|
||||||
|
except ApiException as exc:
|
||||||
|
if exc.status == 404:
|
||||||
|
raise PanelError(
|
||||||
|
503,
|
||||||
|
"Userbot common Secret or ConfigMap is missing in the userbot namespace",
|
||||||
|
) from exc
|
||||||
|
raise self._api_error(exc, "Could not verify userbot prerequisites") from exc
|
||||||
|
|
||||||
|
def list_instances(
|
||||||
|
self,
|
||||||
|
query: str = "",
|
||||||
|
status: str = "",
|
||||||
|
) -> list[InstanceSummary]:
|
||||||
|
instances: list[InstanceSummary] = []
|
||||||
|
for namespace in (self.settings.namespace, *self.settings.legacy_namespaces):
|
||||||
|
try:
|
||||||
|
deployments = self.apps.list_namespaced_deployment(
|
||||||
|
namespace,
|
||||||
|
label_selector=MANAGED_LABEL,
|
||||||
|
).items
|
||||||
|
except ApiException as exc:
|
||||||
|
raise self._api_error(exc, f"Could not list Deployments in {namespace}") from exc
|
||||||
|
instances.extend(self._summarize(namespace, deployment) for deployment in deployments)
|
||||||
|
|
||||||
|
query = query.strip().lower()
|
||||||
|
if query:
|
||||||
|
instances = [
|
||||||
|
item
|
||||||
|
for item in instances
|
||||||
|
if query in item.instance_id.lower()
|
||||||
|
or query in item.display_name.lower()
|
||||||
|
or query in (item.pod or "").lower()
|
||||||
|
]
|
||||||
|
if status:
|
||||||
|
instances = [item for item in instances if item.status == status]
|
||||||
|
return sorted(instances, key=lambda item: (item.legacy, item.instance_id))
|
||||||
|
|
||||||
|
def get_instance(self, instance_id: str) -> InstanceSummary:
|
||||||
|
namespace, deployment = self._find_deployment(instance_id)
|
||||||
|
return self._summarize(namespace, deployment)
|
||||||
|
|
||||||
|
def assert_available(self, instance_id: str) -> None:
|
||||||
|
names = self._resource_names(instance_id)
|
||||||
|
checks = (
|
||||||
|
(self.apps.read_namespaced_deployment, names["deployment"], "Deployment"),
|
||||||
|
(self.core.read_namespaced_secret, names["secret"], "Secret"),
|
||||||
|
(self.core.read_namespaced_persistent_volume_claim, names["pvc"], "PVC"),
|
||||||
|
)
|
||||||
|
for read, name, kind in checks:
|
||||||
|
try:
|
||||||
|
read(name, self.settings.namespace)
|
||||||
|
except ApiException as exc:
|
||||||
|
if exc.status == 404:
|
||||||
|
continue
|
||||||
|
raise self._api_error(exc, f"Could not check {kind} {name}") from exc
|
||||||
|
raise PanelError(409, f"{kind} {name} already exists")
|
||||||
|
|
||||||
|
def provision(self, account: AccountBase, session_string: str) -> InstanceSummary:
|
||||||
|
self.ensure_prerequisites()
|
||||||
|
self.assert_available(account.instance_id)
|
||||||
|
names = self._resource_names(account.instance_id)
|
||||||
|
created: list[tuple[str, str]] = []
|
||||||
|
try:
|
||||||
|
self.core.create_namespaced_secret(
|
||||||
|
self.settings.namespace,
|
||||||
|
self._secret(account, session_string, names),
|
||||||
|
)
|
||||||
|
created.append(("secret", names["secret"]))
|
||||||
|
self.core.create_namespaced_persistent_volume_claim(
|
||||||
|
self.settings.namespace,
|
||||||
|
self._pvc(account, names),
|
||||||
|
)
|
||||||
|
created.append(("pvc", names["pvc"]))
|
||||||
|
self.apps.create_namespaced_deployment(
|
||||||
|
self.settings.namespace,
|
||||||
|
self._deployment(account, names),
|
||||||
|
)
|
||||||
|
created.append(("deployment", names["deployment"]))
|
||||||
|
except ApiException as exc:
|
||||||
|
self._rollback(created)
|
||||||
|
raise self._api_error(exc, "Could not create userbot instance") from exc
|
||||||
|
return self.get_instance(account.instance_id)
|
||||||
|
|
||||||
|
def scale(self, instance_id: str, replicas: int) -> InstanceSummary:
|
||||||
|
namespace, deployment = self._find_deployment(instance_id)
|
||||||
|
try:
|
||||||
|
self.apps.patch_namespaced_deployment_scale(
|
||||||
|
deployment.metadata.name,
|
||||||
|
namespace,
|
||||||
|
{"spec": {"replicas": replicas}},
|
||||||
|
)
|
||||||
|
except ApiException as exc:
|
||||||
|
raise self._api_error(exc, "Could not scale userbot instance") from exc
|
||||||
|
return self.get_instance(instance_id)
|
||||||
|
|
||||||
|
def restart(self, instance_id: str) -> InstanceSummary:
|
||||||
|
namespace, deployment = self._find_deployment(instance_id)
|
||||||
|
if (deployment.spec.replicas or 0) == 0:
|
||||||
|
raise PanelError(409, "Stopped instance cannot be restarted")
|
||||||
|
timestamp = datetime.now(UTC).isoformat()
|
||||||
|
try:
|
||||||
|
self.apps.patch_namespaced_deployment(
|
||||||
|
deployment.metadata.name,
|
||||||
|
namespace,
|
||||||
|
{
|
||||||
|
"spec": {
|
||||||
|
"template": {
|
||||||
|
"metadata": {
|
||||||
|
"annotations": {RESTART_ANNOTATION: timestamp},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except ApiException as exc:
|
||||||
|
raise self._api_error(exc, "Could not restart userbot instance") from exc
|
||||||
|
return self.get_instance(instance_id)
|
||||||
|
|
||||||
|
def logs(self, instance_id: str, tail: int = 250) -> str:
|
||||||
|
namespace, deployment = self._find_deployment(instance_id)
|
||||||
|
pods = self._pods_for_deployment(namespace, deployment)
|
||||||
|
if not pods:
|
||||||
|
raise PanelError(409, "Userbot Pod is not running")
|
||||||
|
pod = pods[0]
|
||||||
|
container = deployment.spec.template.spec.containers[0].name
|
||||||
|
try:
|
||||||
|
return self.core.read_namespaced_pod_log(
|
||||||
|
pod.metadata.name,
|
||||||
|
namespace,
|
||||||
|
container=container,
|
||||||
|
tail_lines=max(1, min(tail, 1000)),
|
||||||
|
timestamps=True,
|
||||||
|
)
|
||||||
|
except ApiException as exc:
|
||||||
|
raise self._api_error(exc, "Could not read userbot logs") from exc
|
||||||
|
|
||||||
|
def delete(self, instance_id: str, *, delete_data: bool) -> None:
|
||||||
|
namespace, deployment = self._find_deployment(instance_id)
|
||||||
|
annotations = deployment.metadata.annotations or {}
|
||||||
|
if annotations.get(LEGACY_ANNOTATION) == "true" or namespace != self.settings.namespace:
|
||||||
|
raise PanelError(409, "Legacy instances cannot be deleted from the panel")
|
||||||
|
|
||||||
|
names = self._resource_names(instance_id)
|
||||||
|
secret_name = annotations.get(CREDENTIALS_ANNOTATION, names["secret"])
|
||||||
|
pvc_name = annotations.get(PVC_ANNOTATION, names["pvc"])
|
||||||
|
operations = [
|
||||||
|
(
|
||||||
|
self.apps.delete_namespaced_deployment,
|
||||||
|
(deployment.metadata.name, namespace),
|
||||||
|
{"propagation_policy": "Foreground"},
|
||||||
|
),
|
||||||
|
(self.core.delete_namespaced_secret, (secret_name, namespace), {}),
|
||||||
|
]
|
||||||
|
if delete_data:
|
||||||
|
operations.append(
|
||||||
|
(
|
||||||
|
self.core.delete_namespaced_persistent_volume_claim,
|
||||||
|
(pvc_name, namespace),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for delete_resource, args, kwargs in operations:
|
||||||
|
try:
|
||||||
|
delete_resource(*args, **kwargs)
|
||||||
|
except ApiException as exc:
|
||||||
|
if exc.status != 404:
|
||||||
|
raise self._api_error(exc, "Could not delete userbot instance") from exc
|
||||||
|
|
||||||
|
def _find_deployment(self, instance_id: str) -> tuple[str, Any]:
|
||||||
|
selector = f"{MANAGED_LABEL},{INSTANCE_LABEL}={instance_id}"
|
||||||
|
for namespace in (self.settings.namespace, *self.settings.legacy_namespaces):
|
||||||
|
try:
|
||||||
|
items = self.apps.list_namespaced_deployment(
|
||||||
|
namespace,
|
||||||
|
label_selector=selector,
|
||||||
|
).items
|
||||||
|
except ApiException as exc:
|
||||||
|
raise self._api_error(exc, "Could not find userbot instance") from exc
|
||||||
|
if items:
|
||||||
|
return namespace, items[0]
|
||||||
|
raise PanelError(404, f"Instance {instance_id} does not exist")
|
||||||
|
|
||||||
|
def _summarize(self, namespace: str, deployment: Any) -> InstanceSummary:
|
||||||
|
labels = deployment.metadata.labels or {}
|
||||||
|
annotations = deployment.metadata.annotations or {}
|
||||||
|
instance_id = labels.get(INSTANCE_LABEL, deployment.metadata.name)
|
||||||
|
legacy = annotations.get(LEGACY_ANNOTATION) == "true"
|
||||||
|
pods = self._pods_for_deployment(namespace, deployment)
|
||||||
|
pod = pods[0] if pods else None
|
||||||
|
desired = deployment.spec.replicas or 0
|
||||||
|
ready = False
|
||||||
|
reason: str | None = None
|
||||||
|
restarts = 0
|
||||||
|
updated_at = deployment.metadata.creation_timestamp
|
||||||
|
|
||||||
|
if pod is not None:
|
||||||
|
statuses = pod.status.container_statuses or []
|
||||||
|
ready = bool(statuses) and all(item.ready for item in statuses)
|
||||||
|
restarts = sum(item.restart_count for item in statuses)
|
||||||
|
for item in statuses:
|
||||||
|
state = item.state
|
||||||
|
if state and state.waiting and state.waiting.reason:
|
||||||
|
reason = state.waiting.reason
|
||||||
|
break
|
||||||
|
if state and state.terminated and state.terminated.reason:
|
||||||
|
reason = state.terminated.reason
|
||||||
|
break
|
||||||
|
updated_at = pod.status.start_time or pod.metadata.creation_timestamp
|
||||||
|
|
||||||
|
if desired == 0:
|
||||||
|
status = "stopped"
|
||||||
|
elif reason in {
|
||||||
|
"CrashLoopBackOff",
|
||||||
|
"Error",
|
||||||
|
"ImagePullBackOff",
|
||||||
|
"ErrImagePull",
|
||||||
|
"CreateContainerConfigError",
|
||||||
|
"RunContainerError",
|
||||||
|
} or (pod is not None and pod.status.phase == "Failed"):
|
||||||
|
status = "error"
|
||||||
|
elif ready and (deployment.status.available_replicas or 0) > 0:
|
||||||
|
status = "running"
|
||||||
|
else:
|
||||||
|
status = "pending"
|
||||||
|
reason = reason or (pod.status.phase if pod is not None else "Scheduling")
|
||||||
|
|
||||||
|
container_spec = deployment.spec.template.spec.containers[0]
|
||||||
|
limits = (container_spec.resources.limits or {}) if container_spec.resources else {}
|
||||||
|
pvc_name = annotations.get(PVC_ANNOTATION) or self._deployment_pvc(deployment)
|
||||||
|
storage = self._pvc_storage(namespace, pvc_name)
|
||||||
|
cpu_usage, memory_usage = self._pod_metrics(namespace, pod.metadata.name if pod else None)
|
||||||
|
return InstanceSummary(
|
||||||
|
instance_id=instance_id,
|
||||||
|
display_name=annotations.get(DISPLAY_ANNOTATION, instance_id),
|
||||||
|
namespace=namespace,
|
||||||
|
deployment=deployment.metadata.name,
|
||||||
|
pod=pod.metadata.name if pod else None,
|
||||||
|
status=status,
|
||||||
|
reason=reason,
|
||||||
|
ready=ready,
|
||||||
|
restarts=restarts,
|
||||||
|
pvc=pvc_name,
|
||||||
|
storage=storage,
|
||||||
|
image=container_spec.image,
|
||||||
|
cpu_limit=limits.get("cpu"),
|
||||||
|
memory_limit=limits.get("memory"),
|
||||||
|
cpu_usage=cpu_usage,
|
||||||
|
memory_usage=memory_usage,
|
||||||
|
updated_at=_as_datetime(updated_at),
|
||||||
|
legacy=legacy,
|
||||||
|
deletable=not legacy and namespace == self.settings.namespace,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _pods_for_deployment(self, namespace: str, deployment: Any) -> list[Any]:
|
||||||
|
selector = _selector(deployment.spec.selector.match_labels)
|
||||||
|
try:
|
||||||
|
pods = self.core.list_namespaced_pod(
|
||||||
|
namespace,
|
||||||
|
label_selector=selector,
|
||||||
|
).items
|
||||||
|
except ApiException as exc:
|
||||||
|
raise self._api_error(exc, "Could not list userbot Pods") from exc
|
||||||
|
return sorted(
|
||||||
|
pods,
|
||||||
|
key=lambda pod: pod.metadata.creation_timestamp or datetime.min.replace(tzinfo=UTC),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _pod_metrics(self, namespace: str, pod_name: str | None) -> tuple[str | None, str | None]:
|
||||||
|
if not pod_name:
|
||||||
|
return None, None
|
||||||
|
try:
|
||||||
|
metrics = self.custom.get_namespaced_custom_object(
|
||||||
|
"metrics.k8s.io",
|
||||||
|
"v1beta1",
|
||||||
|
namespace,
|
||||||
|
"pods",
|
||||||
|
pod_name,
|
||||||
|
)
|
||||||
|
except (ApiException, AttributeError):
|
||||||
|
return None, None
|
||||||
|
containers = metrics.get("containers", [])
|
||||||
|
cpu = containers[0].get("usage", {}).get("cpu") if containers else None
|
||||||
|
memory = containers[0].get("usage", {}).get("memory") if containers else None
|
||||||
|
return cpu, memory
|
||||||
|
|
||||||
|
def _pvc_storage(self, namespace: str, pvc_name: str | None) -> str | None:
|
||||||
|
if not pvc_name:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
pvc = self.core.read_namespaced_persistent_volume_claim(pvc_name, namespace)
|
||||||
|
except ApiException:
|
||||||
|
return None
|
||||||
|
requests = pvc.spec.resources.requests or {}
|
||||||
|
return requests.get("storage")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deployment_pvc(deployment: Any) -> str | None:
|
||||||
|
for volume in deployment.spec.template.spec.volumes or []:
|
||||||
|
if volume.persistent_volume_claim:
|
||||||
|
return volume.persistent_volume_claim.claim_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resource_names(instance_id: str) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"deployment": f"userbot-{instance_id}",
|
||||||
|
"secret": f"userbot-{instance_id}-credentials",
|
||||||
|
"pvc": f"userbot-{instance_id}-data",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _metadata(
|
||||||
|
self,
|
||||||
|
account: AccountBase,
|
||||||
|
names: dict[str, str],
|
||||||
|
resource_name: str,
|
||||||
|
) -> client.V1ObjectMeta:
|
||||||
|
return client.V1ObjectMeta(
|
||||||
|
name=resource_name,
|
||||||
|
namespace=self.settings.namespace,
|
||||||
|
labels={
|
||||||
|
"app.kubernetes.io/name": "userbot",
|
||||||
|
INSTANCE_LABEL: account.instance_id,
|
||||||
|
MANAGED_BY_LABEL: "userbot-panel",
|
||||||
|
},
|
||||||
|
annotations={
|
||||||
|
DISPLAY_ANNOTATION: account.display_name,
|
||||||
|
CREDENTIALS_ANNOTATION: names["secret"],
|
||||||
|
PVC_ANNOTATION: names["pvc"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _secret(
|
||||||
|
self,
|
||||||
|
account: AccountBase,
|
||||||
|
session_string: str,
|
||||||
|
names: dict[str, str],
|
||||||
|
) -> client.V1Secret:
|
||||||
|
return client.V1Secret(
|
||||||
|
metadata=self._metadata(account, names, names["secret"]),
|
||||||
|
type="Opaque",
|
||||||
|
string_data={
|
||||||
|
"API_ID": str(account.api_id),
|
||||||
|
"API_HASH": account.api_hash,
|
||||||
|
"STRINGSESSION": session_string,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _pvc(
|
||||||
|
self,
|
||||||
|
account: AccountBase,
|
||||||
|
names: dict[str, str],
|
||||||
|
) -> client.V1PersistentVolumeClaim:
|
||||||
|
return client.V1PersistentVolumeClaim(
|
||||||
|
metadata=self._metadata(account, names, names["pvc"]),
|
||||||
|
spec=client.V1PersistentVolumeClaimSpec(
|
||||||
|
access_modes=["ReadWriteOnce"],
|
||||||
|
storage_class_name=self.settings.storage_class,
|
||||||
|
resources=client.V1VolumeResourceRequirements(
|
||||||
|
requests={"storage": account.resources.storage},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _deployment(
|
||||||
|
self,
|
||||||
|
account: AccountBase,
|
||||||
|
names: dict[str, str],
|
||||||
|
) -> client.V1Deployment:
|
||||||
|
pod_labels = {
|
||||||
|
"app.kubernetes.io/name": "userbot",
|
||||||
|
INSTANCE_LABEL: account.instance_id,
|
||||||
|
MANAGED_BY_LABEL: "userbot-panel",
|
||||||
|
}
|
||||||
|
container = client.V1Container(
|
||||||
|
name="userbot",
|
||||||
|
image=self.settings.image,
|
||||||
|
image_pull_policy="Always",
|
||||||
|
env_from=[
|
||||||
|
client.V1EnvFromSource(
|
||||||
|
secret_ref=client.V1SecretEnvSource(name=self.settings.common_secret)
|
||||||
|
),
|
||||||
|
client.V1EnvFromSource(
|
||||||
|
config_map_ref=client.V1ConfigMapEnvSource(name=self.settings.common_config)
|
||||||
|
),
|
||||||
|
client.V1EnvFromSource(
|
||||||
|
secret_ref=client.V1SecretEnvSource(name=names["secret"])
|
||||||
|
),
|
||||||
|
],
|
||||||
|
resources=client.V1ResourceRequirements(
|
||||||
|
requests={"cpu": "80m", "memory": "512Mi"},
|
||||||
|
limits={
|
||||||
|
"cpu": account.resources.cpu_limit,
|
||||||
|
"memory": account.resources.memory_limit,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
volume_mounts=[
|
||||||
|
client.V1VolumeMount(name="data", mount_path="/app/data"),
|
||||||
|
client.V1VolumeMount(name="downloads", mount_path="/app/downloads"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
pod_spec = client.V1PodSpec(
|
||||||
|
service_account_name="userbot-runtime",
|
||||||
|
automount_service_account_token=False,
|
||||||
|
containers=[container],
|
||||||
|
termination_grace_period_seconds=30,
|
||||||
|
volumes=[
|
||||||
|
client.V1Volume(
|
||||||
|
name="data",
|
||||||
|
persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource(
|
||||||
|
claim_name=names["pvc"]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
client.V1Volume(
|
||||||
|
name="downloads",
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path=self.settings.downloads_host_path,
|
||||||
|
type="DirectoryOrCreate",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return client.V1Deployment(
|
||||||
|
metadata=self._metadata(account, names, names["deployment"]),
|
||||||
|
spec=client.V1DeploymentSpec(
|
||||||
|
replicas=1,
|
||||||
|
strategy=client.V1DeploymentStrategy(type="Recreate"),
|
||||||
|
selector=client.V1LabelSelector(match_labels=pod_labels),
|
||||||
|
template=client.V1PodTemplateSpec(
|
||||||
|
metadata=client.V1ObjectMeta(labels=pod_labels),
|
||||||
|
spec=pod_spec,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rollback(self, created: list[tuple[str, str]]) -> None:
|
||||||
|
for kind, name in reversed(created):
|
||||||
|
try:
|
||||||
|
if kind == "deployment":
|
||||||
|
self.apps.delete_namespaced_deployment(name, self.settings.namespace)
|
||||||
|
elif kind == "pvc":
|
||||||
|
self.core.delete_namespaced_persistent_volume_claim(
|
||||||
|
name,
|
||||||
|
self.settings.namespace,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.core.delete_namespaced_secret(name, self.settings.namespace)
|
||||||
|
except ApiException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _api_error(exc: ApiException, detail: str) -> PanelError:
|
||||||
|
if exc.status == 403:
|
||||||
|
return PanelError(503, f"{detail}: Kubernetes RBAC denied the operation")
|
||||||
|
if exc.status == 409:
|
||||||
|
return PanelError(409, f"{detail}: resource conflict")
|
||||||
|
if exc.status == 404:
|
||||||
|
return PanelError(404, f"{detail}: resource not found")
|
||||||
|
return PanelError(503, detail)
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Query, Request
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from .auth_service import TelegramAuthService
|
||||||
|
from .config import settings
|
||||||
|
from .errors import PanelError
|
||||||
|
from .kubernetes_service import KubernetesService
|
||||||
|
from .models import (
|
||||||
|
AuthResult,
|
||||||
|
CodeSubmit,
|
||||||
|
DeleteRequest,
|
||||||
|
InstanceSummary,
|
||||||
|
PasswordSubmit,
|
||||||
|
PhoneStart,
|
||||||
|
StringSessionStart,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
app.state.kubernetes = KubernetesService(settings)
|
||||||
|
app.state.telegram = TelegramAuthService(settings.auth_ttl_seconds)
|
||||||
|
yield
|
||||||
|
await app.state.telegram.close()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Userbot Kubernetes Control",
|
||||||
|
version="1.0.0",
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(PanelError)
|
||||||
|
async def panel_error_handler(_request: Request, exc: PanelError) -> JSONResponse:
|
||||||
|
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_error_handler(
|
||||||
|
_request: Request,
|
||||||
|
exc: RequestValidationError,
|
||||||
|
) -> JSONResponse:
|
||||||
|
errors = [
|
||||||
|
{
|
||||||
|
"loc": error.get("loc", ()),
|
||||||
|
"msg": error.get("msg", "Invalid value"),
|
||||||
|
"type": error.get("type", "value_error"),
|
||||||
|
}
|
||||||
|
for error in exc.errors()
|
||||||
|
]
|
||||||
|
return JSONResponse(status_code=422, content={"detail": errors})
|
||||||
|
|
||||||
|
|
||||||
|
def kube(request: Request) -> KubernetesService:
|
||||||
|
return request.app.state.kubernetes
|
||||||
|
|
||||||
|
|
||||||
|
def telegram(request: Request) -> TelegramAuthService:
|
||||||
|
return request.app.state.telegram
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health(request: Request) -> dict[str, object]:
|
||||||
|
service = kube(request)
|
||||||
|
try:
|
||||||
|
service.apps.list_namespaced_deployment(
|
||||||
|
settings.namespace,
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return {"ok": False, "kubernetes": False}
|
||||||
|
return {"ok": True, "kubernetes": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/instances", response_model=list[InstanceSummary])
|
||||||
|
def list_instances(
|
||||||
|
request: Request,
|
||||||
|
query: str = "",
|
||||||
|
status: str = "",
|
||||||
|
) -> list[InstanceSummary]:
|
||||||
|
return kube(request).list_instances(query=query, status=status)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/instances/{instance_id}", response_model=InstanceSummary)
|
||||||
|
def get_instance(instance_id: str, request: Request) -> InstanceSummary:
|
||||||
|
return kube(request).get_instance(instance_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/instances/{instance_id}/logs")
|
||||||
|
def get_logs(
|
||||||
|
instance_id: str,
|
||||||
|
request: Request,
|
||||||
|
tail: Annotated[int, Query(ge=1, le=1000)] = 250,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
return {"logs": kube(request).logs(instance_id, tail)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/instances/{instance_id}/start", response_model=InstanceSummary)
|
||||||
|
def start_instance(instance_id: str, request: Request) -> InstanceSummary:
|
||||||
|
return kube(request).scale(instance_id, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/instances/{instance_id}/stop", response_model=InstanceSummary)
|
||||||
|
def stop_instance(instance_id: str, request: Request) -> InstanceSummary:
|
||||||
|
return kube(request).scale(instance_id, 0)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/instances/{instance_id}/restart", response_model=InstanceSummary)
|
||||||
|
def restart_instance(instance_id: str, request: Request) -> InstanceSummary:
|
||||||
|
return kube(request).restart(instance_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/instances/{instance_id}/delete", status_code=204)
|
||||||
|
def delete_instance(
|
||||||
|
instance_id: str,
|
||||||
|
payload: DeleteRequest,
|
||||||
|
request: Request,
|
||||||
|
) -> Response:
|
||||||
|
if payload.confirmation != instance_id:
|
||||||
|
raise PanelError(422, "Type the instance id exactly to confirm deletion")
|
||||||
|
kube(request).delete(instance_id, delete_data=payload.delete_data)
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/phone/start", response_model=AuthResult)
|
||||||
|
async def auth_phone_start(
|
||||||
|
payload: PhoneStart,
|
||||||
|
request: Request,
|
||||||
|
) -> AuthResult:
|
||||||
|
service = kube(request)
|
||||||
|
service.ensure_prerequisites()
|
||||||
|
service.assert_available(payload.instance_id)
|
||||||
|
flow_id = await telegram(request).start_phone(payload)
|
||||||
|
return AuthResult(status="code_required", flow_id=flow_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/phone/{flow_id}/code", response_model=AuthResult)
|
||||||
|
async def auth_phone_code(
|
||||||
|
flow_id: str,
|
||||||
|
payload: CodeSubmit,
|
||||||
|
request: Request,
|
||||||
|
) -> AuthResult:
|
||||||
|
authorized = await telegram(request).submit_code(flow_id, payload.code)
|
||||||
|
if authorized is None:
|
||||||
|
return AuthResult(status="password_required", flow_id=flow_id)
|
||||||
|
instance = kube(request).provision(authorized.account, authorized.session_string)
|
||||||
|
return AuthResult(status="ready", instance=instance)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/phone/{flow_id}/password", response_model=AuthResult)
|
||||||
|
async def auth_phone_password(
|
||||||
|
flow_id: str,
|
||||||
|
payload: PasswordSubmit,
|
||||||
|
request: Request,
|
||||||
|
) -> AuthResult:
|
||||||
|
authorized = await telegram(request).submit_password(flow_id, payload.password)
|
||||||
|
instance = kube(request).provision(authorized.account, authorized.session_string)
|
||||||
|
return AuthResult(status="ready", instance=instance)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/string-session", response_model=AuthResult)
|
||||||
|
async def auth_string_session(
|
||||||
|
payload: StringSessionStart,
|
||||||
|
request: Request,
|
||||||
|
) -> AuthResult:
|
||||||
|
service = kube(request)
|
||||||
|
service.ensure_prerequisites()
|
||||||
|
service.assert_available(payload.instance_id)
|
||||||
|
authorized = await telegram(request).validate_string_session(payload)
|
||||||
|
instance = service.provision(authorized.account, authorized.session_string)
|
||||||
|
return AuthResult(status="ready", instance=instance)
|
||||||
|
|
||||||
|
|
||||||
|
static_dir = settings.static_dir
|
||||||
|
assets_dir = static_dir / "assets"
|
||||||
|
if assets_dir.exists():
|
||||||
|
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", include_in_schema=False)
|
||||||
|
def index() -> FileResponse:
|
||||||
|
return FileResponse(static_dir / "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/{path:path}", include_in_schema=False)
|
||||||
|
def spa_fallback(path: str) -> FileResponse:
|
||||||
|
candidate = (static_dir / path).resolve()
|
||||||
|
if candidate.is_file() and static_dir.resolve() in candidate.parents:
|
||||||
|
return FileResponse(candidate)
|
||||||
|
return FileResponse(static_dir / "index.html")
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
INSTANCE_PATTERN = r"^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$"
|
||||||
|
|
||||||
|
|
||||||
|
class Resources(BaseModel):
|
||||||
|
storage: str = "1Gi"
|
||||||
|
cpu_limit: str = "300m"
|
||||||
|
memory_limit: str = "1536Mi"
|
||||||
|
|
||||||
|
|
||||||
|
class AccountBase(BaseModel):
|
||||||
|
instance_id: str = Field(pattern=INSTANCE_PATTERN, max_length=40)
|
||||||
|
display_name: str = Field(min_length=1, max_length=80)
|
||||||
|
api_id: int = Field(gt=0)
|
||||||
|
api_hash: str = Field(min_length=16, max_length=128)
|
||||||
|
resources: Resources = Field(default_factory=Resources)
|
||||||
|
|
||||||
|
@field_validator("display_name", "api_hash")
|
||||||
|
@classmethod
|
||||||
|
def strip_text(cls, value: str) -> str:
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class PhoneStart(AccountBase):
|
||||||
|
phone: str = Field(min_length=7, max_length=32)
|
||||||
|
|
||||||
|
@field_validator("phone")
|
||||||
|
@classmethod
|
||||||
|
def normalize_phone(cls, value: str) -> str:
|
||||||
|
value = value.strip()
|
||||||
|
if not value.startswith("+"):
|
||||||
|
raise ValueError("phone must use international format")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class StringSessionStart(AccountBase):
|
||||||
|
session_string: str = Field(min_length=32)
|
||||||
|
|
||||||
|
|
||||||
|
class CodeSubmit(BaseModel):
|
||||||
|
code: str = Field(min_length=3, max_length=12)
|
||||||
|
|
||||||
|
@field_validator("code")
|
||||||
|
@classmethod
|
||||||
|
def normalize_code(cls, value: str) -> str:
|
||||||
|
return "".join(value.split())
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordSubmit(BaseModel):
|
||||||
|
password: str = Field(min_length=1, max_length=256)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRequest(BaseModel):
|
||||||
|
confirmation: str
|
||||||
|
delete_data: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceSummary(BaseModel):
|
||||||
|
instance_id: str
|
||||||
|
display_name: str
|
||||||
|
namespace: str
|
||||||
|
deployment: str
|
||||||
|
pod: str | None = None
|
||||||
|
status: Literal["running", "stopped", "pending", "error"]
|
||||||
|
reason: str | None = None
|
||||||
|
ready: bool = False
|
||||||
|
restarts: int = 0
|
||||||
|
pvc: str | None = None
|
||||||
|
storage: str | None = None
|
||||||
|
image: str
|
||||||
|
cpu_limit: str | None = None
|
||||||
|
memory_limit: str | None = None
|
||||||
|
cpu_usage: str | None = None
|
||||||
|
memory_usage: str | None = None
|
||||||
|
updated_at: datetime | None = None
|
||||||
|
legacy: bool = False
|
||||||
|
deletable: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class AuthResult(BaseModel):
|
||||||
|
status: Literal["code_required", "password_required", "ready"]
|
||||||
|
flow_id: str | None = None
|
||||||
|
instance: InstanceSummary | None = None
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
httpx==0.28.1
|
||||||
|
pytest==8.4.1
|
||||||
|
pytest-asyncio==1.1.0
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.115.12
|
||||||
|
kubernetes==33.1.0
|
||||||
|
pyrofork==2.3.68
|
||||||
|
tgcrypto==1.2.5
|
||||||
|
uvicorn[standard]==0.34.2
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from app.auth_service import TelegramAuthService
|
||||||
|
from app.models import PhoneStart, StringSessionStart
|
||||||
|
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
instances = []
|
||||||
|
requires_password = False
|
||||||
|
|
||||||
|
def __init__(self, *_args, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.is_connected = False
|
||||||
|
self.disconnected = False
|
||||||
|
self.__class__.instances.append(self)
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
self.is_connected = True
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
self.is_connected = False
|
||||||
|
self.disconnected = True
|
||||||
|
|
||||||
|
async def send_code(self, _phone):
|
||||||
|
return SimpleNamespace(phone_code_hash="hash")
|
||||||
|
|
||||||
|
async def sign_in(self, *_args):
|
||||||
|
if self.requires_password:
|
||||||
|
from pyrogram.errors import SessionPasswordNeeded
|
||||||
|
|
||||||
|
raise SessionPasswordNeeded()
|
||||||
|
|
||||||
|
async def check_password(self, _password):
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_me(self):
|
||||||
|
return SimpleNamespace(id=1)
|
||||||
|
|
||||||
|
async def export_session_string(self):
|
||||||
|
return "exported-session"
|
||||||
|
|
||||||
|
|
||||||
|
def phone_payload() -> PhoneStart:
|
||||||
|
return PhoneStart(
|
||||||
|
instance_id="test",
|
||||||
|
display_name="Test",
|
||||||
|
api_id=123,
|
||||||
|
api_hash="0123456789abcdef",
|
||||||
|
phone="+421900000000",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_phone_code_success_closes_and_forgets_client(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr("app.auth_service.Client", FakeClient)
|
||||||
|
auth = TelegramAuthService()
|
||||||
|
|
||||||
|
flow_id = await auth.start_phone(phone_payload())
|
||||||
|
result = await auth.submit_code(flow_id, "12345")
|
||||||
|
|
||||||
|
assert result.session_string == "exported-session"
|
||||||
|
assert flow_id not in auth.flows
|
||||||
|
assert FakeClient.instances[-1].disconnected is True
|
||||||
|
assert FakeClient.instances[-1].kwargs["in_memory"] is True
|
||||||
|
assert FakeClient.instances[-1].kwargs["no_updates"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_string_session_is_validated_and_closed(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr("app.auth_service.Client", FakeClient)
|
||||||
|
auth = TelegramAuthService()
|
||||||
|
payload = StringSessionStart(
|
||||||
|
instance_id="test",
|
||||||
|
display_name="Test",
|
||||||
|
api_id=123,
|
||||||
|
api_hash="0123456789abcdef",
|
||||||
|
session_string="x" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await auth.validate_string_session(payload)
|
||||||
|
|
||||||
|
assert result.session_string == "exported-session"
|
||||||
|
assert FakeClient.instances[-1].disconnected is True
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from app.config import Settings
|
||||||
|
from app.errors import PanelError
|
||||||
|
from app.kubernetes_service import KubernetesService
|
||||||
|
from app.models import AccountBase
|
||||||
|
from kubernetes.client.exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
def service() -> KubernetesService:
|
||||||
|
core = Mock()
|
||||||
|
apps = Mock()
|
||||||
|
custom = Mock()
|
||||||
|
custom.get_namespaced_custom_object.side_effect = ApiException(status=404)
|
||||||
|
return KubernetesService(Settings(), core=core, apps=apps, custom=custom)
|
||||||
|
|
||||||
|
|
||||||
|
def account() -> AccountBase:
|
||||||
|
return AccountBase(
|
||||||
|
instance_id="test-account",
|
||||||
|
display_name="Test Account",
|
||||||
|
api_id=12345,
|
||||||
|
api_hash="0123456789abcdef0123456789abcdef",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_renders_managed_resources_without_leaking_credentials() -> None:
|
||||||
|
kube = service()
|
||||||
|
names = kube._resource_names("test-account")
|
||||||
|
secret = kube._secret(account(), "SESSION", names)
|
||||||
|
pvc = kube._pvc(account(), names)
|
||||||
|
deployment = kube._deployment(account(), names)
|
||||||
|
|
||||||
|
assert secret.string_data == {
|
||||||
|
"API_ID": "12345",
|
||||||
|
"API_HASH": "0123456789abcdef0123456789abcdef",
|
||||||
|
"STRINGSESSION": "SESSION",
|
||||||
|
}
|
||||||
|
assert pvc.spec.storage_class_name == "local-path-retain"
|
||||||
|
assert pvc.spec.resources.requests["storage"] == "1Gi"
|
||||||
|
assert deployment.spec.strategy.type == "Recreate"
|
||||||
|
assert deployment.spec.replicas == 1
|
||||||
|
assert deployment.spec.template.spec.automount_service_account_token is False
|
||||||
|
assert deployment.spec.template.spec.service_account_name == "userbot-runtime"
|
||||||
|
assert deployment.spec.template.spec.volumes[1].host_path.path.endswith("/Downloads")
|
||||||
|
assert deployment.spec.template.spec.containers[0].resources.requests == {
|
||||||
|
"cpu": "80m",
|
||||||
|
"memory": "512Mi",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collision_is_reported_before_auth() -> None:
|
||||||
|
kube = service()
|
||||||
|
kube.apps.read_namespaced_deployment.return_value = object()
|
||||||
|
|
||||||
|
with pytest.raises(PanelError) as error:
|
||||||
|
kube.assert_available("test-account")
|
||||||
|
|
||||||
|
assert error.value.status_code == 409
|
||||||
|
assert "Deployment" in error.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
def test_partial_provision_rolls_back_only_created_resources() -> None:
|
||||||
|
kube = service()
|
||||||
|
kube.ensure_prerequisites = Mock()
|
||||||
|
kube.assert_available = Mock()
|
||||||
|
kube.core.create_namespaced_persistent_volume_claim.side_effect = ApiException(status=500)
|
||||||
|
|
||||||
|
with pytest.raises(PanelError):
|
||||||
|
kube.provision(account(), "SESSION")
|
||||||
|
|
||||||
|
kube.core.delete_namespaced_secret.assert_called_once_with(
|
||||||
|
"userbot-test-account-credentials",
|
||||||
|
"userbot",
|
||||||
|
)
|
||||||
|
kube.core.delete_namespaced_persistent_volume_claim.assert_not_called()
|
||||||
|
kube.apps.delete_namespaced_deployment.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_retains_pvc_unless_explicitly_requested() -> None:
|
||||||
|
kube = service()
|
||||||
|
deployment = SimpleNamespace(
|
||||||
|
metadata=SimpleNamespace(
|
||||||
|
name="userbot-test-account",
|
||||||
|
annotations={
|
||||||
|
"userbot.forust.xyz/credentials-secret": "credentials",
|
||||||
|
"userbot.forust.xyz/pvc": "data",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
kube._find_deployment = Mock(return_value=("userbot", deployment))
|
||||||
|
|
||||||
|
kube.delete("test-account", delete_data=False)
|
||||||
|
|
||||||
|
kube.apps.delete_namespaced_deployment.assert_called_once()
|
||||||
|
kube.core.delete_namespaced_secret.assert_called_once_with("credentials", "userbot")
|
||||||
|
kube.core.delete_namespaced_persistent_volume_claim.assert_not_called()
|
||||||
|
|
||||||
|
kube.delete("test-account", delete_data=True)
|
||||||
|
kube.core.delete_namespaced_persistent_volume_claim.assert_called_once_with(
|
||||||
|
"data",
|
||||||
|
"userbot",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_delete_is_blocked() -> None:
|
||||||
|
kube = service()
|
||||||
|
deployment = SimpleNamespace(
|
||||||
|
metadata=SimpleNamespace(
|
||||||
|
name="forust-userbot-deployment",
|
||||||
|
annotations={"userbot.forust.xyz/legacy": "true"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
kube._find_deployment = Mock(return_value=("default", deployment))
|
||||||
|
|
||||||
|
with pytest.raises(PanelError) as error:
|
||||||
|
kube.delete("forust", delete_data=False)
|
||||||
|
|
||||||
|
assert error.value.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def pod(*, ready: bool, phase: str = "Running", reason: str | None = None):
|
||||||
|
waiting = SimpleNamespace(reason=reason) if reason else None
|
||||||
|
state = SimpleNamespace(waiting=waiting, terminated=None)
|
||||||
|
status = SimpleNamespace(
|
||||||
|
container_statuses=[
|
||||||
|
SimpleNamespace(ready=ready, restart_count=2, state=state),
|
||||||
|
],
|
||||||
|
phase=phase,
|
||||||
|
start_time=datetime.now(UTC),
|
||||||
|
)
|
||||||
|
return SimpleNamespace(
|
||||||
|
metadata=SimpleNamespace(name="pod-1", creation_timestamp=datetime.now(UTC)),
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deployment(replicas: int = 1):
|
||||||
|
resources = SimpleNamespace(limits={"cpu": "300m", "memory": "1536Mi"})
|
||||||
|
container = SimpleNamespace(image="userbot:latest", resources=resources)
|
||||||
|
template_spec = SimpleNamespace(containers=[container], volumes=[])
|
||||||
|
return SimpleNamespace(
|
||||||
|
metadata=SimpleNamespace(
|
||||||
|
name="userbot-test-account",
|
||||||
|
labels={"app.kubernetes.io/instance": "test-account"},
|
||||||
|
annotations={},
|
||||||
|
creation_timestamp=datetime.now(UTC),
|
||||||
|
),
|
||||||
|
spec=SimpleNamespace(
|
||||||
|
replicas=replicas,
|
||||||
|
selector=SimpleNamespace(match_labels={"app": "test"}),
|
||||||
|
template=SimpleNamespace(spec=template_spec),
|
||||||
|
),
|
||||||
|
status=SimpleNamespace(available_replicas=1 if replicas else 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("replicas", "pod_value", "expected"),
|
||||||
|
[
|
||||||
|
(0, None, "stopped"),
|
||||||
|
(1, pod(ready=True), "running"),
|
||||||
|
(1, pod(ready=False, phase="Pending"), "pending"),
|
||||||
|
(1, pod(ready=False, reason="CrashLoopBackOff"), "error"),
|
||||||
|
(1, pod(ready=False, phase="Failed"), "error"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_status_classification(replicas, pod_value, expected) -> None:
|
||||||
|
kube = service()
|
||||||
|
kube._pods_for_deployment = Mock(return_value=[pod_value] if pod_value else [])
|
||||||
|
kube._pvc_storage = Mock(return_value=None)
|
||||||
|
kube._pod_metrics = Mock(return_value=(None, None))
|
||||||
|
|
||||||
|
result = kube._summarize("userbot", deployment(replicas))
|
||||||
|
|
||||||
|
assert result.status == expected
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import pytest
|
||||||
|
from app.main import validation_error_handler
|
||||||
|
from app.models import AccountBase, DeleteRequest
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"instance_id",
|
||||||
|
["Upper", "has_space", "-leading", "trailing-", "x" * 41],
|
||||||
|
)
|
||||||
|
def test_invalid_instance_ids(instance_id: str) -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
AccountBase(
|
||||||
|
instance_id=instance_id,
|
||||||
|
display_name="Test",
|
||||||
|
api_id=1,
|
||||||
|
api_hash="0123456789abcdef",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_data_defaults_to_false() -> None:
|
||||||
|
request = DeleteRequest(confirmation="test")
|
||||||
|
assert request.delete_data is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validation_response_does_not_echo_secret_input() -> None:
|
||||||
|
sensitive_value = "-".join(("very", "private", "string", "session"))
|
||||||
|
error = RequestValidationError(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "string_too_short",
|
||||||
|
"loc": ("body", "session_string"),
|
||||||
|
"msg": "String should have at least 32 characters",
|
||||||
|
"input": sensitive_value,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await validation_error_handler(None, error)
|
||||||
|
|
||||||
|
assert sensitive_value.encode() not in response.body
|
||||||
|
assert b'"input"' not in response.body
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#080808" />
|
||||||
|
<title>Userbot · Kubernetes Control</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3970
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "userbot-panel",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-svelte": "^0.468.0",
|
||||||
|
"svelte": "^5.16.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/svelte": "^5.2.6",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"svelte-check": "^4.7.3",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"vite": "^6.0.7",
|
||||||
|
"vitest": "^2.1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
|
Search,
|
||||||
|
Server,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import AddAccountDrawer from './components/AddAccountDrawer.svelte';
|
||||||
|
import DetailsDrawer from './components/DetailsDrawer.svelte';
|
||||||
|
import InstanceTable from './components/InstanceTable.svelte';
|
||||||
|
import Sidebar from './components/Sidebar.svelte';
|
||||||
|
import { api } from './lib/api';
|
||||||
|
import { filterInstances } from './lib/instances';
|
||||||
|
import type { Instance } from './lib/types';
|
||||||
|
|
||||||
|
let instances: Instance[] = [];
|
||||||
|
let selected: Instance | null = null;
|
||||||
|
let query = '';
|
||||||
|
let status = '';
|
||||||
|
let loading = true;
|
||||||
|
let refreshing = false;
|
||||||
|
let error = '';
|
||||||
|
let addOpen = false;
|
||||||
|
let menuOpen = false;
|
||||||
|
let busyId = '';
|
||||||
|
|
||||||
|
$: visibleInstances = filterInstances(instances, query, status);
|
||||||
|
$: running = instances.filter((item) => item.status === 'running').length;
|
||||||
|
$: stopped = instances.filter((item) => item.status === 'stopped').length;
|
||||||
|
$: restarts = instances.reduce((total, item) => total + item.restarts, 0);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
refresh();
|
||||||
|
const timer = window.setInterval(() => refresh(true), 5000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refresh(silent = false) {
|
||||||
|
if (!silent) refreshing = true;
|
||||||
|
try {
|
||||||
|
const next = await api.instances();
|
||||||
|
instances = next;
|
||||||
|
if (selected) {
|
||||||
|
selected =
|
||||||
|
next.find(
|
||||||
|
(item) =>
|
||||||
|
item.instance_id === selected?.instance_id && item.namespace === selected?.namespace,
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
error = '';
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Не удалось загрузить инстансы';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
refreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAction(
|
||||||
|
instance: Instance,
|
||||||
|
action: 'start' | 'stop' | 'restart' | 'logs',
|
||||||
|
) {
|
||||||
|
if (action === 'logs') {
|
||||||
|
selected = instance;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
busyId = instance.instance_id;
|
||||||
|
try {
|
||||||
|
const updated = await api.action(instance.instance_id, action);
|
||||||
|
instances = instances.map((item) =>
|
||||||
|
item.instance_id === updated.instance_id && item.namespace === updated.namespace
|
||||||
|
? updated
|
||||||
|
: item,
|
||||||
|
);
|
||||||
|
if (selected?.instance_id === updated.instance_id) selected = updated;
|
||||||
|
window.setTimeout(() => refresh(true), 900);
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : `Не удалось выполнить ${action}`;
|
||||||
|
} finally {
|
||||||
|
busyId = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteInstance(instance: Instance, deleteData: boolean) {
|
||||||
|
busyId = instance.instance_id;
|
||||||
|
try {
|
||||||
|
await api.delete(instance.instance_id, deleteData);
|
||||||
|
selected = null;
|
||||||
|
await refresh();
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Не удалось удалить инстанс';
|
||||||
|
} finally {
|
||||||
|
busyId = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function created(instance: Instance) {
|
||||||
|
addOpen = false;
|
||||||
|
selected = instance;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="app-shell">
|
||||||
|
<Sidebar
|
||||||
|
open={menuOpen}
|
||||||
|
on:add={() => {
|
||||||
|
addOpen = true;
|
||||||
|
menuOpen = false;
|
||||||
|
}}
|
||||||
|
on:close={() => (menuOpen = false)}
|
||||||
|
/>
|
||||||
|
{#if menuOpen}
|
||||||
|
<button class="mobile-overlay" aria-label="Закрыть меню" on:click={() => (menuOpen = false)}></button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section class="workspace">
|
||||||
|
<header class="topbar">
|
||||||
|
<button class="mobile-menu icon-button" aria-label="Открыть меню" on:click={() => (menuOpen = true)}>
|
||||||
|
<Menu size={18} />
|
||||||
|
</button>
|
||||||
|
<div class="live-state"><span></span>{running} инстанса работают</div>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<span class="namespace"><Server size={14} />userbot</span>
|
||||||
|
<button class="primary" on:click={() => (addOpen = true)}>
|
||||||
|
<Plus size={16} />
|
||||||
|
Добавить аккаунт
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="page-heading">
|
||||||
|
<div>
|
||||||
|
<h1>Инстансы</h1>
|
||||||
|
<p>Telegram-аккаунты и их состояние в Kubernetes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="summary-strip" aria-label="Сводка">
|
||||||
|
<div><span>Всего</span><strong>{instances.length}</strong></div>
|
||||||
|
<div><span>Работают</span><strong>{running}</strong></div>
|
||||||
|
<div><span>Остановлены</span><strong>{stopped}</strong></div>
|
||||||
|
<div><span>Перезапуски</span><strong>{restarts}</strong></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<label class="search-field">
|
||||||
|
<Search size={16} />
|
||||||
|
<input bind:value={query} placeholder="Поиск" aria-label="Поиск инстансов" />
|
||||||
|
</label>
|
||||||
|
<select bind:value={status} aria-label="Фильтр статуса">
|
||||||
|
<option value="">Все статусы</option>
|
||||||
|
<option value="running">Работают</option>
|
||||||
|
<option value="stopped">Остановлены</option>
|
||||||
|
<option value="pending">Запускаются</option>
|
||||||
|
<option value="error">Ошибки</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="icon-button refresh"
|
||||||
|
aria-label="Обновить"
|
||||||
|
disabled={refreshing}
|
||||||
|
on:click={() => refresh()}
|
||||||
|
>
|
||||||
|
<RefreshCw size={17} class={refreshing ? 'spin' : ''} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error-banner" role="alert">
|
||||||
|
<span>{error}</span>
|
||||||
|
<button on:click={() => refresh()}>Повторить</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="center-state"><RefreshCw class="spin" size={22} />Загружаем инстансы…</div>
|
||||||
|
{:else if visibleInstances.length}
|
||||||
|
<InstanceTable
|
||||||
|
instances={visibleInstances}
|
||||||
|
{busyId}
|
||||||
|
on:select={(event) => (selected = event.detail)}
|
||||||
|
on:action={(event) => runAction(event.detail.instance, event.detail.action)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="empty-state">
|
||||||
|
<Server size={25} />
|
||||||
|
<h2>{instances.length ? 'Ничего не найдено' : 'Инстансов пока нет'}</h2>
|
||||||
|
<p>{instances.length ? 'Измените поиск или фильтр.' : 'Добавьте первый Telegram-аккаунт.'}</p>
|
||||||
|
{#if !instances.length}
|
||||||
|
<button class="primary" on:click={() => (addOpen = true)}>
|
||||||
|
<Plus size={16} />Добавить аккаунт
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<AddAccountDrawer open={addOpen} on:close={() => (addOpen = false)} on:created={(event) => created(event.detail)} />
|
||||||
|
<DetailsDrawer
|
||||||
|
instance={selected}
|
||||||
|
busy={busyId === selected?.instance_id}
|
||||||
|
on:close={() => (selected = null)}
|
||||||
|
on:action={(event) => runAction(event.detail.instance, event.detail.action)}
|
||||||
|
on:delete={(event) => deleteInstance(event.detail.instance, event.detail.deleteData)}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ArrowLeft, Check, Eye, EyeOff, LoaderCircle, X } from 'lucide-svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { api } from '../lib/api';
|
||||||
|
import type { AccountDraft, Instance } from '../lib/types';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
|
||||||
|
type Method = 'phone' | 'session';
|
||||||
|
type AuthPhase = 'code' | 'password';
|
||||||
|
|
||||||
|
const emptyDraft = (): AccountDraft => ({
|
||||||
|
instance_id: '',
|
||||||
|
display_name: '',
|
||||||
|
api_id: '',
|
||||||
|
api_hash: '',
|
||||||
|
phone: '',
|
||||||
|
session_string: '',
|
||||||
|
resources: {
|
||||||
|
storage: '1Gi',
|
||||||
|
cpu_limit: '300m',
|
||||||
|
memory_limit: '1536Mi',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let draft = emptyDraft();
|
||||||
|
let step = 1;
|
||||||
|
let method: Method = 'phone';
|
||||||
|
let authPhase: AuthPhase = 'code';
|
||||||
|
let flowId = '';
|
||||||
|
let code = '';
|
||||||
|
let password = '';
|
||||||
|
let error = '';
|
||||||
|
let busy = false;
|
||||||
|
let showHash = false;
|
||||||
|
let showPassword = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ close: void; created: Instance }>();
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
draft = emptyDraft();
|
||||||
|
step = 1;
|
||||||
|
method = 'phone';
|
||||||
|
authPhase = 'code';
|
||||||
|
flowId = '';
|
||||||
|
code = '';
|
||||||
|
password = '';
|
||||||
|
error = '';
|
||||||
|
busy = false;
|
||||||
|
showHash = false;
|
||||||
|
showPassword = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
reset();
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function validSettings() {
|
||||||
|
return (
|
||||||
|
/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(draft.instance_id) &&
|
||||||
|
draft.display_name.trim().length > 0 &&
|
||||||
|
Number(draft.api_id) > 0 &&
|
||||||
|
draft.api_hash.trim().length >= 16 &&
|
||||||
|
(method === 'phone'
|
||||||
|
? /^\+\d[\d\s()-]{6,}$/.test(draft.phone)
|
||||||
|
: draft.session_string.trim().length >= 32)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilReady(initial: Instance) {
|
||||||
|
let instance = initial;
|
||||||
|
for (let attempt = 0; attempt < 60 && instance.status === 'pending'; attempt += 1) {
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 2000));
|
||||||
|
instance = await api.instance(initial.instance_id);
|
||||||
|
if (instance.status === 'error') {
|
||||||
|
throw new Error(`Pod не запустился: ${instance.reason || 'неизвестная ошибка'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finishProvisioning(instance: Instance) {
|
||||||
|
step = 3;
|
||||||
|
const readyInstance = await waitUntilReady(instance);
|
||||||
|
dispatch('created', readyInstance);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function begin() {
|
||||||
|
error = '';
|
||||||
|
if (!validSettings()) {
|
||||||
|
error = 'Заполните обязательные поля и проверьте формат значений.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
if (method === 'session') {
|
||||||
|
step = 3;
|
||||||
|
const result = await api.stringSession(draft);
|
||||||
|
if (result.instance) {
|
||||||
|
await finishProvisioning(result.instance);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await api.startPhone(draft);
|
||||||
|
flowId = result.flow_id || '';
|
||||||
|
authPhase = 'code';
|
||||||
|
step = 2;
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Не удалось начать авторизацию';
|
||||||
|
step = 1;
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCode() {
|
||||||
|
if (!flowId || !code.trim()) return;
|
||||||
|
error = '';
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
const result = await api.submitCode(flowId, code);
|
||||||
|
if (result.status === 'password_required') {
|
||||||
|
authPhase = 'password';
|
||||||
|
} else if (result.instance) {
|
||||||
|
await finishProvisioning(result.instance);
|
||||||
|
}
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Не удалось проверить код';
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitPassword() {
|
||||||
|
if (!flowId || !password) return;
|
||||||
|
error = '';
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
const result = await api.submitPassword(flowId, password);
|
||||||
|
if (result.instance) {
|
||||||
|
await finishProvisioning(result.instance);
|
||||||
|
}
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : 'Не удалось проверить пароль';
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div class="drawer-backdrop" on:click={close} aria-hidden="true"></div>
|
||||||
|
<aside class="drawer add-drawer" aria-label="Добавить аккаунт">
|
||||||
|
<header class="drawer-header">
|
||||||
|
<div>
|
||||||
|
<h2>Добавить аккаунт</h2>
|
||||||
|
<p>Новый Kubernetes-инстанс userbot</p>
|
||||||
|
</div>
|
||||||
|
<button class="icon-button borderless" aria-label="Закрыть" on:click={close}>
|
||||||
|
<X size={19} />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ol class="steps" aria-label="Шаги создания">
|
||||||
|
<li class:active={step === 1} class:complete={step > 1}><span>1</span>Настройки</li>
|
||||||
|
<li class:active={step === 2} class:complete={step > 2}><span>2</span>Авторизация</li>
|
||||||
|
<li class:active={step === 3}><span>3</span>Создание</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{#if step === 1}
|
||||||
|
<form class="drawer-form" on:submit|preventDefault={begin}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Инстанс</legend>
|
||||||
|
<label>
|
||||||
|
Имя
|
||||||
|
<input
|
||||||
|
bind:value={draft.instance_id}
|
||||||
|
placeholder="personal"
|
||||||
|
maxlength="40"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small>Только a-z, 0-9 и дефис</small>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Отображаемое имя
|
||||||
|
<input bind:value={draft.display_name} placeholder="Personal" maxlength="80" required />
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Telegram API</legend>
|
||||||
|
<div class="two-columns">
|
||||||
|
<label>
|
||||||
|
API ID
|
||||||
|
<input bind:value={draft.api_id} inputmode="numeric" placeholder="12345678" required />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
API Hash
|
||||||
|
<span class="input-with-action">
|
||||||
|
<input
|
||||||
|
bind:value={draft.api_hash}
|
||||||
|
type={showHash ? 'text' : 'password'}
|
||||||
|
placeholder="0123456789abcdef"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={showHash ? 'Скрыть API Hash' : 'Показать API Hash'}
|
||||||
|
on:click={() => (showHash = !showHash)}
|
||||||
|
>
|
||||||
|
{#if showHash}<EyeOff size={15} />{:else}<Eye size={15} />{/if}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Авторизация</legend>
|
||||||
|
<div class="segmented">
|
||||||
|
<button type="button" class:active={method === 'phone'} on:click={() => (method = 'phone')}>
|
||||||
|
По номеру
|
||||||
|
</button>
|
||||||
|
<button type="button" class:active={method === 'session'} on:click={() => (method = 'session')}>
|
||||||
|
StringSession
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if method === 'phone'}
|
||||||
|
<label>
|
||||||
|
Номер телефона
|
||||||
|
<input bind:value={draft.phone} type="tel" placeholder="+421 900 123 456" required />
|
||||||
|
<small>Код придёт в Telegram. При включённой 2FA потребуется пароль.</small>
|
||||||
|
</label>
|
||||||
|
{:else}
|
||||||
|
<label>
|
||||||
|
StringSession
|
||||||
|
<textarea
|
||||||
|
bind:value={draft.session_string}
|
||||||
|
rows="3"
|
||||||
|
placeholder="Вставьте Pyrogram/Pyrofork StringSession"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Ресурсы</legend>
|
||||||
|
<div class="three-columns">
|
||||||
|
<label>
|
||||||
|
Хранилище
|
||||||
|
<select bind:value={draft.resources.storage}>
|
||||||
|
<option value="1Gi">1 GiB</option>
|
||||||
|
<option value="2Gi">2 GiB</option>
|
||||||
|
<option value="5Gi">5 GiB</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
CPU
|
||||||
|
<select bind:value={draft.resources.cpu_limit}>
|
||||||
|
<option value="200m">200m</option>
|
||||||
|
<option value="300m">300m</option>
|
||||||
|
<option value="500m">500m</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
RAM
|
||||||
|
<select bind:value={draft.resources.memory_limit}>
|
||||||
|
<option value="1024Mi">1 GiB</option>
|
||||||
|
<option value="1536Mi">1.5 GiB</option>
|
||||||
|
<option value="2048Mi">2 GiB</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{#if error}<p class="form-error" role="alert">{error}</p>{/if}
|
||||||
|
|
||||||
|
<footer class="form-actions">
|
||||||
|
<button type="button" on:click={close}>Отмена</button>
|
||||||
|
<button class="primary" type="submit" disabled={busy}>
|
||||||
|
{#if busy}<LoaderCircle class="spin" size={16} />{/if}
|
||||||
|
{method === 'phone' ? 'Запросить код' : 'Создать инстанс'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
{:else if step === 2}
|
||||||
|
<div class="auth-step">
|
||||||
|
<button class="back-link" on:click={() => (step = 1)}>
|
||||||
|
<ArrowLeft size={15} />Назад к настройкам
|
||||||
|
</button>
|
||||||
|
{#if authPhase === 'code'}
|
||||||
|
<h3>Введите код</h3>
|
||||||
|
<p>Код отправлен в Telegram на {draft.phone}.</p>
|
||||||
|
<form on:submit|preventDefault={submitCode}>
|
||||||
|
<label>
|
||||||
|
Код Telegram
|
||||||
|
<input
|
||||||
|
class="code-input"
|
||||||
|
bind:value={code}
|
||||||
|
inputmode="numeric"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
placeholder="12345"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{#if error}<p class="form-error" role="alert">{error}</p>{/if}
|
||||||
|
<button class="primary full" type="submit" disabled={busy || !code.trim()}>
|
||||||
|
{#if busy}<LoaderCircle class="spin" size={16} />{/if}
|
||||||
|
Авторизовать
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<h3>Пароль 2FA</h3>
|
||||||
|
<p>Для этого аккаунта включена двухэтапная аутентификация.</p>
|
||||||
|
<form on:submit|preventDefault={submitPassword}>
|
||||||
|
<label>
|
||||||
|
Пароль
|
||||||
|
<span class="input-with-action">
|
||||||
|
<input
|
||||||
|
bind:value={password}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={showPassword ? 'Скрыть пароль' : 'Показать пароль'}
|
||||||
|
on:click={() => (showPassword = !showPassword)}
|
||||||
|
>
|
||||||
|
{#if showPassword}<EyeOff size={15} />{:else}<Eye size={15} />{/if}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{#if error}<p class="form-error" role="alert">{error}</p>{/if}
|
||||||
|
<button class="primary full" type="submit" disabled={busy || !password}>
|
||||||
|
{#if busy}<LoaderCircle class="spin" size={16} />{/if}
|
||||||
|
Продолжить
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="creating-state">
|
||||||
|
<LoaderCircle class="spin" size={28} />
|
||||||
|
<h3>Создаём инстанс</h3>
|
||||||
|
<p>Secret, PVC и Deployment созданы. Ждём Ready в namespace userbot.</p>
|
||||||
|
{#if error}<p class="form-error" role="alert">{error}</p>{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
FileTerminal,
|
||||||
|
Play,
|
||||||
|
RotateCw,
|
||||||
|
Square,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { api } from '../lib/api';
|
||||||
|
import type { Instance } from '../lib/types';
|
||||||
|
import StatusDot from './StatusDot.svelte';
|
||||||
|
|
||||||
|
export let instance: Instance | null = null;
|
||||||
|
export let busy = false;
|
||||||
|
|
||||||
|
let logs = '';
|
||||||
|
let logsLoading = false;
|
||||||
|
let logsOpen = false;
|
||||||
|
let deleteOpen = false;
|
||||||
|
let deleteData = false;
|
||||||
|
let confirmation = '';
|
||||||
|
let activeInstance = '';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
close: void;
|
||||||
|
action: { instance: Instance; action: 'start' | 'stop' | 'restart' };
|
||||||
|
delete: { instance: Instance; deleteData: boolean };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
$: instanceKey = instance ? `${instance.namespace}/${instance.instance_id}` : '';
|
||||||
|
$: if (instanceKey !== activeInstance) {
|
||||||
|
activeInstance = instanceKey;
|
||||||
|
logs = '';
|
||||||
|
logsOpen = false;
|
||||||
|
deleteOpen = false;
|
||||||
|
deleteData = false;
|
||||||
|
confirmation = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLogs() {
|
||||||
|
if (!instance) return;
|
||||||
|
logsOpen = true;
|
||||||
|
logsLoading = true;
|
||||||
|
try {
|
||||||
|
logs = (await api.logs(instance.instance_id)).logs;
|
||||||
|
} catch (error) {
|
||||||
|
logs = error instanceof Error ? error.message : 'Не удалось загрузить логи';
|
||||||
|
} finally {
|
||||||
|
logsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(value: string | null) {
|
||||||
|
if (value) navigator.clipboard.writeText(value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if instance}
|
||||||
|
<div class="drawer-backdrop" on:click={() => dispatch('close')} aria-hidden="true"></div>
|
||||||
|
<aside class="drawer details-drawer" aria-label={`Инстанс ${instance.display_name}`}>
|
||||||
|
<header class="drawer-header">
|
||||||
|
<div>
|
||||||
|
<h2>{instance.display_name}</h2>
|
||||||
|
<StatusDot status={instance.status} />
|
||||||
|
</div>
|
||||||
|
<button class="icon-button borderless" aria-label="Закрыть" on:click={() => dispatch('close')}>
|
||||||
|
<X size={19} />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="drawer-scroll">
|
||||||
|
<section>
|
||||||
|
<h3>Ресурсы</h3>
|
||||||
|
<div class="resource-grid">
|
||||||
|
<div class="resource">
|
||||||
|
<span>CPU</span>
|
||||||
|
<strong>{instance.cpu_usage || '—'} <small>/ {instance.cpu_limit || '—'}</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="resource">
|
||||||
|
<span>RAM</span>
|
||||||
|
<strong>{instance.memory_usage || '—'} <small>/ {instance.memory_limit || '—'}</small></strong>
|
||||||
|
</div>
|
||||||
|
<div class="resource">
|
||||||
|
<span>Рестарты</span>
|
||||||
|
<strong>{instance.restarts}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="resource">
|
||||||
|
<span>Хранилище</span>
|
||||||
|
<strong>{instance.storage || '—'}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Kubernetes</h3>
|
||||||
|
<dl class="metadata-list">
|
||||||
|
<div>
|
||||||
|
<dt>Namespace</dt>
|
||||||
|
<dd>{instance.namespace}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Deployment</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{instance.deployment}</span>
|
||||||
|
<button class="copy-button" aria-label="Копировать Deployment" on:click={() => copy(instance?.deployment)}>
|
||||||
|
<Copy size={14} />
|
||||||
|
</button>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Pod</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{instance.pod || 'Нет pod'}</span>
|
||||||
|
{#if instance.pod}
|
||||||
|
<button class="copy-button" aria-label="Копировать Pod" on:click={() => copy(instance?.pod)}>
|
||||||
|
<Copy size={14} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>PVC</dt>
|
||||||
|
<dd>{instance.pvc || '—'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Image</dt>
|
||||||
|
<dd class="wrap">{instance.image}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if logsOpen}
|
||||||
|
<section class="logs-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<h3>Логи</h3>
|
||||||
|
<button class="text-button" on:click={loadLogs}>Обновить</button>
|
||||||
|
</div>
|
||||||
|
<pre>{logsLoading ? 'Загрузка…' : logs}</pre>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if deleteOpen}
|
||||||
|
<section class="danger-zone">
|
||||||
|
<h3>Удалить инстанс</h3>
|
||||||
|
<p>Deployment и credentials Secret будут удалены. PVC по умолчанию останется.</p>
|
||||||
|
<label>
|
||||||
|
Введите <strong>{instance.instance_id}</strong>
|
||||||
|
<input bind:value={confirmation} autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" bind:checked={deleteData} />
|
||||||
|
Удалить PVC и все данные
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="danger-button"
|
||||||
|
disabled={confirmation !== instance.instance_id || busy}
|
||||||
|
on:click={() => dispatch('delete', { instance, deleteData })}
|
||||||
|
>
|
||||||
|
Подтвердить удаление
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="drawer-actions">
|
||||||
|
<button on:click={loadLogs}><FileTerminal size={16} />Логи</button>
|
||||||
|
<button
|
||||||
|
disabled={busy || instance.status === 'stopped'}
|
||||||
|
on:click={() => dispatch('action', { instance, action: 'restart' })}
|
||||||
|
>
|
||||||
|
<RotateCw size={16} />Перезапустить
|
||||||
|
</button>
|
||||||
|
{#if instance.status === 'stopped'}
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
disabled={busy}
|
||||||
|
on:click={() => dispatch('action', { instance, action: 'start' })}
|
||||||
|
>
|
||||||
|
<Play size={16} />Запустить
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="primary"
|
||||||
|
disabled={busy}
|
||||||
|
on:click={() => dispatch('action', { instance, action: 'stop' })}
|
||||||
|
>
|
||||||
|
<Square size={15} />Остановить
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="delete-trigger"
|
||||||
|
disabled={!instance.deletable}
|
||||||
|
title={instance.deletable ? 'Удалить инстанс' : 'Legacy-инстансы удаляются из Git'}
|
||||||
|
on:click={() => (deleteOpen = !deleteOpen)}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />Удалить
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { FileTerminal, MoreVertical, Play, RotateCw, Square } from 'lucide-svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { relativeTime } from '../lib/instances';
|
||||||
|
import type { Instance } from '../lib/types';
|
||||||
|
import StatusDot from './StatusDot.svelte';
|
||||||
|
|
||||||
|
export let instances: Instance[] = [];
|
||||||
|
export let busyId = '';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
select: Instance;
|
||||||
|
action: { instance: Instance; action: 'start' | 'stop' | 'restart' | 'logs' };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function action(
|
||||||
|
event: MouseEvent,
|
||||||
|
instance: Instance,
|
||||||
|
name: 'start' | 'stop' | 'restart' | 'logs',
|
||||||
|
) {
|
||||||
|
event.stopPropagation();
|
||||||
|
dispatch('action', { instance, action: name });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="table-frame">
|
||||||
|
<div class="table-head table-grid" aria-hidden="true">
|
||||||
|
<span>ИНСТАНС</span>
|
||||||
|
<span>СТАТУС</span>
|
||||||
|
<span>POD</span>
|
||||||
|
<span>РЕСТАРТЫ</span>
|
||||||
|
<span>ХРАНИЛИЩЕ</span>
|
||||||
|
<span>ОБНОВЛЁН</span>
|
||||||
|
<span class="right">ДЕЙСТВИЯ</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each instances as instance (instance.namespace + instance.instance_id)}
|
||||||
|
<div
|
||||||
|
class="instance-row table-grid"
|
||||||
|
class:error-row={instance.status === 'error'}
|
||||||
|
on:click={() => dispatch('select', instance)}
|
||||||
|
on:keydown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') dispatch('select', instance);
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label={`Открыть ${instance.display_name}`}
|
||||||
|
>
|
||||||
|
<span class="instance-name">
|
||||||
|
<strong>{instance.display_name}</strong>
|
||||||
|
<small>{instance.instance_id} · {instance.namespace}</small>
|
||||||
|
</span>
|
||||||
|
<StatusDot status={instance.status} />
|
||||||
|
<span class="pod-name" title={instance.pod || instance.reason || ''}>
|
||||||
|
{instance.pod || instance.reason || 'Нет pod'}
|
||||||
|
</span>
|
||||||
|
<span>{instance.restarts}</span>
|
||||||
|
<span>{instance.storage || '—'}</span>
|
||||||
|
<span>{relativeTime(instance.updated_at)}</span>
|
||||||
|
<span class="row-actions">
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
title="Логи"
|
||||||
|
aria-label={`Логи ${instance.display_name}`}
|
||||||
|
on:click={(event) => action(event, instance, 'logs')}
|
||||||
|
>
|
||||||
|
<FileTerminal size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
title="Перезапустить"
|
||||||
|
aria-label={`Перезапустить ${instance.display_name}`}
|
||||||
|
disabled={busyId === instance.instance_id || instance.status === 'stopped'}
|
||||||
|
on:click={(event) => action(event, instance, 'restart')}
|
||||||
|
>
|
||||||
|
<RotateCw size={16} />
|
||||||
|
</button>
|
||||||
|
{#if instance.status === 'stopped'}
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
title="Запустить"
|
||||||
|
aria-label={`Запустить ${instance.display_name}`}
|
||||||
|
disabled={busyId === instance.instance_id}
|
||||||
|
on:click={(event) => action(event, instance, 'start')}
|
||||||
|
>
|
||||||
|
<Play size={16} />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
title="Остановить"
|
||||||
|
aria-label={`Остановить ${instance.display_name}`}
|
||||||
|
disabled={busyId === instance.instance_id}
|
||||||
|
on:click={(event) => action(event, instance, 'stop')}
|
||||||
|
>
|
||||||
|
<Square size={15} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
title="Подробнее"
|
||||||
|
aria-label={`Подробнее ${instance.display_name}`}
|
||||||
|
on:click={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
dispatch('select', instance);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVertical size={16} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Bot, Box, Plus, X } from 'lucide-svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
const dispatch = createEventDispatcher<{ add: void; close: void }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class:open aria-label="Навигация">
|
||||||
|
<div class="brand">
|
||||||
|
<Bot size={31} strokeWidth={1.8} />
|
||||||
|
<div>
|
||||||
|
<strong>USERBOT</strong>
|
||||||
|
<span>Kubernetes Control</span>
|
||||||
|
</div>
|
||||||
|
<button class="mobile-close" aria-label="Закрыть меню" on:click={() => dispatch('close')}>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<button class="nav-item active" on:click={() => dispatch('close')}>
|
||||||
|
<Box size={18} />
|
||||||
|
Инстансы
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" on:click={() => dispatch('add')}>
|
||||||
|
<Plus size={19} />
|
||||||
|
Добавить аккаунт
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="cluster-state">
|
||||||
|
<span class="cluster-dot"></span>
|
||||||
|
<div>
|
||||||
|
<strong>Кластер доступен</strong>
|
||||||
|
<span>namespace: userbot</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { InstanceStatus } from '../lib/types';
|
||||||
|
|
||||||
|
export let status: InstanceStatus;
|
||||||
|
|
||||||
|
const labels: Record<InstanceStatus, string> = {
|
||||||
|
running: 'Работает',
|
||||||
|
stopped: 'Остановлен',
|
||||||
|
pending: 'Запускается',
|
||||||
|
error: 'Ошибка',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="status status-{status}">
|
||||||
|
<span class="status-dot" aria-hidden="true"></span>
|
||||||
|
{labels[status]}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { AccountDraft, AuthResult, Instance } from "./types";
|
||||||
|
|
||||||
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(path, init);
|
||||||
|
if (!response.ok) {
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(payload.detail || response.statusText || "Request failed");
|
||||||
|
}
|
||||||
|
if (response.status === 204) return undefined as T;
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = (body: unknown): RequestInit => ({
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
instances(query = "", status = "") {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (query) params.set("query", query);
|
||||||
|
if (status) params.set("status", status);
|
||||||
|
const suffix = params.size ? `?${params}` : "";
|
||||||
|
return request<Instance[]>(`/api/instances${suffix}`);
|
||||||
|
},
|
||||||
|
instance(id: string) {
|
||||||
|
return request<Instance>(`/api/instances/${encodeURIComponent(id)}`);
|
||||||
|
},
|
||||||
|
logs(id: string) {
|
||||||
|
return request<{ logs: string }>(`/api/instances/${encodeURIComponent(id)}/logs`);
|
||||||
|
},
|
||||||
|
action(id: string, action: "start" | "stop" | "restart") {
|
||||||
|
return request<Instance>(`/api/instances/${encodeURIComponent(id)}/${action}`, { method: "POST" });
|
||||||
|
},
|
||||||
|
delete(id: string, deleteData: boolean) {
|
||||||
|
return request<void>(
|
||||||
|
`/api/instances/${encodeURIComponent(id)}/delete`,
|
||||||
|
json({ confirmation: id, delete_data: deleteData }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
startPhone(draft: AccountDraft) {
|
||||||
|
return request<AuthResult>(
|
||||||
|
"/api/auth/phone/start",
|
||||||
|
json({
|
||||||
|
...basePayload(draft),
|
||||||
|
phone: draft.phone,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
submitCode(flowId: string, code: string) {
|
||||||
|
return request<AuthResult>(`/api/auth/phone/${encodeURIComponent(flowId)}/code`, json({ code }));
|
||||||
|
},
|
||||||
|
submitPassword(flowId: string, password: string) {
|
||||||
|
return request<AuthResult>(`/api/auth/phone/${encodeURIComponent(flowId)}/password`, json({ password }));
|
||||||
|
},
|
||||||
|
stringSession(draft: AccountDraft) {
|
||||||
|
return request<AuthResult>(
|
||||||
|
"/api/auth/string-session",
|
||||||
|
json({
|
||||||
|
...basePayload(draft),
|
||||||
|
session_string: draft.session_string,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function basePayload(draft: AccountDraft) {
|
||||||
|
return {
|
||||||
|
instance_id: draft.instance_id,
|
||||||
|
display_name: draft.display_name,
|
||||||
|
api_id: Number(draft.api_id),
|
||||||
|
api_hash: draft.api_hash,
|
||||||
|
resources: draft.resources,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { filterInstances, relativeTime } from "./instances";
|
||||||
|
import type { Instance } from "./types";
|
||||||
|
|
||||||
|
const instance = (overrides: Partial<Instance>): Instance => ({
|
||||||
|
instance_id: "forust",
|
||||||
|
display_name: "Forust",
|
||||||
|
namespace: "userbot",
|
||||||
|
deployment: "userbot-forust",
|
||||||
|
pod: "userbot-forust-abc",
|
||||||
|
status: "running",
|
||||||
|
reason: null,
|
||||||
|
ready: true,
|
||||||
|
restarts: 0,
|
||||||
|
pvc: "userbot-forust-data",
|
||||||
|
storage: "1Gi",
|
||||||
|
image: "userbot:latest",
|
||||||
|
cpu_limit: "300m",
|
||||||
|
memory_limit: "1536Mi",
|
||||||
|
cpu_usage: null,
|
||||||
|
memory_usage: null,
|
||||||
|
updated_at: null,
|
||||||
|
legacy: false,
|
||||||
|
deletable: true,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filterInstances", () => {
|
||||||
|
it("filters by status and searchable fields", () => {
|
||||||
|
const items = [
|
||||||
|
instance({ instance_id: "forust" }),
|
||||||
|
instance({ instance_id: "anna", display_name: "Anna", status: "stopped" }),
|
||||||
|
];
|
||||||
|
expect(filterInstances(items, "ann", "stopped").map((item) => item.instance_id)).toEqual(["anna"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("relativeTime", () => {
|
||||||
|
it("formats recent timestamps", () => {
|
||||||
|
expect(relativeTime("2026-01-01T00:00:00Z", Date.parse("2026-01-01T00:00:45Z"))).toBe("45 сек назад");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Instance } from "./types";
|
||||||
|
|
||||||
|
export function filterInstances(instances: Instance[], query: string, status: string): Instance[] {
|
||||||
|
const normalized = query.trim().toLowerCase();
|
||||||
|
return instances.filter((instance) => {
|
||||||
|
const matchesStatus = !status || instance.status === status;
|
||||||
|
const matchesQuery =
|
||||||
|
!normalized ||
|
||||||
|
instance.instance_id.toLowerCase().includes(normalized) ||
|
||||||
|
instance.display_name.toLowerCase().includes(normalized) ||
|
||||||
|
(instance.pod || "").toLowerCase().includes(normalized);
|
||||||
|
return matchesStatus && matchesQuery;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function relativeTime(value: string | null, now = Date.now()): string {
|
||||||
|
if (!value) return "—";
|
||||||
|
const seconds = Math.max(0, Math.round((now - new Date(value).getTime()) / 1000));
|
||||||
|
if (seconds < 60) return `${seconds} сек назад`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) return `${minutes} мин назад`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
return `${hours} ч назад`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export type InstanceStatus = "running" | "stopped" | "pending" | "error";
|
||||||
|
|
||||||
|
export interface Instance {
|
||||||
|
instance_id: string;
|
||||||
|
display_name: string;
|
||||||
|
namespace: string;
|
||||||
|
deployment: string;
|
||||||
|
pod: string | null;
|
||||||
|
status: InstanceStatus;
|
||||||
|
reason: string | null;
|
||||||
|
ready: boolean;
|
||||||
|
restarts: number;
|
||||||
|
pvc: string | null;
|
||||||
|
storage: string | null;
|
||||||
|
image: string;
|
||||||
|
cpu_limit: string | null;
|
||||||
|
memory_limit: string | null;
|
||||||
|
cpu_usage: string | null;
|
||||||
|
memory_usage: string | null;
|
||||||
|
updated_at: string | null;
|
||||||
|
legacy: boolean;
|
||||||
|
deletable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resources {
|
||||||
|
storage: string;
|
||||||
|
cpu_limit: string;
|
||||||
|
memory_limit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountDraft {
|
||||||
|
instance_id: string;
|
||||||
|
display_name: string;
|
||||||
|
api_id: string;
|
||||||
|
api_hash: string;
|
||||||
|
phone: string;
|
||||||
|
session_string: string;
|
||||||
|
resources: Resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResult {
|
||||||
|
status: "code_required" | "password_required" | "ready";
|
||||||
|
flow_id?: string;
|
||||||
|
instance?: Instance;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { mount } from "svelte";
|
||||||
|
|
||||||
|
import App from "./App.svelte";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
mount(App, {
|
||||||
|
target: document.getElementById("app")!,
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["svelte", "vite/client", "vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://127.0.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: ["./src/test/setup.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"venvPath": "/home/forust/homelab/userbot",
|
||||||
|
"venv": ".venv"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofiles"
|
name = "aiofiles"
|
||||||
@@ -35,6 +35,40 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" },
|
||||||
@@ -94,6 +128,7 @@ version = "1.4.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "frozenlist" },
|
{ name = "frozenlist" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -146,6 +181,38 @@ version = "3.4.4"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||||
@@ -247,6 +314,38 @@ version = "1.8.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
|
||||||
@@ -395,6 +494,28 @@ version = "3.0.3"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||||
@@ -456,6 +577,42 @@ version = "6.7.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" },
|
||||||
@@ -517,7 +674,7 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/a3/67f183VOE_STREET_ID0f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" },
|
{ url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" },
|
{ url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" },
|
{ url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" },
|
||||||
@@ -546,6 +703,28 @@ version = "12.0.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
|
||||||
@@ -565,7 +744,7 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f162VOE_STREET_ID327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
|
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
|
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
|
||||||
@@ -592,10 +771,17 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f9VOE_STREET_IDd3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -604,6 +790,36 @@ version = "0.4.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
|
||||||
@@ -726,6 +942,26 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/7b/a709c85dc716eb85b69f71a4bb375cf1e72758a7e872103f27551243319c/pymongo-4.15.3.tar.gz", hash = "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5", size = 2470801, upload-time = "2025-10-07T21:57:50.384Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/7b/a709c85dc716eb85b69f71a4bb375cf1e72758a7e872103f27551243319c/pymongo-4.15.3.tar.gz", hash = "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5", size = 2470801, upload-time = "2025-10-07T21:57:50.384Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/04/3dbc426c5868961d8308f19750243f8472f587f5f8a5029ce6953ba74b82/pymongo-4.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a13d8f7141294404ce46dfbabb2f2d17e9b1192456651ae831fa351f86fbeb", size = 865889, upload-time = "2025-10-07T21:56:14.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/39/7f7652f53dd0eb0c4c3420a175183da757e9c53f9a2bf3ebc589758a1b9e/pymongo-4.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:17d13458baf4a6a9f2e787d95adf8ec50d412accb9926a044bd1c41029c323b2", size = 866230, upload-time = "2025-10-07T21:56:15.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/0b/84e119e6bab7b19cf4fa1ebb9b4c29bf6c0e76521ed8221b44e3f94a3a37/pymongo-4.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fe4bcb8acfb288e238190397d4a699aeb4adb70e8545a6f4e44f99d4e8096ab1", size = 1429788, upload-time = "2025-10-07T21:56:17.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/39/9905fcb99903de6ac8483114d1c85efe56bc5df735857bdfcc372cf8a3ec/pymongo-4.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d09d895c7f08bcbed4d2e96a00e52e9e545ae5a37b32d2dc10099b205a21fc6d", size = 1456758, upload-time = "2025-10-07T21:56:18.841Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/58/3c3ac32b8d6ebb654083d53f58e4621cd4c7f306b3b85acef667b80acf08/pymongo-4.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21c0a95a4db72562fd0805e2f76496bf432ba2e27a5651f4b9c670466260c258", size = 1514666, upload-time = "2025-10-07T21:56:20.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/e2/52f41de224218dc787b7e1187a1ca1a51946dcb979ee553ec917745ccd8d/pymongo-4.15.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89e45d7fa987f4e246cdf43ff001e3f911f73eb19ba9dabc2a6d80df5c97883b", size = 1500703, upload-time = "2025-10-07T21:56:21.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/0d/a5271073339ba6fc8a5f4e3a62baaa5dd8bf35246c37b512317e2a22848e/pymongo-4.15.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1246a82fa6dd73ac2c63aa7e463752d5d1ca91e0c7a23396b78f21273befd3a7", size = 1452013, upload-time = "2025-10-07T21:56:23.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/3b/f39b721ca0db9f0820e12eeffec84eb87b7502abb13a685226c5434f9618/pymongo-4.15.3-cp311-cp311-win32.whl", hash = "sha256:9483521c03f6017336f54445652ead3145154e8d3ea06418e52cea57fee43292", size = 844461, upload-time = "2025-10-07T21:56:24.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/72/e58b9df862edbf238a1d71fa32749a6eaf30a3f60289602681351c29093a/pymongo-4.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:c57dad9f289d72af1d7c47a444c4d9fa401f951cedbbcc54c7dd0c2107d6d786", size = 859200, upload-time = "2025-10-07T21:56:26.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/8f/64c15df5e87de759412c3b962950561202c9b39e5cc604061e056043e163/pymongo-4.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:2fd3b99520f2bb013960ac29dece1b43f2f1b6d94351ca33ba1b1211ecf79a09", size = 848372, upload-time = "2025-10-07T21:56:27.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/92/7491a2046b41bfd3641da0a23529c88e27eac67c681de3cd9fbef4113d38/pymongo-4.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd0497c564b0ae34fb816464ffc09986dd9ca29e2772a0f7af989e472fecc2ad", size = 920953, upload-time = "2025-10-07T21:56:29.737Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/0c/98864cbfa8fbc954ae7480c91a35f0dc4e3339dab0c55f669e4dbeac808f/pymongo-4.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:292fd5a3f045751a823a54cdea75809b2216a62cc5f74a1a96b337db613d46a8", size = 920690, upload-time = "2025-10-07T21:56:31.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/a6/7dc8043a10a1c30153be2d6847ab37911b169d53a6b05d21871b35b3de82/pymongo-4.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:959ef69c5e687b6b749fbf2140c7062abdb4804df013ae0507caabf30cba6875", size = 1690357, upload-time = "2025-10-07T21:56:32.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/96/3d85da60094d2022217f2849e1b61a79af9d51ed8d05455d7413d68ab88e/pymongo-4.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de3bc878c3be54ae41c2cabc9e9407549ed4fec41f4e279c04e840dddd7c630c", size = 1726102, upload-time = "2025-10-07T21:56:33.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/fd/dfd6ddee0330171f2f52f7e5344c02d25d2dd8dfa95ce0e5e413579f52fd/pymongo-4.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07bcc36d11252f24fe671e7e64044d39a13d997b0502c6401161f28cc144f584", size = 1800630, upload-time = "2025-10-07T21:56:35.632Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/3b/e19a5f2de227ff720bc76c41d166d508e6fbe1096ba1ad18ade43b790b5e/pymongo-4.15.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b63bac343b79bd209e830aac1f5d9d552ff415f23a924d3e51abbe3041265436", size = 1785478, upload-time = "2025-10-07T21:56:37.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/d2/927c9b1383c6708fc50c3700ecb1c2876e67dde95ad5fb1d29d04e8ac083/pymongo-4.15.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33d59bf6fa1ca1d7d96d4fccff51e41312358194190d53ef70a84c070f5287e", size = 1718548, upload-time = "2025-10-07T21:56:38.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/10/a63592d1445f894b18d04865c2d4c235e2261f3d63f31f45ba4fe0486ec4/pymongo-4.15.3-cp312-cp312-win32.whl", hash = "sha256:b3a0ec660d61efb91c16a5962ec937011fe3572c4338216831f102e53d294e5c", size = 891301, upload-time = "2025-10-07T21:56:40.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/ba/a8fdc43044408ed769c83108fa569aa52ee87968bdbf1e2ea142b109c268/pymongo-4.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:f6b0513e5765fdde39f36e6a29a36c67071122b5efa748940ae51075beb5e4bc", size = 910928, upload-time = "2025-10-07T21:56:41.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/61/d53c17fdfaa9149864ab1fa84436ae218b72c969f00e4c124e017e461ce6/pymongo-4.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4fdd8e6eab8ff77c1c8041792b5f760d48508623cd10b50d5639e73f1eec049", size = 896347, upload-time = "2025-10-07T21:56:43.271Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/a4/e1ce9d408a1c1bcb1554ff61251b108e16cefd7db91b33faa2afc92294de/pymongo-4.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a47a3218f7900f65bf0f36fcd1f2485af4945757360e7e143525db9d715d2010", size = 975329, upload-time = "2025-10-07T21:56:44.674Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/a4/e1ce9d408a1c1bcb1554ff61251b108e16cefd7db91b33faa2afc92294de/pymongo-4.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a47a3218f7900f65bf0f36fcd1f2485af4945757360e7e143525db9d715d2010", size = 975329, upload-time = "2025-10-07T21:56:44.674Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/3c/6796f653d22be43cc0b13c07dbed84133eebbc334ebed4426459b7250163/pymongo-4.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1", size = 975129, upload-time = "2025-10-07T21:56:46.104Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/3c/6796f653d22be43cc0b13c07dbed84133eebbc334ebed4426459b7250163/pymongo-4.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1", size = 975129, upload-time = "2025-10-07T21:56:46.104Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/33/22453dbfe11031e89c9cbdfde6405c03960daaf5da1b4dfdd458891846b5/pymongo-4.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97f9babdb98c31676f97d468f7fe2dc49b8a66fb6900effddc4904c1450196c8", size = 1950979, upload-time = "2025-10-07T21:56:47.877Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/33/22453dbfe11031e89c9cbdfde6405c03960daaf5da1b4dfdd458891846b5/pymongo-4.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97f9babdb98c31676f97d468f7fe2dc49b8a66fb6900effddc4904c1450196c8", size = 1950979, upload-time = "2025-10-07T21:56:47.877Z" },
|
||||||
@@ -849,6 +1085,17 @@ name = "tgcrypto"
|
|||||||
version = "1.2.5"
|
version = "1.2.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/33/59/7cf5ced989e3139a791d5452d58cb8994de589576b80f9267ba76d794f6c/TgCrypto-1.2.5.tar.gz", hash = "sha256:9bc2cac6fb9a12ef5b08f3dd500174fe374d89b660cce981f57e3138559cb682", size = 37280, upload-time = "2022-11-11T19:55:11.436Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/33/59/7cf5ced989e3139a791d5452d58cb8994de589576b80f9267ba76d794f6c/TgCrypto-1.2.5.tar.gz", hash = "sha256:9bc2cac6fb9a12ef5b08f3dd500174fe374d89b660cce981f57e3138559cb682", size = 37280, upload-time = "2022-11-11T19:55:11.436Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/e6/07dc413e57f3677e0b887975b174d9cbc8170e92651dd35a784da9571d5f/TgCrypto-1.2.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:59597cdb1c87eb1184088563d20b42a8f2e431e9334fed64926079044ad2a4af", size = 58988, upload-time = "2022-11-11T19:51:41.686Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/fe/a418223ff6079184f50f5ad8c2dac37ec40adbb42b886c3ee44fdba2a540/TgCrypto-1.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1283337ae75b02406dd700377b8b783e70033b548492517df6e6c4156b0ed69c", size = 43347, upload-time = "2022-11-11T19:51:43.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/0e/e861a9987c6d9d3ae04d156732311e018558f862490c466c369d73094807/TgCrypto-1.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1735437df0023a40e5fdd95e6b09ce806ec8f2cd2f8879023818840dfae60cab", size = 43123, upload-time = "2022-11-11T19:51:45.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/ce/d60e249ac4d31d591f4c8ca76e7bd537f4c54f1947b400a14ac3a1678b09/TgCrypto-1.2.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfa17a20206532c6d2442c9d7a7f6434120bd75896ad9a3e9b9277477afa084f", size = 60713, upload-time = "2022-11-11T19:51:46.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/55/50f81b1a557bd8f9b73f7fcda2381f8826665858750968394ceec0b01282/TgCrypto-1.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48da3674474839e5619e7430ff1f98aed9f55369f3cfaef7f65511852869572e", size = 60418, upload-time = "2022-11-11T19:51:47.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/55/7f5f5a024b71ef81b07c3fe15e79d03b634eb349b04bc79a62c8c3ed3376/TgCrypto-1.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b49e982e5b156be821a5235bd9102c00dc506a58607e2c8bd50ac872724a951f", size = 64397, upload-time = "2022-11-11T19:51:49.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/3e/8f059edce82de8ebbc1e513076c0f65a86fac95887fae21ae087440ef23a/TgCrypto-1.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9d9f13586065a6d86d05c16409054033a84be208acee29b49f6f194e27b08642", size = 64436, upload-time = "2022-11-11T19:51:51.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/87/fe894eb7c4cce4b6f710223c24e0ca22f4107fe46f3f56091ceabd482de7/TgCrypto-1.2.5-cp311-cp311-win32.whl", hash = "sha256:10dd3870aecb1a783c6eafd3b164b2149dbc93a9ee13feb7e6f5c58f87c24cd0", size = 44723, upload-time = "2022-11-11T19:51:52.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/fc/b918235b19b70a45f4a596c773fad2c379d941d2ac5005293e2ee18bdc63/TgCrypto-1.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:a1beec47d6af8b509af7cf266e30f7703208076076594714005b42d2c25225b3", size = 45073, upload-time = "2022-11-11T19:51:54.724Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
@@ -951,6 +1198,38 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
|
||||||
@@ -984,7 +1263,7 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
|
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
|
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2VOE_STREET_ID99c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
|
||||||
|
|||||||
Reference in New Issue
Block a user