Add unit tests (#133)
* Add initial test & add pipeline * Add very basic YAML config tests * Add coverage reporting * Add some webserver & template loading tests * Add test cases for the helpers * Implement initial OIDC server tests * Test codestore & discovery checker * Test basics of the config flow * Add test for the HA auth provider * Cleaned up tests & test injection
This commit is contained in:
committed by
GitHub
parent
5714e844a7
commit
404d2451df
24
.github/workflows/test.yaml
vendored
Normal file
24
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Tests (pytest)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: "Set up Python"
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version-file: ".python-version"
|
||||||
|
- name: Install the latest version of uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
- name: Sync dependencies
|
||||||
|
run: scripts/sync
|
||||||
|
- name: Test
|
||||||
|
run: scripts/test
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -108,3 +108,5 @@ dmypy.json
|
|||||||
/config/
|
/config/
|
||||||
|
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
|
.pytest_logs.log
|
||||||
@@ -67,6 +67,4 @@ class OIDCCallbackView(HomeAssistantView):
|
|||||||
return web.Response(text=view_html, content_type="text/html")
|
return web.Response(text=view_html, content_type="text/html")
|
||||||
|
|
||||||
code = await self.oidc_provider.async_save_user_info(user_details)
|
code = await self.oidc_provider.async_save_user_info(user_details)
|
||||||
return web.HTTPFound(
|
raise web.HTTPFound(get_url("/auth/oidc/finish?code=" + code, self.force_https))
|
||||||
get_url("/auth/oidc/finish?code=" + code, self.force_https)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class OIDCFinishView(HomeAssistantView):
|
|||||||
return web.Response(text="No code received", status=500)
|
return web.Response(text="No code received", status=500)
|
||||||
|
|
||||||
# Return redirect to the main page for sign in with a cookie
|
# Return redirect to the main page for sign in with a cookie
|
||||||
return web.HTTPFound(
|
raise web.HTTPFound(
|
||||||
location="/?storeToken=true",
|
location="/?storeToken=true",
|
||||||
headers={
|
headers={
|
||||||
# Set a cookie to enable autologin on only the specific path used
|
# Set a cookie to enable autologin on only the specific path used
|
||||||
|
|||||||
@@ -25,10 +25,14 @@ class OIDCRedirectView(HomeAssistantView):
|
|||||||
"""Receive response."""
|
"""Receive response."""
|
||||||
|
|
||||||
redirect_uri = get_url("/auth/oidc/callback", self.force_https)
|
redirect_uri = get_url("/auth/oidc/callback", self.force_https)
|
||||||
auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri)
|
|
||||||
|
|
||||||
if auth_url:
|
try:
|
||||||
return web.HTTPFound(auth_url)
|
auth_url = await self.oidc_client.async_get_authorization_url(redirect_uri)
|
||||||
|
|
||||||
|
if auth_url:
|
||||||
|
raise web.HTTPFound(auth_url)
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
view_html = await get_view(
|
view_html = await get_view(
|
||||||
"error",
|
"error",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class OIDCWelcomeView(HomeAssistantView):
|
|||||||
"""Receive response."""
|
"""Receive response."""
|
||||||
|
|
||||||
if not self.is_enabled:
|
if not self.is_enabled:
|
||||||
return web.HTTPTemporaryRedirect(get_url("/", self.force_https))
|
raise web.HTTPTemporaryRedirect(get_url("/", self.force_https))
|
||||||
|
|
||||||
view_html = await get_view("welcome", {"name": self.name})
|
view_html = await get_view("welcome", {"name": self.name})
|
||||||
return web.Response(text=view_html, content_type="text/html")
|
return web.Response(text=view_html, content_type="text/html")
|
||||||
|
|||||||
@@ -177,9 +177,9 @@ class OpenIDAuthProvider(AuthProvider):
|
|||||||
# If person creation is enabled, add a person for this user
|
# If person creation is enabled, add a person for this user
|
||||||
if self.create_persons:
|
if self.create_persons:
|
||||||
user_meta = await self.async_user_meta_for_credentials(credential)
|
user_meta = await self.async_user_meta_for_credentials(credential)
|
||||||
await self.async_create_person(user, user_meta.name)
|
await self._async_create_person(user, user_meta.name)
|
||||||
|
|
||||||
async def async_create_person(self, user: User, name: str) -> None:
|
async def _async_create_person(self, user: User, name: str) -> None:
|
||||||
"""Create a person for the user."""
|
"""Create a person for the user."""
|
||||||
_LOGGER.info("Automatically creating person for new user %s", user.id)
|
_LOGGER.info("Automatically creating person for new user %s", user.id)
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class OpenIDAuthProvider(AuthProvider):
|
|||||||
# pylint: disable=broad-exception-caught
|
# pylint: disable=broad-exception-caught
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Requested automatic person creation, but person creation failed."
|
"Requested automatic person creation, but person creation failed"
|
||||||
)
|
)
|
||||||
# pylint: enable=broad-exception-caught
|
# pylint: enable=broad-exception-caught
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ class OpenIdLoginFlow(LoginFlow):
|
|||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
|
|
||||||
# Try to use the user input first
|
# Try to use the user input first
|
||||||
if user_input is not None:
|
if user_input is not None and "code" in user_input:
|
||||||
try:
|
try:
|
||||||
return await self._finalize_user(user_input["code"])
|
return await self._finalize_user(user_input["code"])
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
@@ -323,14 +323,15 @@ class OpenIdLoginFlow(LoginFlow):
|
|||||||
|
|
||||||
# If not available, check the cookie
|
# If not available, check the cookie
|
||||||
req = http.current_request.get()
|
req = http.current_request.get()
|
||||||
code_cookie = req.cookies.get("auth_oidc_code")
|
if req and req.cookies:
|
||||||
|
code_cookie = req.cookies.get("auth_oidc_code")
|
||||||
|
|
||||||
if code_cookie:
|
if code_cookie:
|
||||||
_LOGGER.debug("Code cookie found on login: %s", code_cookie)
|
_LOGGER.debug("Code cookie found on login: %s", code_cookie)
|
||||||
try:
|
try:
|
||||||
return await self._finalize_user(code_cookie)
|
return await self._finalize_user(code_cookie)
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# If none are available, just show the form
|
# If none are available, just show the form
|
||||||
return self._show_login_form()
|
return self._show_login_form()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import cast, Optional
|
from typing import cast, Optional
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -31,7 +31,7 @@ class CodeStore:
|
|||||||
data = cast(dict[str, UserDetails], {})
|
data = cast(dict[str, UserDetails], {})
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
async def async_save(self) -> None:
|
async def _async_save(self) -> None:
|
||||||
"""Save data."""
|
"""Save data."""
|
||||||
if self._data is not None:
|
if self._data is not None:
|
||||||
await self._store.async_save(self._data)
|
await self._store.async_save(self._data)
|
||||||
@@ -46,7 +46,7 @@ class CodeStore:
|
|||||||
raise RuntimeError("Data not loaded")
|
raise RuntimeError("Data not loaded")
|
||||||
|
|
||||||
code = self._generate_code()
|
code = self._generate_code()
|
||||||
expiration = datetime.utcnow() + timedelta(minutes=5)
|
expiration = datetime.now(timezone.utc) + timedelta(minutes=5)
|
||||||
|
|
||||||
self._data[code] = {
|
self._data[code] = {
|
||||||
"user_info": user_info,
|
"user_info": user_info,
|
||||||
@@ -54,7 +54,7 @@ class CodeStore:
|
|||||||
"expiration": expiration.isoformat(),
|
"expiration": expiration.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.async_save()
|
await self._async_save()
|
||||||
return code
|
return code
|
||||||
|
|
||||||
async def receive_userinfo_for_code(self, code: str) -> Optional[UserDetails]:
|
async def receive_userinfo_for_code(self, code: str) -> Optional[UserDetails]:
|
||||||
@@ -67,12 +67,15 @@ class CodeStore:
|
|||||||
if user_data:
|
if user_data:
|
||||||
# We should now wipe it from the database, as it's one time use code
|
# We should now wipe it from the database, as it's one time use code
|
||||||
self._data.pop(code)
|
self._data.pop(code)
|
||||||
await self.async_save()
|
await self._async_save()
|
||||||
|
|
||||||
if (
|
if user_data and datetime.fromisoformat(user_data["expiration"]) > datetime.now(
|
||||||
user_data
|
timezone.utc
|
||||||
and datetime.fromisoformat(user_data["expiration"]) > datetime.utcnow()
|
|
||||||
):
|
):
|
||||||
return user_data["user_info"]
|
return user_data["user_info"]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
"""Get the internal data for testing purposes."""
|
||||||
|
return self._data
|
||||||
|
|||||||
@@ -39,12 +39,8 @@ class OIDCDiscoveryInvalid(OIDCClientException):
|
|||||||
type: Optional[str]
|
type: Optional[str]
|
||||||
details: Optional[dict]
|
details: Optional[dict]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
if args:
|
self.message = "OIDC Discovery document is invalid"
|
||||||
self.message = args[0]
|
|
||||||
else:
|
|
||||||
self.message = "OIDC Discovery document is invalid"
|
|
||||||
|
|
||||||
self.type = kwargs.pop("type", None)
|
self.type = kwargs.pop("type", None)
|
||||||
self.details = kwargs.pop("details", None)
|
self.details = kwargs.pop("details", None)
|
||||||
super().__init__(self.message)
|
super().__init__(self.message)
|
||||||
@@ -196,7 +192,7 @@ class OIDCDiscoveryClient:
|
|||||||
)
|
)
|
||||||
raise OIDCDiscoveryInvalid(
|
raise OIDCDiscoveryInvalid(
|
||||||
type="does_not_support_response_mode",
|
type="does_not_support_response_mode",
|
||||||
modes=document["response_modes_supported"],
|
details={"modes": document["response_modes_supported"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
# If grant_types_supported is set, should support 'authorization_code'
|
# If grant_types_supported is set, should support 'authorization_code'
|
||||||
@@ -281,7 +277,7 @@ class OIDCDiscoveryClient:
|
|||||||
await self._validate_discovery_document(document)
|
await self._validate_discovery_document(document)
|
||||||
return document
|
return document
|
||||||
|
|
||||||
async def fetch_jwks(self, jwks_uri: str | None):
|
async def fetch_jwks(self, jwks_uri: str | None = None):
|
||||||
"""Fetches JWKS."""
|
"""Fetches JWKS."""
|
||||||
if jwks_uri is None:
|
if jwks_uri is None:
|
||||||
discovery_document = await self._fetch_discovery_document()
|
discovery_document = await self._fetch_discovery_document()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def validate_url(url: str) -> bool:
|
|||||||
try:
|
try:
|
||||||
parsed = urlparse(url.strip())
|
parsed = urlparse(url.strip())
|
||||||
return bool(parsed.scheme in ("http", "https") and parsed.netloc)
|
return bool(parsed.scheme in ("http", "https") and parsed.netloc)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError, AttributeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ def validate_discovery_url(url: str) -> bool:
|
|||||||
and parsed.netloc
|
and parsed.netloc
|
||||||
and parsed.path.endswith("/.well-known/openid-configuration")
|
and parsed.path.endswith("/.well-known/openid-configuration")
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError, AttributeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class AsyncTemplateRenderer:
|
|||||||
) as f:
|
) as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
templates[filename] = content
|
templates[filename] = content
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e: # pragma: no cover
|
||||||
_LOGGER.warning("Error reading template file %s: %s", filename, e)
|
_LOGGER.warning("Error reading template file %s: %s", filename, e)
|
||||||
|
|
||||||
async def render_template(self, template_name: str, **kwargs: Any) -> str:
|
async def render_template(self, template_name: str, **kwargs: Any) -> str:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ dependencies = [
|
|||||||
"aiofiles~=24.1",
|
"aiofiles~=24.1",
|
||||||
"jinja2~=3.1",
|
"jinja2~=3.1",
|
||||||
"bcrypt~=4.2",
|
"bcrypt~=4.2",
|
||||||
|
"joserfc>=1.3.4",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = "~=3.13.7"
|
requires-python = "~=3.13.7"
|
||||||
@@ -19,6 +20,10 @@ requires-python = "~=3.13.7"
|
|||||||
dev = [
|
dev = [
|
||||||
"homeassistant~=2025.8",
|
"homeassistant~=2025.8",
|
||||||
"pylint~=3.3",
|
"pylint~=3.3",
|
||||||
|
"pytest>=8.4.2",
|
||||||
|
"pytest-asyncio>=1.2.0",
|
||||||
|
"pytest-cov>=7.0.0",
|
||||||
|
"pytest-homeassistant-custom-component>=0.13.286",
|
||||||
"ruff~=0.12",
|
"ruff~=0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -34,3 +39,8 @@ allow-direct-references = true
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["custom_components/auth_oidc"]
|
packages = ["custom_components/auth_oidc"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
addopts = "--cov=custom_components --cov-fail-under=0"
|
||||||
|
log_level = "DEBUG"
|
||||||
|
|||||||
3
scripts/coverage-report
Executable file
3
scripts/coverage-report
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
uv run pytest --cov-report html tests/
|
||||||
|
uv run python -m http.server 8000 -d htmlcov
|
||||||
2
scripts/test
Executable file
2
scripts/test
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
uv run pytest --cov-report term:skip-covered tests/
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
8
tests/conftest.py
Normal file
8
tests/conftest.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""Fixtures for testing."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def auto_enable_custom_integrations(enable_custom_integrations):
|
||||||
|
yield
|
||||||
0
tests/mocks/__init__.py
Normal file
0
tests/mocks/__init__.py
Normal file
14
tests/mocks/auth_page.html
Normal file
14
tests/mocks/auth_page.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
Test page
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
197
tests/mocks/oidc_server.py
Normal file
197
tests/mocks/oidc_server.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
"""A simple mock OIDC server for testing purposes."""
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
from joserfc import jwt
|
||||||
|
from joserfc.jwk import RSAKey, KeySet
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BASE_URL = "https://oidc.example.com"
|
||||||
|
SUBJECT = "testuser"
|
||||||
|
|
||||||
|
|
||||||
|
class MockOIDCServer:
|
||||||
|
"""A simple mock OIDC server for testing purposes."""
|
||||||
|
|
||||||
|
_code_storage = {}
|
||||||
|
_scenario = {}
|
||||||
|
|
||||||
|
def __init__(self, scenario: str | None = None):
|
||||||
|
"""Initialize the mock OIDC server."""
|
||||||
|
# Create a JWK private key
|
||||||
|
self._jwk = RSAKey.generate_key(
|
||||||
|
2048, {"alg": "RS256", "use": "sig"}, private=True, auto_kid=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if scenario:
|
||||||
|
# Load scenario JSON file from disk
|
||||||
|
scenario_path = os.path.join(
|
||||||
|
os.path.dirname(__file__), "scenarios", f"{scenario}.json"
|
||||||
|
)
|
||||||
|
with open(scenario_path, "r", encoding="utf-8") as f:
|
||||||
|
self._scenario = json.load(f)
|
||||||
|
|
||||||
|
# Log it
|
||||||
|
_LOGGER.debug("Loaded scenario: %s", self._scenario)
|
||||||
|
|
||||||
|
def get_random_code(self):
|
||||||
|
"""Return a random authorization code."""
|
||||||
|
return "".join(str(random.randint(0, 9)) for _ in range(6))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_discovery_url():
|
||||||
|
"""Return the discovery URL for the given base URL."""
|
||||||
|
return f"{BASE_URL}/.well-known/openid-configuration"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_authorize_url():
|
||||||
|
"""Return the authorization URL for the given base URL."""
|
||||||
|
return f"{BASE_URL}/authorize"
|
||||||
|
|
||||||
|
def process_request(self, url: str, method: str, body: dict) -> tuple[dict, int]:
|
||||||
|
"""Process a request to the mock OIDC server."""
|
||||||
|
_LOGGER.debug("Received %s request to %s in OIDC mock server", method, url)
|
||||||
|
|
||||||
|
if url == self.get_discovery_url() and method == "GET":
|
||||||
|
response = self._get_discovery_document()
|
||||||
|
elif url.startswith(self.get_authorize_url()) and method == "GET":
|
||||||
|
response = self._get_authorize_response(url)
|
||||||
|
elif url == f"{BASE_URL}/token" and method == "POST":
|
||||||
|
response = self._get_token_response(body)
|
||||||
|
elif url == f"{BASE_URL}/jwks" and method == "GET":
|
||||||
|
response = self._get_jwks_response()
|
||||||
|
else:
|
||||||
|
response = {"error": "Unknown endpoint"}, 404
|
||||||
|
|
||||||
|
_LOGGER.debug("Responding with: %s", response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _get_discovery_document(self) -> tuple[dict, int]:
|
||||||
|
"""Return a mock discovery document."""
|
||||||
|
|
||||||
|
if "discovery" in self._scenario:
|
||||||
|
return self._scenario["discovery"], 200
|
||||||
|
|
||||||
|
return {
|
||||||
|
"issuer": BASE_URL,
|
||||||
|
"authorization_endpoint": self.get_authorize_url(),
|
||||||
|
"token_endpoint": f"{BASE_URL}/token",
|
||||||
|
"userinfo_endpoint": f"{BASE_URL}/userinfo",
|
||||||
|
"jwks_uri": f"{BASE_URL}/jwks",
|
||||||
|
"id_token_signing_alg_values_supported": ["RS256"],
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
def _get_authorize_response(self, url: str) -> tuple[dict, int]:
|
||||||
|
"""Return a mock authorization response."""
|
||||||
|
# Parse the url
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
|
||||||
|
code = self.get_random_code()
|
||||||
|
self._code_storage[code] = query_params
|
||||||
|
|
||||||
|
return {"code": code, "state": "xyz"}, 200
|
||||||
|
|
||||||
|
def _get_token_response(self, body: dict) -> tuple[dict, int]:
|
||||||
|
"""Return a mock token response."""
|
||||||
|
|
||||||
|
if body.get("code") in self._code_storage:
|
||||||
|
# TODO: Verify PKCE?
|
||||||
|
return {
|
||||||
|
"access_token": "exampleAccessToken",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"id_token": self._create_id_token(body.get("code")),
|
||||||
|
}, 200
|
||||||
|
else:
|
||||||
|
return {"error": "invalid_request"}, 400
|
||||||
|
|
||||||
|
def _create_id_token(self, code: str) -> str:
|
||||||
|
"""Create a mock ID token."""
|
||||||
|
# Get the query params
|
||||||
|
if code not in self._code_storage:
|
||||||
|
raise ValueError("Invalid code")
|
||||||
|
query_params = self._code_storage[code]
|
||||||
|
_LOGGER.debug("Creating ID token with query params: %s", query_params)
|
||||||
|
|
||||||
|
# Get username
|
||||||
|
if "username" in self._scenario:
|
||||||
|
username = self._scenario["username"]
|
||||||
|
else:
|
||||||
|
username = "testuser"
|
||||||
|
|
||||||
|
# Create a simple signed JWT with our JWK
|
||||||
|
header = {"alg": self._jwk.alg, "kid": self._jwk.kid}
|
||||||
|
claims = {
|
||||||
|
"iss": BASE_URL,
|
||||||
|
"sub": SUBJECT,
|
||||||
|
"aud": query_params.get("client_id", [""])[0],
|
||||||
|
"nonce": query_params.get("nonce", [""])[0],
|
||||||
|
"name": "Test Name",
|
||||||
|
"preferred_username": username,
|
||||||
|
}
|
||||||
|
|
||||||
|
now = int(time.time())
|
||||||
|
claims["nbf"] = now
|
||||||
|
claims["iat"] = now
|
||||||
|
claims["exp"] = now + 3600 # 1 hour expiry
|
||||||
|
|
||||||
|
return jwt.encode(header, claims, self._jwk)
|
||||||
|
|
||||||
|
def _get_jwks_response(self) -> tuple[dict, int]:
|
||||||
|
"""Return a mock JWKS response."""
|
||||||
|
private_key = self._jwk
|
||||||
|
public_key_dict = private_key.as_dict(private=False)
|
||||||
|
public_key = RSAKey.import_key(
|
||||||
|
public_key_dict, {"use": "sig", "alg": "RS256", "kid": private_key.kid}
|
||||||
|
)
|
||||||
|
|
||||||
|
key_set = KeySet([public_key])
|
||||||
|
|
||||||
|
return key_set.as_dict(), 200
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_final_subject():
|
||||||
|
"""Return the subject that's returned to HA."""
|
||||||
|
return hashlib.sha256(f"{BASE_URL}.{SUBJECT}".encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_oidc_responses(scenario: str | None = None):
|
||||||
|
"""Mock OIDC responses for testing."""
|
||||||
|
|
||||||
|
mock_oidc_server = MockOIDCServer(scenario)
|
||||||
|
|
||||||
|
def make_mock_response(json_data, status):
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.__aenter__.return_value = mock_response
|
||||||
|
mock_response.__aexit__.return_value = None
|
||||||
|
mock_response.json = AsyncMock(return_value=json_data)
|
||||||
|
mock_response.status = status
|
||||||
|
return mock_response
|
||||||
|
|
||||||
|
def default_handler(method, url, *args, **kwargs):
|
||||||
|
_LOGGER.debug("Mocked %s request to %s", method, url)
|
||||||
|
body = kwargs.get("data") or kwargs.get("json") or None
|
||||||
|
response = mock_oidc_server.process_request(url, method, body)
|
||||||
|
return make_mock_response(response[0], response[1])
|
||||||
|
|
||||||
|
def get_side_effect(url, *args, **kwargs):
|
||||||
|
return default_handler("GET", url, *args, **kwargs)
|
||||||
|
|
||||||
|
def post_side_effect(url, *args, **kwargs):
|
||||||
|
return default_handler("POST", url, *args, **kwargs)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("aiohttp.ClientSession.get", side_effect=get_side_effect) as get_patch,
|
||||||
|
patch("aiohttp.ClientSession.post", side_effect=post_side_effect) as post_patch,
|
||||||
|
):
|
||||||
|
yield (get_patch, post_patch, default_handler)
|
||||||
5
tests/mocks/scenarios/empty.json
Normal file
5
tests/mocks/scenarios/empty.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
10
tests/mocks/scenarios/invalid_code_challenge_types.json
Normal file
10
tests/mocks/scenarios/invalid_code_challenge_types.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "https://mock-oidc-server.local/jwks",
|
||||||
|
"response_types_supported": ["code"],
|
||||||
|
"code_challenge_methods_supported": ["plain"]
|
||||||
|
}
|
||||||
|
}
|
||||||
10
tests/mocks/scenarios/invalid_grant_types.json
Normal file
10
tests/mocks/scenarios/invalid_grant_types.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "https://mock-oidc-server.local/jwks",
|
||||||
|
"response_types_supported": ["code"],
|
||||||
|
"grant_types_supported": ["refresh_token"]
|
||||||
|
}
|
||||||
|
}
|
||||||
8
tests/mocks/scenarios/invalid_id_token_signing_alg.json
Normal file
8
tests/mocks/scenarios/invalid_id_token_signing_alg.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "https://mock-oidc-server.local/jwks"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tests/mocks/scenarios/invalid_response_modes.json
Normal file
9
tests/mocks/scenarios/invalid_response_modes.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "https://mock-oidc-server.local/jwks",
|
||||||
|
"response_modes_supported": ["post"]
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tests/mocks/scenarios/invalid_response_types.json
Normal file
9
tests/mocks/scenarios/invalid_response_types.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "https://mock-oidc-server.local/jwks",
|
||||||
|
"response_types_supported": ["token"]
|
||||||
|
}
|
||||||
|
}
|
||||||
8
tests/mocks/scenarios/invalid_url.json
Normal file
8
tests/mocks/scenarios/invalid_url.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "/jwks"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
tests/mocks/scenarios/missing_jwks.json
Normal file
7
tests/mocks/scenarios/missing_jwks.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
tests/mocks/scenarios/missing_token.json
Normal file
6
tests/mocks/scenarios/missing_token.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
tests/mocks/scenarios/only_issuer.json
Normal file
5
tests/mocks/scenarios/only_issuer.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
tests/mocks/scenarios/username.json
Normal file
3
tests/mocks/scenarios/username.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"username": "foobar"
|
||||||
|
}
|
||||||
9
tests/mocks/scenarios/wrong_id_token_signing_alg.json
Normal file
9
tests/mocks/scenarios/wrong_id_token_signing_alg.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"discovery": {
|
||||||
|
"issuer": "https://mock-oidc-server.local",
|
||||||
|
"authorization_endpoint": "https://mock-oidc-server.local/authorize",
|
||||||
|
"token_endpoint": "https://mock-oidc-server.local/token",
|
||||||
|
"jwks_uri": "https://mock-oidc-server.local/jwks",
|
||||||
|
"id_token_signing_alg_values_supported": ["HS256"]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
tests/resources/fake_templates/index.html
Normal file
1
tests/resources/fake_templates/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>Example template</p>
|
||||||
90
tests/test_code_store.py
Normal file
90
tests/test_code_store.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""Tests for the code store"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from auth_oidc.stores.code_store import CodeStore
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_code_store_generate_and_receive_code(hass: HomeAssistant):
|
||||||
|
"""Test generating and receiving a code."""
|
||||||
|
store_mock = AsyncMock()
|
||||||
|
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||||
|
code_store = CodeStore(hass)
|
||||||
|
|
||||||
|
# Simulate loading with empty data
|
||||||
|
store_mock.async_load.return_value = {}
|
||||||
|
await code_store.async_load()
|
||||||
|
assert code_store.get_data() == {}
|
||||||
|
|
||||||
|
user_info = {"sub": "user1", "name": "Test User"}
|
||||||
|
code = await code_store.async_generate_code_for_userinfo(user_info)
|
||||||
|
assert code in code_store.get_data()
|
||||||
|
|
||||||
|
# Should return user_info and remove the code
|
||||||
|
with patch("custom_components.auth_oidc.stores.code_store.datetime") as dt_mock:
|
||||||
|
dt_mock.utcnow.return_value = datetime.now(timezone.utc)
|
||||||
|
dt_mock.fromisoformat.side_effect = datetime.fromisoformat
|
||||||
|
result = await code_store.receive_userinfo_for_code(code)
|
||||||
|
assert result == user_info
|
||||||
|
assert code not in code_store.get_data()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_code_store_expired_code(hass):
|
||||||
|
"""Test that expired codes return None."""
|
||||||
|
store_mock = AsyncMock()
|
||||||
|
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||||
|
code_store = CodeStore(hass)
|
||||||
|
store_mock.async_load.return_value = {}
|
||||||
|
await code_store.async_load()
|
||||||
|
assert code_store.get_data() == {}
|
||||||
|
|
||||||
|
user_info = {"sub": "user2", "name": "Expired User"}
|
||||||
|
code = await code_store.async_generate_code_for_userinfo(user_info)
|
||||||
|
|
||||||
|
# Patch expiration to be in the past
|
||||||
|
code_store.get_data()[code]["expiration"] = (
|
||||||
|
datetime.now(timezone.utc) - timedelta(minutes=10)
|
||||||
|
).isoformat()
|
||||||
|
|
||||||
|
with patch("custom_components.auth_oidc.stores.code_store.datetime") as dt_mock:
|
||||||
|
dt_mock.utcnow.return_value = datetime.now(timezone.utc)
|
||||||
|
dt_mock.fromisoformat.side_effect = datetime.fromisoformat
|
||||||
|
result = await code_store.receive_userinfo_for_code(code)
|
||||||
|
assert result is None
|
||||||
|
assert code not in code_store.get_data()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_code_store_data_not_loaded(hass):
|
||||||
|
"""Test that using the store before loading raises RuntimeError."""
|
||||||
|
store_mock = AsyncMock()
|
||||||
|
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||||
|
code_store = CodeStore(hass)
|
||||||
|
|
||||||
|
# Data is not loaded yet, should result in RuntimeError
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
await code_store.async_generate_code_for_userinfo({"sub": "user3"})
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
await code_store.receive_userinfo_for_code("123456")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_code_store_generate_code_length(hass):
|
||||||
|
"""Test that generated codes are 6 digits."""
|
||||||
|
store_mock = AsyncMock()
|
||||||
|
with patch("homeassistant.helpers.storage.Store", return_value=store_mock):
|
||||||
|
code_store = CodeStore(hass)
|
||||||
|
store_mock.async_load.return_value = {}
|
||||||
|
await code_store.async_load()
|
||||||
|
assert code_store.get_data() == {}
|
||||||
|
user_info = {"sub": "user4"}
|
||||||
|
code = await code_store.async_generate_code_for_userinfo(user_info)
|
||||||
|
assert len(code) == 6
|
||||||
|
assert code.isdigit()
|
||||||
229
tests/test_hass_auth_provider.py
Normal file
229
tests/test_hass_auth_provider.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
"""Tests for the Auth Provider registration in HA"""
|
||||||
|
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
|
||||||
|
|
||||||
|
from custom_components.auth_oidc import DOMAIN
|
||||||
|
from custom_components.auth_oidc.config.const import (
|
||||||
|
DISCOVERY_URL,
|
||||||
|
CLIENT_ID,
|
||||||
|
FEATURES,
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION,
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING,
|
||||||
|
)
|
||||||
|
from .mocks.oidc_server import MockOIDCServer, mock_oidc_responses
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(hass: HomeAssistant, config: dict, expect_success: bool) -> bool:
|
||||||
|
"""Set up the auth_oidc component."""
|
||||||
|
result = await async_setup_component(hass, DOMAIN, {DOMAIN: config})
|
||||||
|
|
||||||
|
if expect_success:
|
||||||
|
assert result
|
||||||
|
assert DOMAIN in hass.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_success_auth_provider_registration(hass: HomeAssistant):
|
||||||
|
"""Test successful setup"""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the auth provider is registered
|
||||||
|
auth_providers = hass.auth.get_auth_providers(DOMAIN)
|
||||||
|
assert len(auth_providers) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def login_user(hass: HomeAssistant, code: str):
|
||||||
|
"""Helper to login a user."""
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
flow = await provider.async_login_flow({})
|
||||||
|
|
||||||
|
result = await flow.async_step_init({"code": code})
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] is not None
|
||||||
|
|
||||||
|
data = result["data"]
|
||||||
|
sub = data["sub"]
|
||||||
|
assert sub == MockOIDCServer.get_final_subject()
|
||||||
|
|
||||||
|
# Get credentials
|
||||||
|
credentials = await provider.async_get_or_create_credentials(data)
|
||||||
|
assert credentials is not None
|
||||||
|
assert credentials.data["sub"] == sub
|
||||||
|
|
||||||
|
user = await hass.auth.async_get_or_create_user(credentials)
|
||||||
|
assert user.is_active
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def get_login_code(hass: HomeAssistant, hass_client):
|
||||||
|
"""Helper to get a login code."""
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/redirect", allow_redirects=False)
|
||||||
|
assert resp.status == 302
|
||||||
|
location = resp.headers["Location"]
|
||||||
|
parsed_url = urlparse(location)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
state = query_params["state"][0]
|
||||||
|
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
resp = session.get(location, allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
json_parsed = await resp.json()
|
||||||
|
assert "code" in json_parsed and json_parsed["code"]
|
||||||
|
|
||||||
|
code = json_parsed["code"]
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get(
|
||||||
|
f"/auth/oidc/callback?code={code}&state={state}", allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 302
|
||||||
|
location = resp.headers["Location"]
|
||||||
|
assert "/auth/oidc/finish?code=" in location
|
||||||
|
|
||||||
|
# Get the code from the finish URL
|
||||||
|
code = location.split("code=")[1]
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_full_login(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test a full login flow."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock_oidc_responses():
|
||||||
|
# Actually start the login and get a code
|
||||||
|
code = await get_login_code(hass, hass_client)
|
||||||
|
|
||||||
|
# Use the code to login directly with the registered auth provider
|
||||||
|
# Inspired by tests for the built-in providers
|
||||||
|
user = await login_user(hass, code)
|
||||||
|
assert user.name == "Test Name"
|
||||||
|
|
||||||
|
# Login again to see if we trigger the re-use path
|
||||||
|
code2 = await get_login_code(hass, hass_client)
|
||||||
|
user2 = await login_user(hass, code2)
|
||||||
|
assert user2.id == user.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_with_linking(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test a linking login."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock_oidc_responses("username"):
|
||||||
|
# Create a user first with username 'foobar'
|
||||||
|
user = await hass.auth.async_create_user("Foo Bar")
|
||||||
|
assert user.is_active
|
||||||
|
|
||||||
|
hass_provider = hass.auth.get_auth_providers("homeassistant")[0]
|
||||||
|
credential = await hass_provider.async_get_or_create_credentials(
|
||||||
|
{"username": "foobar"}
|
||||||
|
)
|
||||||
|
await hass.auth.async_link_user(user, credential)
|
||||||
|
|
||||||
|
# Actually start the login and get a code
|
||||||
|
code = await get_login_code(hass, hass_client)
|
||||||
|
|
||||||
|
# Use the code to login directly with the registered auth provider
|
||||||
|
user2 = await login_user(hass, code)
|
||||||
|
assert user2.id == user.id # Assert that the user was linked
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_with_person_create(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test a person create."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: True,
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await async_setup_component(hass, PERSON_DOMAIN, {})
|
||||||
|
|
||||||
|
with mock_oidc_responses():
|
||||||
|
code = await get_login_code(hass, hass_client)
|
||||||
|
user = await login_user(hass, code)
|
||||||
|
assert user.is_active
|
||||||
|
|
||||||
|
# Find the person associated to this user using the PersonRegistry API
|
||||||
|
person_store = hass.data[PERSON_DOMAIN][1]
|
||||||
|
persons = person_store.async_items()
|
||||||
|
assert len(persons) == 1
|
||||||
|
|
||||||
|
person = persons[0]
|
||||||
|
assert person["user_id"] == user.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_shows_form(hass: HomeAssistant):
|
||||||
|
"""Test a login"""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
flow = await provider.async_login_flow({})
|
||||||
|
|
||||||
|
result = await flow.async_step_init({})
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "mfa"
|
||||||
|
|
||||||
|
# Attempt an invalid code
|
||||||
|
result = await flow.async_step_init({"code": "invalid"})
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
287
tests/test_hass_oidc_client.py
Normal file
287
tests/test_hass_oidc_client.py
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
"""Tests for the OIDC client"""
|
||||||
|
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
import pytest
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from auth_oidc import DOMAIN
|
||||||
|
from auth_oidc.tools.oidc_client import OIDCDiscoveryClient, OIDCDiscoveryInvalid
|
||||||
|
from auth_oidc.config.const import (
|
||||||
|
DISCOVERY_URL,
|
||||||
|
CLIENT_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .mocks.oidc_server import MockOIDCServer, mock_oidc_responses
|
||||||
|
|
||||||
|
EXAMPLE_CLIENT_ID = "dummyclient"
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(hass: HomeAssistant):
|
||||||
|
"""Set up the integration within Home Assistant"""
|
||||||
|
mock_config = {
|
||||||
|
DOMAIN: {
|
||||||
|
CLIENT_ID: EXAMPLE_CLIENT_ID,
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await async_setup_component(hass, DOMAIN, mock_config)
|
||||||
|
assert result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_full_oidc_flow(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that one full OIDC flow works if OIDC is mocked."""
|
||||||
|
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
with mock_oidc_responses():
|
||||||
|
# Start by going to /auth/oidc/redirect
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/redirect", allow_redirects=False)
|
||||||
|
assert resp.status == 302
|
||||||
|
assert resp.headers["Location"].startswith(MockOIDCServer.get_authorize_url())
|
||||||
|
|
||||||
|
# Parse the location header and test the query params for correctness
|
||||||
|
location = resp.headers["Location"]
|
||||||
|
parsed_url = urlparse(location)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
|
||||||
|
assert "response_type" in query_params and query_params.get(
|
||||||
|
"response_type"
|
||||||
|
) == ["code"]
|
||||||
|
assert "client_id" in query_params and query_params.get("client_id") == [
|
||||||
|
EXAMPLE_CLIENT_ID
|
||||||
|
]
|
||||||
|
assert "scope" in query_params and query_params.get("scope") == [
|
||||||
|
"openid profile groups"
|
||||||
|
]
|
||||||
|
assert "state" in query_params and query_params["state"]
|
||||||
|
state = query_params["state"][0]
|
||||||
|
assert len(state) >= 16 # Ensure state is sufficiently long
|
||||||
|
assert (
|
||||||
|
"redirect_uri" in query_params
|
||||||
|
and query_params["redirect_uri"]
|
||||||
|
and query_params["redirect_uri"][0].endswith("/auth/oidc/callback")
|
||||||
|
) # TODO: Also test that the URL itself is correct
|
||||||
|
assert "nonce" in query_params and query_params["nonce"]
|
||||||
|
assert "code_challenge_method" in query_params and query_params.get(
|
||||||
|
"code_challenge_method"
|
||||||
|
) == ["S256"]
|
||||||
|
assert "code_challenge" in query_params and query_params["code_challenge"]
|
||||||
|
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
resp = session.get(location, allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
json_parsed = await resp.json()
|
||||||
|
assert "code" in json_parsed and json_parsed["code"]
|
||||||
|
|
||||||
|
# Now go back to the callback with a sample code
|
||||||
|
code = json_parsed["code"]
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get(
|
||||||
|
f"/auth/oidc/callback?code={code}&state={state}", allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Test if logged text contains our login
|
||||||
|
# TODO: Test if the code actually works
|
||||||
|
assert resp.status == 302
|
||||||
|
assert "/auth/oidc/finish?code=" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
async def discovery_test_through_redirect(
|
||||||
|
hass_client, caplog, scenario: str, match_log_line: str
|
||||||
|
):
|
||||||
|
"""Test that discovery document retrieval fails gracefully through redirect endpoint."""
|
||||||
|
with mock_oidc_responses(scenario):
|
||||||
|
# Start by going to /auth/oidc/redirect
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/redirect", allow_redirects=False)
|
||||||
|
|
||||||
|
# Find matching log line
|
||||||
|
assert match_log_line in caplog.text
|
||||||
|
|
||||||
|
# Assert that we get a 200 response with an error message
|
||||||
|
assert resp.status == 200
|
||||||
|
text = await resp.text()
|
||||||
|
assert "Integration is misconfigured, discovery could not be obtained." in text
|
||||||
|
|
||||||
|
|
||||||
|
async def direct_discovery_test(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
scenario: str,
|
||||||
|
match_type: str,
|
||||||
|
match_log_line: str | None = None,
|
||||||
|
):
|
||||||
|
"""Test that discovery document retrieval fails with nice error directly."""
|
||||||
|
with mock_oidc_responses(scenario):
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
client = OIDCDiscoveryClient(
|
||||||
|
MockOIDCServer.get_discovery_url(),
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
"id_token_signing_alg": "RS256",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(OIDCDiscoveryInvalid) as exc_info:
|
||||||
|
await client.fetch_discovery_document()
|
||||||
|
|
||||||
|
assert exc_info.value.type == match_type
|
||||||
|
assert exc_info.value.get_detail_string().startswith("type: " + match_type)
|
||||||
|
|
||||||
|
if match_log_line:
|
||||||
|
assert match_log_line in exc_info.value.get_detail_string()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_discovery_failures(hass: HomeAssistant, hass_client, caplog):
|
||||||
|
"""Test that discovery document retrieval fails gracefully."""
|
||||||
|
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
# Empty scenario
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client, caplog, "empty", "is missing required endpoint: issuer"
|
||||||
|
)
|
||||||
|
await direct_discovery_test(hass, "empty", "missing_endpoint", "endpoint: issuer")
|
||||||
|
|
||||||
|
# Missing authorization_endpoint
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"only_issuer",
|
||||||
|
"is missing required endpoint: authorization_endpoint",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "only_issuer", "missing_endpoint", "endpoint: authorization_endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Missing token_endpoint
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"missing_token",
|
||||||
|
"is missing required endpoint: token_endpoint",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "missing_token", "missing_endpoint", "endpoint: token_endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Missing jwks_uri
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"missing_jwks",
|
||||||
|
"is missing required endpoint: jwks_uri",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "missing_jwks", "missing_endpoint", "endpoint: jwks_uri"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid response_modes_supported
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"invalid_response_modes",
|
||||||
|
"does not support required 'query' response mode, only supports: ['post']",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "invalid_response_modes", "does_not_support_response_mode", "post"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid grant_types supported
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"invalid_grant_types",
|
||||||
|
"does not support required 'authorization_code' grant type, only supports: ['refresh_token']",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "invalid_grant_types", "does_not_support_grant_type", "refresh_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid response types
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"invalid_response_types",
|
||||||
|
"does not support required 'code' response type, only supports: ['token']",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "invalid_response_types", "does_not_support_response_type", "token"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid code_challenge types
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"invalid_code_challenge_types",
|
||||||
|
"does not support required 'S256' code challenge method, only supports: ['plain']",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass,
|
||||||
|
"invalid_code_challenge_types",
|
||||||
|
"does_not_support_required_code_challenge_method",
|
||||||
|
"plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid id_token_signing alg
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"invalid_id_token_signing_alg",
|
||||||
|
"does not have 'id_token_signing_alg_values_supported' field",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass, "invalid_id_token_signing_alg", "missing_id_token_signing_alg_values"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Not matching id_token_signing alg
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"wrong_id_token_signing_alg",
|
||||||
|
"does not support requested id_token_signing_alg 'RS256', only supports: ['HS256']",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass,
|
||||||
|
"wrong_id_token_signing_alg",
|
||||||
|
"does_not_support_id_token_signing_alg",
|
||||||
|
"requested: RS256, supported: ['HS256']",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid URL
|
||||||
|
await discovery_test_through_redirect(
|
||||||
|
hass_client,
|
||||||
|
caplog,
|
||||||
|
"invalid_url",
|
||||||
|
"has invalid URL in endpoint: jwks_uri (/jwks)",
|
||||||
|
)
|
||||||
|
await direct_discovery_test(
|
||||||
|
hass,
|
||||||
|
"invalid_url",
|
||||||
|
"invalid_endpoint",
|
||||||
|
"endpoint: jwks_uri, url: /jwks",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_direct_jwks_fetch(hass: HomeAssistant):
|
||||||
|
"""Test direct fetch of JWKS."""
|
||||||
|
with mock_oidc_responses():
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
client = OIDCDiscoveryClient(
|
||||||
|
MockOIDCServer.get_discovery_url(),
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
"id_token_signing_alg": "RS256",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.fetch_discovery_document()
|
||||||
|
jwks = await client.fetch_jwks()
|
||||||
|
assert "keys" in jwks
|
||||||
364
tests/test_hass_ui_config_flow.py
Normal file
364
tests/test_hass_ui_config_flow.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
"""Tests for the UI config flow"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
|
||||||
|
from custom_components.auth_oidc import DOMAIN, async_setup_entry
|
||||||
|
from custom_components.auth_oidc.config.const import (
|
||||||
|
OIDC_PROVIDERS,
|
||||||
|
CLIENT_ID,
|
||||||
|
CLIENT_SECRET,
|
||||||
|
DISCOVERY_URL,
|
||||||
|
DISPLAY_NAME,
|
||||||
|
FEATURES,
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING,
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION,
|
||||||
|
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||||
|
CLAIMS,
|
||||||
|
CLAIMS_DISPLAY_NAME,
|
||||||
|
CLAIMS_GROUPS,
|
||||||
|
CLAIMS_USERNAME,
|
||||||
|
ROLES,
|
||||||
|
ROLE_ADMINS,
|
||||||
|
ROLE_USERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .mocks.oidc_server import MockOIDCServer, mock_oidc_responses
|
||||||
|
|
||||||
|
DEMO_CLIENT_ID = "testing_example_client_id"
|
||||||
|
DEMO_CLIENT_SECRET = "faz"
|
||||||
|
DEMO_ADMIN_ROLE = "boo"
|
||||||
|
DEMO_USER_ROLE = "far"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_full_config_flow_success(hass: HomeAssistant):
|
||||||
|
"""Test a successful full config flow."""
|
||||||
|
|
||||||
|
with mock_oidc_responses():
|
||||||
|
# 1. Start the user step
|
||||||
|
# This simulates clicking "Add Integration" in the UI.
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that it's a form and expects user input for the 'user' step
|
||||||
|
# 'user' is always the first step if it is user triggered
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
schema = result["data_schema"]
|
||||||
|
# Extract the schema dict from voluptuous Schema
|
||||||
|
schema_dict = schema.schema
|
||||||
|
# Assert 'provider' is a key in the schema
|
||||||
|
assert "provider" in schema_dict
|
||||||
|
# Assert 'authentik' is one of the allowed values for 'provider'
|
||||||
|
provider_field = schema_dict["provider"]
|
||||||
|
# If provider_field is a voluptuous In validator, get its container
|
||||||
|
allowed_providers = getattr(provider_field, "container", None)
|
||||||
|
assert "authentik" in OIDC_PROVIDERS
|
||||||
|
assert allowed_providers is not None and "authentik" in allowed_providers
|
||||||
|
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
# 2. Submit user input for the 'user' step
|
||||||
|
# This simulates the user filling out host/port
|
||||||
|
user_input_step_user = {"provider": "authentik"}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input_step_user
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that it proceeds to the 'auth' step
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "discovery_url"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
# Fill in the discovery URL
|
||||||
|
user_input_step_discovery = {
|
||||||
|
"discovery_url": MockOIDCServer.get_discovery_url()
|
||||||
|
}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input_step_discovery
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that it proceeds to the 'credentials' step
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "validate_connection"
|
||||||
|
|
||||||
|
# Assert that it validates correctly with our mock
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
# Send in continue
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"action": "continue"}
|
||||||
|
)
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "client_config"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
# Fill in the client config
|
||||||
|
user_input_step_client_config = {
|
||||||
|
"client_id": DEMO_CLIENT_ID,
|
||||||
|
"client_secret": DEMO_CLIENT_SECRET,
|
||||||
|
}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input_step_client_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that we are at groups_config
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "groups_config"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
# Fill in the groups config
|
||||||
|
user_input_step_groups_config = {
|
||||||
|
"admin_group": DEMO_ADMIN_ROLE,
|
||||||
|
"user_group": DEMO_USER_ROLE,
|
||||||
|
}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input_step_groups_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that were are at user_linking config
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user_linking"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
# Fill in the user linking config
|
||||||
|
user_input_step_user_linking = {"enable_user_linking": False}
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input_step_user_linking
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finally, assert that the flow is complete and a config entry is created
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == OIDC_PROVIDERS["authentik"]["name"]
|
||||||
|
|
||||||
|
expected_data = {
|
||||||
|
"provider": "authentik",
|
||||||
|
CLIENT_ID: DEMO_CLIENT_ID,
|
||||||
|
CLIENT_SECRET: DEMO_CLIENT_SECRET,
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
DISPLAY_NAME: OIDC_PROVIDERS["authentik"]["name"],
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: True,
|
||||||
|
FEATURES_INCLUDE_GROUPS_SCOPE: True,
|
||||||
|
},
|
||||||
|
CLAIMS: {
|
||||||
|
CLAIMS_DISPLAY_NAME: OIDC_PROVIDERS["authentik"]["claims"][
|
||||||
|
"display_name"
|
||||||
|
],
|
||||||
|
CLAIMS_USERNAME: OIDC_PROVIDERS["authentik"]["claims"]["username"],
|
||||||
|
CLAIMS_GROUPS: OIDC_PROVIDERS["authentik"]["claims"]["groups"],
|
||||||
|
},
|
||||||
|
ROLES: {ROLE_ADMINS: DEMO_ADMIN_ROLE, ROLE_USERS: DEMO_USER_ROLE},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert result["data"] == expected_data
|
||||||
|
|
||||||
|
# Verify that the config entry was loaded into Home Assistant
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].data == expected_data
|
||||||
|
|
||||||
|
# You can also assert that `async_setup_entry` was called for this entry
|
||||||
|
# (assuming it's mocked or you let it run if it's simple)
|
||||||
|
# The PHCC `hass` fixture automatically mocks `async_setup_entry`
|
||||||
|
# and `async_unload_entry` for you, making it easy to test that they're called.
|
||||||
|
assert await async_setup_entry(hass, entries[0]) is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_options_flow_success(hass: HomeAssistant):
|
||||||
|
"""Test a successful options flow."""
|
||||||
|
|
||||||
|
# First, set up an initial config entry as in the full config flow
|
||||||
|
initial_data = {
|
||||||
|
"provider": "authentik",
|
||||||
|
CLIENT_ID: DEMO_CLIENT_ID,
|
||||||
|
CLIENT_SECRET: DEMO_CLIENT_SECRET,
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
DISPLAY_NAME: OIDC_PROVIDERS["authentik"]["name"],
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: True,
|
||||||
|
FEATURES_INCLUDE_GROUPS_SCOPE: True,
|
||||||
|
},
|
||||||
|
CLAIMS: {
|
||||||
|
CLAIMS_DISPLAY_NAME: OIDC_PROVIDERS["authentik"]["claims"]["display_name"],
|
||||||
|
CLAIMS_USERNAME: OIDC_PROVIDERS["authentik"]["claims"]["username"],
|
||||||
|
CLAIMS_GROUPS: OIDC_PROVIDERS["authentik"]["claims"]["groups"],
|
||||||
|
},
|
||||||
|
ROLES: {ROLE_ADMINS: DEMO_ADMIN_ROLE, ROLE_USERS: DEMO_USER_ROLE},
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = config_entries.ConfigEntry(
|
||||||
|
version=1,
|
||||||
|
minor_version=0,
|
||||||
|
domain=DOMAIN,
|
||||||
|
title=OIDC_PROVIDERS["authentik"]["name"],
|
||||||
|
data=initial_data,
|
||||||
|
source=config_entries.SOURCE_USER,
|
||||||
|
entry_id="1",
|
||||||
|
unique_id="test_unique_id",
|
||||||
|
options={},
|
||||||
|
pref_disable_new_entities=False,
|
||||||
|
pref_disable_polling=False,
|
||||||
|
discovery_keys=None,
|
||||||
|
subentries_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_add(entry)
|
||||||
|
|
||||||
|
# Start the reconfigure flow
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
# Should start the options flow
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
|
||||||
|
# Assert that the schema is as expected
|
||||||
|
# Schema contains enable_user_linking, enable_groups, admin_group & user_groups and no other keys
|
||||||
|
schema = result["data_schema"]
|
||||||
|
schema_dict = schema.schema
|
||||||
|
# Assert that the schema contains the expected keys
|
||||||
|
expected_keys = {
|
||||||
|
"admin_group",
|
||||||
|
"enable_user_linking",
|
||||||
|
"enable_groups",
|
||||||
|
"user_group",
|
||||||
|
}
|
||||||
|
assert set(schema_dict.keys()) == expected_keys
|
||||||
|
|
||||||
|
# Change the client_id and client_secret
|
||||||
|
new_enable_linking = True
|
||||||
|
new_enable_groups = True
|
||||||
|
new_admin_group = "bazzbbb"
|
||||||
|
new_user_group = "foobar"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
"enable_user_linking": new_enable_linking,
|
||||||
|
"enable_groups": new_enable_groups,
|
||||||
|
"admin_group": new_admin_group,
|
||||||
|
"user_group": new_user_group,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should finish and update the entry options
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
# Optionally, check that the entry options are updated
|
||||||
|
updated_entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||||
|
assert updated_entry is not None
|
||||||
|
|
||||||
|
# Verify that the config entry was loaded into Home Assistant
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
|
||||||
|
assert (
|
||||||
|
entries[0].data[FEATURES][FEATURES_AUTOMATIC_USER_LINKING] == new_enable_linking
|
||||||
|
)
|
||||||
|
assert entries[0].data[FEATURES][FEATURES_INCLUDE_GROUPS_SCOPE] == new_enable_groups
|
||||||
|
assert entries[0].data[ROLES][ROLE_ADMINS] == new_admin_group
|
||||||
|
assert entries[0].data[ROLES][ROLE_USERS] == new_user_group
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reconfigure_flow_success(hass: HomeAssistant):
|
||||||
|
"""Test a successful reconfigure flow."""
|
||||||
|
|
||||||
|
# First, set up an initial config entry as in the full config flow
|
||||||
|
initial_data = {
|
||||||
|
"provider": "authentik",
|
||||||
|
CLIENT_ID: DEMO_CLIENT_ID,
|
||||||
|
CLIENT_SECRET: DEMO_CLIENT_SECRET,
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
DISPLAY_NAME: OIDC_PROVIDERS["authentik"]["name"],
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
FEATURES_AUTOMATIC_PERSON_CREATION: True,
|
||||||
|
FEATURES_INCLUDE_GROUPS_SCOPE: True,
|
||||||
|
},
|
||||||
|
CLAIMS: {
|
||||||
|
CLAIMS_DISPLAY_NAME: OIDC_PROVIDERS["authentik"]["claims"]["display_name"],
|
||||||
|
CLAIMS_USERNAME: OIDC_PROVIDERS["authentik"]["claims"]["username"],
|
||||||
|
CLAIMS_GROUPS: OIDC_PROVIDERS["authentik"]["claims"]["groups"],
|
||||||
|
},
|
||||||
|
ROLES: {ROLE_ADMINS: DEMO_ADMIN_ROLE, ROLE_USERS: DEMO_USER_ROLE},
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = config_entries.ConfigEntry(
|
||||||
|
version=1,
|
||||||
|
minor_version=0,
|
||||||
|
domain=DOMAIN,
|
||||||
|
title=OIDC_PROVIDERS["authentik"]["name"],
|
||||||
|
data=initial_data,
|
||||||
|
source=config_entries.SOURCE_USER,
|
||||||
|
entry_id="1",
|
||||||
|
unique_id="test_unique_id",
|
||||||
|
options={},
|
||||||
|
pref_disable_new_entities=False,
|
||||||
|
pref_disable_polling=False,
|
||||||
|
discovery_keys=None,
|
||||||
|
subentries_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_add(entry)
|
||||||
|
|
||||||
|
# Start async_step_reconfigure to reconfigure the entry
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={
|
||||||
|
"source": config_entries.SOURCE_RECONFIGURE,
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should start the reconfigure flow
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "reconfigure"
|
||||||
|
assert result["data_schema"] is not None
|
||||||
|
|
||||||
|
# Assert that the schema is client_id & client_secret
|
||||||
|
schema = result["data_schema"]
|
||||||
|
schema_dict = schema.schema
|
||||||
|
# Assert that the schema contains the expected keys
|
||||||
|
expected_keys = {
|
||||||
|
"client_id",
|
||||||
|
"client_secret",
|
||||||
|
}
|
||||||
|
assert set(schema_dict.keys()) == expected_keys
|
||||||
|
|
||||||
|
# Change the client_id and client_secret
|
||||||
|
new_client_id = "newclientid"
|
||||||
|
new_client_secret = "newclientsecret"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
"client_id": new_client_id,
|
||||||
|
"client_secret": new_client_secret,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should finish and update the entry data
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reconfigure_successful"
|
||||||
|
|
||||||
|
# Verify that the config entry was loaded into Home Assistant
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0].data[CLIENT_ID] == new_client_id
|
||||||
|
assert entries[0].data[CLIENT_SECRET] == new_client_secret
|
||||||
151
tests/test_hass_webserver.py
Normal file
151
tests/test_hass_webserver.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""Tests for the registered webpages"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from auth_oidc.config.const import (
|
||||||
|
DISCOVERY_URL,
|
||||||
|
CLIENT_ID,
|
||||||
|
FEATURES,
|
||||||
|
FEATURES_DISABLE_FRONTEND_INJECTION,
|
||||||
|
)
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.components.http import StaticPathConfig, DOMAIN as HTTP_DOMAIN
|
||||||
|
|
||||||
|
from custom_components.auth_oidc import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(hass: HomeAssistant, enable_frontend_changes: bool = None):
|
||||||
|
mock_config = {
|
||||||
|
DOMAIN: {
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
||||||
|
FEATURES: {
|
||||||
|
FEATURES_DISABLE_FRONTEND_INJECTION: not enable_frontend_changes
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if enable_frontend_changes is None:
|
||||||
|
del mock_config[DOMAIN][FEATURES][FEATURES_DISABLE_FRONTEND_INJECTION]
|
||||||
|
|
||||||
|
result = await async_setup_component(hass, DOMAIN, mock_config)
|
||||||
|
assert result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_welcome_page_registration(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that welcome page is present if frontend changes are disabled."""
|
||||||
|
|
||||||
|
await setup(hass, enable_frontend_changes=False)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/welcome", allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_welcome_page_registration_with_changes(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that welcome page is redirect if frontend changes are enabled."""
|
||||||
|
|
||||||
|
await setup(hass, enable_frontend_changes=True)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/welcome", allow_redirects=False)
|
||||||
|
assert resp.status == 307
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_redirect_page_registration(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that redirect page shows OIDC misconfiguration error if OIDC server is not reachable."""
|
||||||
|
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/redirect", allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
text = await resp.text()
|
||||||
|
assert "Integration is misconfigured" in text
|
||||||
|
|
||||||
|
resp2 = await client.post("/auth/oidc/redirect", allow_redirects=False)
|
||||||
|
assert resp2.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_callback_registration(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that callback page is reachable."""
|
||||||
|
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/callback", allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_finish_registration(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that finish page is reachable."""
|
||||||
|
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/finish", allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
text = await resp.text()
|
||||||
|
|
||||||
|
# Should miss the code parameter if called without it
|
||||||
|
assert "Missing code" in text
|
||||||
|
|
||||||
|
resp2 = await client.get("/auth/oidc/finish?code=123456", allow_redirects=False)
|
||||||
|
assert resp2.status == 200
|
||||||
|
text2 = await resp2.text()
|
||||||
|
assert "Missing code" not in text2
|
||||||
|
assert "123456" in text2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_finish_post(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that finish page works with POST."""
|
||||||
|
|
||||||
|
await setup(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.post("/auth/oidc/finish", data={}, allow_redirects=False)
|
||||||
|
assert resp.status == 500
|
||||||
|
|
||||||
|
resp2 = await client.post(
|
||||||
|
"/auth/oidc/finish", data={"code": "456888"}, allow_redirects=False
|
||||||
|
)
|
||||||
|
assert resp2.status == 302
|
||||||
|
assert resp2.headers["Location"] == "/?storeToken=true"
|
||||||
|
assert resp2.cookies["auth_oidc_code"].value == "456888"
|
||||||
|
|
||||||
|
|
||||||
|
# Test the frontend injection
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_frontend_injection(hass: HomeAssistant, hass_client):
|
||||||
|
"""Test that frontend injection works."""
|
||||||
|
|
||||||
|
# Because there is no frontend in the test setup,
|
||||||
|
# we'll have to fake /auth/authorize for the changes to register
|
||||||
|
await async_setup_component(hass, HTTP_DOMAIN, {})
|
||||||
|
|
||||||
|
mock_html_path = os.path.join(os.path.dirname(__file__), "mocks", "auth_page.html")
|
||||||
|
await hass.http.async_register_static_paths(
|
||||||
|
[
|
||||||
|
StaticPathConfig(
|
||||||
|
"/auth/authorize",
|
||||||
|
mock_html_path,
|
||||||
|
cache_headers=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await setup(hass, enable_frontend_changes=True)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/authorize", allow_redirects=False)
|
||||||
|
assert resp.status == 200
|
||||||
|
text = await resp.text()
|
||||||
|
|
||||||
|
assert "<script src='/auth/oidc/static/injection.js" in text
|
||||||
93
tests/test_hass_yaml_init.py
Normal file
93
tests/test_hass_yaml_init.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""Tests for the YAML config setup of OIDC"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from custom_components.auth_oidc import DOMAIN
|
||||||
|
from custom_components.auth_oidc.config.const import ADDITIONAL_SCOPES
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(hass: HomeAssistant, config: dict, expect_success: bool) -> bool:
|
||||||
|
"""Set up the auth_oidc component."""
|
||||||
|
result = await async_setup_component(hass, DOMAIN, {DOMAIN: config})
|
||||||
|
|
||||||
|
if expect_success:
|
||||||
|
assert result
|
||||||
|
assert DOMAIN in hass.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_success_yaml(hass: HomeAssistant):
|
||||||
|
"""Test successful setup of a YAML configuration."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"client_id": "dummy",
|
||||||
|
"discovery_url": "https://example.com/.well-known/openid-configuration",
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_success_yaml_with_optional(hass: HomeAssistant):
|
||||||
|
"""Test successful setup of a YAML configuration with optional parameters."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"client_id": "dummy",
|
||||||
|
"discovery_url": "https://example.com/.well-known/openid-configuration",
|
||||||
|
ADDITIONAL_SCOPES: "email phone",
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_failure_empty_yaml(hass: HomeAssistant, caplog):
|
||||||
|
"""Test failure setup of an empty YAML configuration."""
|
||||||
|
await setup(hass, {}, False)
|
||||||
|
|
||||||
|
assert "required key 'client_id' not provided" in caplog.text
|
||||||
|
assert "required key 'discovery_url' not provided" in caplog.text
|
||||||
|
assert (
|
||||||
|
"Setup failed for custom integration 'auth_oidc': Invalid config."
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_failure_partial_empty_yaml_discovery(hass: HomeAssistant, caplog):
|
||||||
|
"""Test failure setup of an partial YAML configuration."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{"discovery_url": "https://example.com/.well-known/openid-configuration"},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "required key 'client_id' not provided" in caplog.text
|
||||||
|
assert "required key 'discovery_url' not provided" not in caplog.text
|
||||||
|
assert (
|
||||||
|
"Setup failed for custom integration 'auth_oidc': Invalid config."
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_setup_failure_partial_empty_yaml_client(hass: HomeAssistant, caplog):
|
||||||
|
"""Test failure setup of an partial YAML configuration."""
|
||||||
|
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{"client_id": "test"},
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "required key 'client_id' not provided" not in caplog.text
|
||||||
|
assert "required key 'discovery_url' not provided" in caplog.text
|
||||||
|
assert (
|
||||||
|
"Setup failed for custom integration 'auth_oidc': Invalid config."
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
85
tests/test_helpers.py
Normal file
85
tests/test_helpers.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Tests for the helpers and validation tools"""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.test_utils import make_mocked_request
|
||||||
|
|
||||||
|
from custom_components.auth_oidc.tools.helpers import get_url, get_view
|
||||||
|
from custom_components.auth_oidc.tools.validation import (
|
||||||
|
validate_client_id,
|
||||||
|
sanitize_client_secret,
|
||||||
|
validate_discovery_url,
|
||||||
|
validate_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_url():
|
||||||
|
"""Test the get_url helper."""
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as excinfo:
|
||||||
|
get_url("https://example.com", "/test")
|
||||||
|
assert str(excinfo.value) == "No current request in context"
|
||||||
|
|
||||||
|
# Mock homeassistant.components.http.current_request.get() to test the force HTTP flag
|
||||||
|
with patch("homeassistant.components.http.current_request") as mock_current_request:
|
||||||
|
fake_request = make_mocked_request("GET", "http://example.com")
|
||||||
|
mock_current_request.get.return_value = fake_request
|
||||||
|
result = get_url("/test", True)
|
||||||
|
assert result == "https://example.com/test"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_view():
|
||||||
|
"""Test the get_view helper."""
|
||||||
|
|
||||||
|
data = await get_view("welcome")
|
||||||
|
assert data.startswith("<!DOCTYPE html>")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_url():
|
||||||
|
"""Test the validate_url helper."""
|
||||||
|
|
||||||
|
assert not validate_url("ftp://example.com")
|
||||||
|
assert validate_url("http://example.com")
|
||||||
|
assert validate_url("https://example.com")
|
||||||
|
assert not validate_url("example.com")
|
||||||
|
assert not validate_url(42)
|
||||||
|
assert not validate_url([])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_discovery_url():
|
||||||
|
"""Test the validate_discovery_url helper."""
|
||||||
|
|
||||||
|
assert not validate_discovery_url("ftp://example.com")
|
||||||
|
assert not validate_discovery_url("http://example.com")
|
||||||
|
assert not validate_discovery_url("https://example.com")
|
||||||
|
assert not validate_discovery_url("example.com")
|
||||||
|
assert not validate_discovery_url(
|
||||||
|
"https://example.com/.well-known/openid_configuration"
|
||||||
|
)
|
||||||
|
assert validate_discovery_url(
|
||||||
|
"https://example.com/.well-known/openid-configuration"
|
||||||
|
)
|
||||||
|
assert not validate_discovery_url(2)
|
||||||
|
assert not validate_discovery_url([])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_secret():
|
||||||
|
"""Test the sanitize_client_secret helper."""
|
||||||
|
|
||||||
|
assert sanitize_client_secret("test ") == "test"
|
||||||
|
assert sanitize_client_secret("test2") == "test2"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_id():
|
||||||
|
"""Test the validate_client_id helper."""
|
||||||
|
|
||||||
|
assert not validate_client_id(" ")
|
||||||
|
assert validate_client_id("test4")
|
||||||
|
assert validate_client_id("test4 ")
|
||||||
49
tests/test_view_template.py
Normal file
49
tests/test_view_template.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""Tests for the view templates"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
from custom_components.auth_oidc.views.loader import AsyncTemplateRenderer
|
||||||
|
|
||||||
|
FAKE_TEMPLATE_PATH = path.join(
|
||||||
|
path.dirname(path.abspath(__file__)), "resources", "fake_templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_real_template_render():
|
||||||
|
"""Test that view template can render an real existing template."""
|
||||||
|
|
||||||
|
renderer = AsyncTemplateRenderer()
|
||||||
|
rendered = await renderer.render_template("welcome.html")
|
||||||
|
assert "<!DOCTYPE html>" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fake_template_render():
|
||||||
|
"""Test that view template can render an fake existing template."""
|
||||||
|
|
||||||
|
renderer = AsyncTemplateRenderer(template_dir=FAKE_TEMPLATE_PATH)
|
||||||
|
await renderer.fetch_templates()
|
||||||
|
rendered = await renderer.render_template("index.html")
|
||||||
|
assert "<p>Example template</p>" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dir_render_error():
|
||||||
|
"""Test that view template sends correct error if you try to render directory."""
|
||||||
|
|
||||||
|
renderer = AsyncTemplateRenderer(template_dir=FAKE_TEMPLATE_PATH)
|
||||||
|
await renderer.fetch_templates()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await renderer.render_template("folder.html")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_random_render_error():
|
||||||
|
"""Test that view template sends correct error if you try to render non-existing."""
|
||||||
|
|
||||||
|
renderer = AsyncTemplateRenderer(template_dir=FAKE_TEMPLATE_PATH)
|
||||||
|
await renderer.fetch_templates()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await renderer.render_template("non_existing.html")
|
||||||
644
uv.lock
generated
644
uv.lock
generated
@@ -50,16 +50,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohasupervisor"
|
name = "aiohasupervisor"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
{ name = "mashumaro" },
|
{ name = "mashumaro" },
|
||||||
{ name = "orjson" },
|
{ name = "orjson" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/13/e9c818c4be157db6383d6e0f5c56df4764553c241bc566cd819f42bd398c/aiohasupervisor-0.3.2.tar.gz", hash = "sha256:eb291b600cc5cf05072e4bd16df5655cfaea3b9f3b964844896b38230e529a7c", size = 42599, upload-time = "2025-08-26T14:47:47.357Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/e0/f8865efa28ce22e44e3526f18654c7a69a6f0d0e8523e2aaf743f2798fd8/aiohasupervisor-0.3.3.tar.gz", hash = "sha256:24e268f58f37f9d8dafadba2ef9d860292ff622bc6e78b1ca4ef5e5095d1bbc8", size = 44696, upload-time = "2025-10-01T14:55:57.98Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/e4/6f62ce34142558cb748c20976dfd8696b8bb4ed71653d43795f3de26a07d/aiohasupervisor-0.3.2-py3-none-any.whl", hash = "sha256:93599f698e7daf238e8040053d455104bac149f9e59fda757c6378708fbd1629", size = 39220, upload-time = "2025-08-26T14:47:46.262Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/97/b811d22148e7227e6f02a1f0f13f60d959bb163c806feab853544da07c3e/aiohasupervisor-0.3.3-py3-none-any.whl", hash = "sha256:bc185dbb81bb8ec6ba91b5512df7fd3bf99db15e648b20aed3f8ce7dc3203f1f", size = 40486, upload-time = "2025-10-01T14:55:56.52Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -167,6 +167,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotatedyaml"
|
name = "annotatedyaml"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -454,6 +463,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/67/bb/19f2928dd9b4d27a74349edc687999c00d9694ff4ca19cf14f44f7548654/bluetooth_data_tools-1.28.2-cp313-cp313-win_amd64.whl", hash = "sha256:e748587be85a8133b0a43e34e2c6f65dbf5113765a03d4f89c26039b8289decb", size = 285881, upload-time = "2025-07-02T03:21:49.356Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/bb/19f2928dd9b4d27a74349edc687999c00d9694ff4ca19cf14f44f7548654/bluetooth_data_tools-1.28.2-cp313-cp313-win_amd64.whl", hash = "sha256:e748587be85a8133b0a43e34e2c6f65dbf5113765a03d4f89c26039b8289decb", size = 285881, upload-time = "2025-07-02T03:21:49.356Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boolean-py"
|
||||||
|
version = "5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.40.40"
|
version = "1.40.40"
|
||||||
@@ -545,9 +563,19 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ciso8601"
|
name = "ciso8601"
|
||||||
version = "2.3.2"
|
version = "2.3.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/09/e9/d83711081c997540aee59ad2f49d81f01d33e8551d766b0ebde346f605af/ciso8601-2.3.2.tar.gz", hash = "sha256:ec1616969aa46c51310b196022e5d3926f8d3fa52b80ec17f6b4133623bd5434", size = 28214, upload-time = "2024-12-09T12:26:40.768Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/075724aea06c98626109bfd670c27c248c87b9ba33e637f069bf46e8c4c3/ciso8601-2.3.3.tar.gz", hash = "sha256:db5d78d9fb0de8686fbad1c1c2d168ed52efb6e8bf8774ae26226e5034a46dae", size = 31909, upload-time = "2025-08-20T16:31:33.51Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/16/88154fe8247e4dcfdbaed8c6b8ccf32b1dd4389c6c95b1986bf31649eb00/ciso8601-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8afa073802c926c3244e1e5fcc5818afd3acb90fb7826a90f91ddbda0636ea70", size = 16109, upload-time = "2025-08-20T16:30:45.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/46/8d46372b3802c7201c20c8b316569f27253aaafba0cdd2cd033985e8b77e/ciso8601-2.3.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8a04e518b4adf8e35e030feaecdb4a835d39b9bb44d207e926aea8ce3447ad7c", size = 24189, upload-time = "2025-08-20T16:30:46.958Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/80/1890e097cb76e41995de82f29c0289ca590d7135e0be3707e5b78f54350d/ciso8601-2.3.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f79ad8372463ba4265981016d1648bc05f4922bc8044c4243fcbaef7a12ee9f7", size = 15925, upload-time = "2025-08-20T16:30:48.082Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/e9/690a2a6beefd9d982c20adde3f09ff54a23291a699b0df7cf0c59027d9cf/ciso8601-2.3.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5894a33f119b5ac1082df187dc58c74fe13c9c092e19ba36495c2b7cee3540b", size = 41352, upload-time = "2025-08-20T16:30:49.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/34/9a498ceb0ebd23f538e6685721c9fc4666701372c651874ed22ec46b1423/ciso8601-2.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09deebf3e326ec59d80019b4ad35175c90b99cde789c644b1496811fe3340587", size = 41866, upload-time = "2025-08-20T16:30:50.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/0a/ee0981502aa1c9f28f7e89cf6cee08bdff2c6ed9d4289b00cceb8a1c500e/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3aa43ed59b2117baccc5bb760e5e53dad77cacba671d757c1e82e0a367b1f42a", size = 41271, upload-time = "2025-08-20T16:30:51.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/65/24a888240324188d8350bc24fb58a6d759c0ca43adfa77210f3d60370b56/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:289515aa3a3b86a9c3450bf482f634138b98788332d136751507bfdfe46e6031", size = 41411, upload-time = "2025-08-20T16:30:52.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/1f/febc9de191acb461e02e616e5366bc2b7757277a11b4bf215d4fb79516a8/ciso8601-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:e7288068a5bffbcc50cbe9cdaf3971f541fcd209c194fa6a59ad06066a3dcff0", size = 17573, upload-time = "2025-08-20T16:30:53.759Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
@@ -558,6 +586,37 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.10.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cronsim"
|
name = "cronsim"
|
||||||
version = "2.6"
|
version = "2.6"
|
||||||
@@ -569,37 +628,37 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "45.0.3"
|
version = "45.0.7"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239, upload-time = "2025-05-25T14:16:12.22Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" },
|
{ url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" },
|
{ url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752, upload-time = "2025-05-25T14:16:32.204Z" },
|
{ url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465, upload-time = "2025-05-25T14:16:33.888Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892, upload-time = "2025-05-25T14:16:36.214Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" },
|
{ url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070, upload-time = "2025-05-25T14:16:53.472Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005, upload-time = "2025-05-25T14:16:55.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -647,6 +706,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" },
|
{ url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "execnet"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv-hash-fast"
|
name = "fnv-hash-fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -677,6 +745,18 @@ version = "0.1.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/01/14ef74ea03ac12e8a80d43bbad5356ae809b125cd2072766e459bcc7d388/fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e", size = 1902, upload-time = "2015-11-28T12:21:00.722Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2f/01/14ef74ea03ac12e8a80d43bbad5356ae809b125cd2072766e459bcc7d388/fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e", size = 1902, upload-time = "2015-11-28T12:21:00.722Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "freezegun"
|
||||||
|
version = "1.5.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/75/0455fa5029507a2150da59db4f165fbc458ff8bb1c4f4d7e8037a14ad421/freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181", size = 34855, upload-time = "2025-05-24T12:38:47.051Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/b2/68d4c9b6431121b6b6aa5e04a153cac41dcacc79600ed6e2e7c3382156f5/freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b", size = 18715, upload-time = "2025-05-24T12:38:45.274Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "frozenlist"
|
name = "frozenlist"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
@@ -720,6 +800,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "go2rtc-client"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "awesomeversion" },
|
||||||
|
{ name = "mashumaro" },
|
||||||
|
{ name = "orjson" },
|
||||||
|
{ name = "webrtc-models" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/96/0598fce9e3167ded882580a3941bd9698d525aa1cb950c102cd786e6df71/go2rtc_client-0.2.1.tar.gz", hash = "sha256:9b60e22a0f554c39f30b92b4abaf68efe41e6942aa2f25900efae7798c9747a8", size = 13199, upload-time = "2025-06-02T17:35:57.447Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/4b/21e4de5458a68f14368ff37af821fd15867b7d168135d840818089691ed4/go2rtc_client-0.2.1-py3-none-any.whl", hash = "sha256:de767a6608cecfc69cd44f54db649af0233bc92ab5c6b408d6b60515b622c620", size = 13249, upload-time = "2025-06-02T17:35:56.329Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.2.4"
|
version = "3.2.4"
|
||||||
@@ -808,6 +904,7 @@ dependencies = [
|
|||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
|
{ name = "joserfc" },
|
||||||
{ name = "python-jose" },
|
{ name = "python-jose" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -815,6 +912,10 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "homeassistant" },
|
{ name = "homeassistant" },
|
||||||
{ name = "pylint" },
|
{ name = "pylint" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-homeassistant-custom-component" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -823,6 +924,7 @@ requires-dist = [
|
|||||||
{ name = "aiofiles", specifier = "~=24.1" },
|
{ name = "aiofiles", specifier = "~=24.1" },
|
||||||
{ name = "bcrypt", specifier = "~=4.2" },
|
{ name = "bcrypt", specifier = "~=4.2" },
|
||||||
{ name = "jinja2", specifier = "~=3.1" },
|
{ name = "jinja2", specifier = "~=3.1" },
|
||||||
|
{ name = "joserfc", specifier = ">=1.3.4" },
|
||||||
{ name = "python-jose", specifier = "~=3.5.0" },
|
{ name = "python-jose", specifier = "~=3.5.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -830,6 +932,10 @@ requires-dist = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "homeassistant", specifier = "~=2025.8" },
|
{ name = "homeassistant", specifier = "~=2025.8" },
|
||||||
{ name = "pylint", specifier = "~=3.3" },
|
{ name = "pylint", specifier = "~=3.3" },
|
||||||
|
{ name = "pytest", specifier = ">=8.4.2" },
|
||||||
|
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||||
|
{ name = "pytest-homeassistant-custom-component", specifier = ">=0.13.286" },
|
||||||
{ name = "ruff", specifier = "~=0.12" },
|
{ name = "ruff", specifier = "~=0.12" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -847,7 +953,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2025.9.4"
|
version = "2025.10.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiodns" },
|
{ name = "aiodns" },
|
||||||
@@ -901,9 +1007,9 @@ dependencies = [
|
|||||||
{ name = "yarl" },
|
{ name = "yarl" },
|
||||||
{ name = "zeroconf" },
|
{ name = "zeroconf" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/78/39/2dc9cce6c04415611992849af34fe016e871d3d2c270d955085102a68efa/homeassistant-2025.9.4.tar.gz", hash = "sha256:9aad9c8dcb7862b51157f4f9d70fec92e93012c515e22c03a61b8e7ed14e51dc", size = 27876892, upload-time = "2025-09-19T21:26:36.902Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/20/6f/08b3b45f9303a8c939ad06086e411d533e7e5af99e4f7c5e3ad2ba06dfde/homeassistant-2025.10.1.tar.gz", hash = "sha256:71501154b83cd30564aee49e77bcb0ae72be96eb4b59b3092f630123001aa410", size = 28363358, upload-time = "2025-10-03T19:14:49.43Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/8f/9b2b7968ef652cc63d746d1067b9b6a993c9677f4d1d65632d02dd2105a2/homeassistant-2025.9.4-py3-none-any.whl", hash = "sha256:d4ba9473cbc3da8458d7dddb5c2dd823796a3c6bbad75a092240be2ae8944cbd", size = 47043145, upload-time = "2025-09-19T21:26:31.1Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/ee/52e40d633dd0885ebed591dae1147d82783d7bdde8614a8fa1ff05f8cb68/homeassistant-2025.10.1-py3-none-any.whl", hash = "sha256:e470f1f5f63b9e3d1503b997c352af2fd2b1180c374d072a75ab2efd7c86664b", size = 47731513, upload-time = "2025-10-03T19:14:43.394Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -952,6 +1058,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "6.0.1"
|
version = "6.0.1"
|
||||||
@@ -994,6 +1109,30 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/47/0e/e248f059986e376b87e546cd840d244370b9f7ef2b336456f0c2cfb2fef0/josepy-2.1.0-py3-none-any.whl", hash = "sha256:0eadf09b96821bdae9a8b14145425cb9fe0bbee64c6fdfce3ccd4ceb7d7efbbd", size = 29065, upload-time = "2025-07-08T17:20:53.504Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/0e/e248f059986e376b87e546cd840d244370b9f7ef2b336456f0c2cfb2fef0/josepy-2.1.0-py3-none-any.whl", hash = "sha256:0eadf09b96821bdae9a8b14145425cb9fe0bbee64c6fdfce3ccd4ceb7d7efbbd", size = 29065, upload-time = "2025-07-08T17:20:53.504Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "joserfc"
|
||||||
|
version = "1.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/79/d63a882c0212e95f3ba8863a115e5ca9a5d39413b02273ddce72058e717f/joserfc-1.3.4.tar.gz", hash = "sha256:67d8413c501c239f65eefad5ae685cfbfc401aa63289fc409ef7cc331b007227", size = 197787, upload-time = "2025-09-21T15:49:34.7Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/78/beb171f0caac81f51695f43c87c0c20b3ce2a190cf74a3fbb0b9afe03b45/joserfc-1.3.4-py3-none-any.whl", hash = "sha256:30c845c58d441cfe32d08ac35e437812481ca8155373873b7abf80224bf601c0", size = 75638, upload-time = "2025-09-21T15:49:33.14Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "license-expression"
|
||||||
|
version = "30.4.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "boolean-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/79/efb4637d56dcd265cb9329ab502be0e01f4daed80caffdc5065b4b7956df/license_expression-30.4.3.tar.gz", hash = "sha256:49f439fea91c4d1a642f9f2902b58db1d42396c5e331045f41ce50df9b40b1f2", size = 183031, upload-time = "2025-06-25T13:02:25.76Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/ba/f6f6573bb21e51b838f1e7b0e8ef831d50db6d0530a5afaba700a34d9e12/license_expression-30.4.3-py3-none-any.whl", hash = "sha256:fd3db53418133e0eef917606623bc125fbad3d1225ba8d23950999ee87c99280", size = 117085, upload-time = "2025-06-25T13:02:24.503Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-dict"
|
name = "lru-dict"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -1051,6 +1190,12 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
|
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mock-open"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/02/cef85a80ff6d3092a458448c46816656d1c532afd45aeeeb8f50a84aed35/mock-open-1.4.0.tar.gz", hash = "sha256:c3ecb6b8c32a5899a4f5bf4495083b598b520c698bba00e1ce2ace6e9c239100", size = 12127, upload-time = "2020-04-15T15:26:51.234Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.6.4"
|
version = "6.6.4"
|
||||||
@@ -1096,6 +1241,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orjson"
|
name = "orjson"
|
||||||
version = "3.11.3"
|
version = "3.11.3"
|
||||||
@@ -1128,6 +1303,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paho-mqtt"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "11.3.0"
|
version = "11.3.0"
|
||||||
@@ -1161,6 +1345,28 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pip"
|
||||||
|
version = "25.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/20/16/650289cd3f43d5a2fadfd98c68bd1e1e7f2550a1a5326768cddfbcedb2c5/pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", size = 1840021, upload-time = "2025-07-30T21:50:15.401Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa/pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717", size = 1752557, upload-time = "2025-07-30T21:50:13.323Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pipdeptree"
|
||||||
|
version = "2.26.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pip" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/74/ef/9158ee3b28274667986d39191760c988a2de22c6321be1262e21c8a19ccf/pipdeptree-2.26.1.tar.gz", hash = "sha256:92a8f37ab79235dacb46af107e691a1309ca4a429315ba2a1df97d1cd56e27ac", size = 41024, upload-time = "2025-04-20T03:27:42.489Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/a5/f9f143b420e53a296869636d1c3bdc144be498ca3136a113f52b53ea2b02/pipdeptree-2.26.1-py3-none-any.whl", hash = "sha256:3849d62a2ed641256afac3058c4f9b85ac4a47e9d8c991ee17a8f3d230c5cffb", size = 32802, upload-time = "2025-04-20T03:27:40.413Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
@@ -1170,6 +1376,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "propcache"
|
name = "propcache"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -1296,6 +1511,58 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.11.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.33.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyjwt"
|
name = "pyjwt"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
@@ -1328,6 +1595,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/2d/1a/711e93a7ab6c392e349428ea56e794a3902bb4e0284c1997cff2d7efdbc1/pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83", size = 523153, upload-time = "2025-08-09T09:12:54.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/1a/711e93a7ab6c392e349428ea56e794a3902bb4e0284c1997cff2d7efdbc1/pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83", size = 523153, upload-time = "2025-08-09T09:12:54.836Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylint-per-file-ignores"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a5/3d/21bec2f2f432519616c34a64ba0766ef972fdfb6234a86bb1b8baf4b0c7c/pylint_per_file_ignores-1.4.0.tar.gz", hash = "sha256:c0de7b3d0169571aefaa1ac3a82a265641b8825b54a0b6f5ef27c3b76b988609", size = 4419, upload-time = "2025-01-17T21:35:02.383Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/0e/bf3473d86648a17e6dd6ee9e6abce526b077169031177f4f2031368f864a/pylint_per_file_ignores-1.4.0-py3-none-any.whl", hash = "sha256:0cd82d22551738b4e63a0aa1dab2a1fc4016e8f27f1429159616483711e122fd", size = 4888, upload-time = "2025-01-17T21:35:00.371Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyobjc-core"
|
name = "pyobjc-core"
|
||||||
version = "11.1"
|
version = "11.1"
|
||||||
@@ -1406,6 +1682,201 @@ version = "0.1.6.3"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-aiohttp"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/4b/d326890c153f2c4ce1bf45d07683c08c10a1766058a22934620bc6ac6592/pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc", size = 12842, upload-time = "2025-01-23T12:44:04.465Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/0f/e6af71c02e0f1098eaf7d2dbf3ffdf0a69fc1e0ef174f96af05cef161f1b/pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d", size = 8932, upload-time = "2025-01-23T12:44:03.27Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cov"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-freezer"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "freezegun" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/81/f0/98dcbc5324064360b19850b14c84cea9ca50785d921741dbfc442346e925/pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a", size = 3177, upload-time = "2024-12-12T08:53:08.684Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/e9/30252bc05bcf67200a17f4f0b4cc7598f0a68df4fa9fa356193aa899f145/pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59", size = 3192, upload-time = "2024-12-12T08:53:07.641Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-github-actions-annotate-failures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/39/d4/c54ee6a871eee4a7468e3a8c0dead28e634c0bc2110c694309dcb7563a66/pytest_github_actions_annotate_failures-0.3.0.tar.gz", hash = "sha256:d4c3177c98046c3900a7f8ddebb22ea54b9f6822201b5d3ab8fcdea51e010db7", size = 11248, upload-time = "2025-01-17T22:39:32.722Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/73/7b0b15cb8605ee967b34aa1d949737ab664f94e6b0f1534e8339d9e64ab2/pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf", size = 6030, upload-time = "2025-01-17T22:39:31.701Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-homeassistant-custom-component"
|
||||||
|
version = "0.13.286"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "freezegun" },
|
||||||
|
{ name = "go2rtc-client" },
|
||||||
|
{ name = "homeassistant" },
|
||||||
|
{ name = "license-expression" },
|
||||||
|
{ name = "mock-open" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "paho-mqtt" },
|
||||||
|
{ name = "pipdeptree" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pylint-per-file-ignores" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-aiohttp" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-freezer" },
|
||||||
|
{ name = "pytest-github-actions-annotate-failures" },
|
||||||
|
{ name = "pytest-picked" },
|
||||||
|
{ name = "pytest-socket" },
|
||||||
|
{ name = "pytest-sugar" },
|
||||||
|
{ name = "pytest-timeout" },
|
||||||
|
{ name = "pytest-unordered" },
|
||||||
|
{ name = "pytest-xdist" },
|
||||||
|
{ name = "requests-mock" },
|
||||||
|
{ name = "respx" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
{ name = "syrupy" },
|
||||||
|
{ name = "tqdm" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/db/06/b6bae30653afd7515c2afd8a36501d5a1d7241a2fa71a275003f1fbbdda3/pytest_homeassistant_custom_component-0.13.286.tar.gz", hash = "sha256:27617cc656cef55f89c04c51127758dceecace3d8024037158385504cd9b00d1", size = 60199, upload-time = "2025-10-04T05:05:21.019Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/fa/c301a2ecef5f2ac2b9b446bd5904452f4204f8c4cdcc58723ea1fcd82d5d/pytest_homeassistant_custom_component-0.13.286-py3-none-any.whl", hash = "sha256:180e4fb46488ce7a3b9b85d7da4cf21e78bc8a59672adff9107d5344bc7f1841", size = 65637, upload-time = "2025-10-04T05:05:18.677Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-picked"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/e4/51a54dd6638fd4a7c45bb20a737235fd92cbb4d24b5ff681d64ace5d02e9/pytest_picked-0.5.1.tar.gz", hash = "sha256:6634c4356a560a5dc3dba35471865e6eb06bbd356b56b69c540593e9d5620ded", size = 8401, upload-time = "2024-11-06T23:19:52.538Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/81/450c017746caab376c4b6700439de9f1cc7d8e1f22dec3c1eb235cd9ad3e/pytest_picked-0.5.1-py3-none-any.whl", hash = "sha256:af65c4763b51dc095ae4bc5073a962406902422ad9629c26d8b01122b677d998", size = 6608, upload-time = "2024-11-06T23:19:51.284Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-socket"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/ff/90c7e1e746baf3d62ce864c479fd53410b534818b9437413903596f81580/pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3", size = 12389, upload-time = "2024-01-28T20:17:23.177Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754, upload-time = "2024-01-28T20:17:22.105Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-sugar"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "termcolor" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-timeout"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-unordered"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bd/3e/6ec9ec74551804c9e005d5b3cbe1fd663f03ed3bd4bdb1ce764c3d334d8e/pytest_unordered-0.7.0.tar.gz", hash = "sha256:0f953a438db00a9f6f99a0f4727f2d75e72dd93319b3d548a97ec9db4903a44f", size = 7930, upload-time = "2025-06-03T12:56:04.289Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/95/ae2875e19472797e9672b65412858ab6639d8e55defd9859241e5ff80d02/pytest_unordered-0.7.0-py3-none-any.whl", hash = "sha256:486b26d24a2d3b879a275c3d16d14eda1bd9c32aafddbb17b98ac755daba7584", size = 6210, upload-time = "2025-06-03T12:36:06.66Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-xdist"
|
||||||
|
version = "3.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "execnet" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -1495,7 +1966,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.4"
|
version = "2.32.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
@@ -1503,9 +1974,33 @@ dependencies = [
|
|||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-mock"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "respx"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "httpx" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1665,6 +2160,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syrupy"
|
||||||
|
version = "4.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492, upload-time = "2025-03-24T01:36:37.225Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "text-unidecode"
|
name = "text-unidecode"
|
||||||
version = "1.3"
|
version = "1.3"
|
||||||
@@ -1683,6 +2199,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
|
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tqdm"
|
||||||
|
version = "4.67.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
@@ -1692,6 +2220,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
@@ -1971,24 +2511,24 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroconf"
|
name = "zeroconf"
|
||||||
version = "0.147.0"
|
version = "0.147.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "ifaddr" },
|
{ name = "ifaddr" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/78/f681afade2a4e7a9ade696cf3d3dcd9905e28720d74c16cafb83b5dd5c0a/zeroconf-0.147.0.tar.gz", hash = "sha256:f517375de6bf2041df826130da41dc7a3e8772176d3076a5da58854c7d2e8d7a", size = 163958, upload-time = "2025-05-03T16:24:54.207Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/be/71/44d5afe5f160c0bbca6dff9ff03709c49bd9b23e0bf09dba84b1d4273ac0/zeroconf-0.147.2.tar.gz", hash = "sha256:2f91e2544433acfa928c8dbaea7af8bf0644e06904610799d6b762fd599d81bd", size = 164391, upload-time = "2025-09-05T20:33:34.616Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/83/c6ee14c962b79f616f8f987a52244e877647db3846007fc167f481a81b7d/zeroconf-0.147.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1deedbedea7402754b3a1a05a2a1c881443451ccd600b2a7f979e97dd9fcbe6d", size = 1841229, upload-time = "2025-05-03T16:59:17.783Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/d5/c1f80a071dc58a9fa2f4a8f8ee5a05d730a6500099c5b3d1580d74f0df7b/zeroconf-0.147.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb1a057d961f66423fe2f423cfc8a1d3ab854b520ac28e1226c200b916d07a4f", size = 1713789, upload-time = "2025-09-05T21:02:31.289Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/c0/42c08a8b2c5b6052d48a5517a5d05076b8ee2c0a458ea9bd5e0e2be38c01/zeroconf-0.147.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5c57d551e65a2a9b6333b685e3b074601f6e85762e4b4a490c663f1f2e215b24", size = 1697806, upload-time = "2025-05-03T16:59:20.083Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/70/2cfe2390af8f2489a3c16ecb27477f77aa95b0b8307db977e540893e5050/zeroconf-0.147.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8cca1d4861d3b7c34d209e03e419a87ec96f5048870973b81f89483cf689c0b", size = 1683334, upload-time = "2025-09-05T21:02:33.464Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/79/d9b440786d62626f2ca4bd692b6c2bbd1e70e1124c56321bac6a2212a5eb/zeroconf-0.147.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:095bdb0cd369355ff919e3be930991b38557baaa8292d82f4a4a8567a3944f05", size = 2141482, upload-time = "2025-05-03T16:59:22.067Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/c7/cb0be06aa71067c59f52786fcbd3bfc6edcea9188019e88c647db1db9713/zeroconf-0.147.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b7a95526c7185c1f828042d4321c4abe1f7c1781c6e2ed7d69551630ffd950f", size = 1998790, upload-time = "2025-09-05T21:02:35.112Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/12/ab7d31620892a7f4d446a3f0261ddb1198318348c039b4a5ec7d9d09579c/zeroconf-0.147.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8ae0fe0bb947b3a128af586c76a16b5a7d027daa65e67637b042c745f9b136c4", size = 2315614, upload-time = "2025-05-03T16:59:24.091Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/30/594a019872099f707ca42f752208e8dc87170736efe29c75d242b402b8ff/zeroconf-0.147.2-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:01d64386cf21b72659d070792faaa621e9c3dcc3f54361b38ac29afcb5f32ad4", size = 2144716, upload-time = "2025-09-05T21:02:37.025Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/48/2de072ee42e36328e1d80408b70eddf3df0a5b9640db188caa363b3e120f/zeroconf-0.147.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cbeea4d8d0c4f6eb5a82099d53f5729b628685039a44c1a84422080f8ec5b0d", size = 2259809, upload-time = "2025-05-03T16:59:25.976Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/e8/e3c2702cab548bd30b324d21430dd6e9764dbe68546a5dba9de8c02b543d/zeroconf-0.147.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbdd3db8e53e8ff7f5f850613a46a22a828e490fbcd769e1acd2a8697aa66c77", size = 2091857, upload-time = "2025-09-05T21:02:38.797Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/ec/3344b1ed4e60b36dd73cb66c36299c83a356e853e728c68314061498e9cd/zeroconf-0.147.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:728f82417800c5c5dd3298f65cf7a8fef1707123b457d3832dbdf17d38f68840", size = 2096364, upload-time = "2025-05-03T16:59:27.786Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/85/031dc17fe5d538f0e0e4582b44c5aa208818c6a96b322787d0e720fab919/zeroconf-0.147.2-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de45086232034d079e8b732ab65caf4df338fbc93f9547d9001558d832018296", size = 1921535, upload-time = "2025-09-05T21:02:40.642Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/30/5f34363e2d3c25a78fc925edcc5d45d332296a756d698ccfc060bba8a7aa/zeroconf-0.147.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:a2dc9ae96cd49b50d651a78204aafe9f41e907122dc98e719be5376b4dddec6f", size = 2307868, upload-time = "2025-05-03T16:24:52.178Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/13/fb485cc2389243bf056602721f04bbffa1ebd87e59948ffa4aadb26ab253/zeroconf-0.147.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:45f4279c908b2ccdac5804392e47c728e3c128b4ec49d0945263fbc5829bf891", size = 2135592, upload-time = "2025-09-05T20:33:31.765Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/a8/9b4242ae78bd271520e019faf47d8a2b36242b3b1a7fd47ee7510d380734/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9570ab3203cc4bd3ad023737ef4339558cdf1f33a5d45d76ed3fe77e5fa5f57", size = 2295063, upload-time = "2025-05-03T16:59:29.695Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/7c/7209ab3b49d9fa79a25c0c6f21cc0c9286933090a9ac8bb94c3d131ea163/zeroconf-0.147.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:915bf241f3bb968dd37447551a114f42384b15e1a1778479fd7d0bf2c9898141", size = 2152702, upload-time = "2025-09-05T21:02:42.558Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/e6/b63e4e09d71e94bfe0d30c6fc80b0e67e3845eb630bcfb056626db070776/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:fd783d9bac258e79d07e2bd164c1962b8f248579392b5078fd607e7bb6760b53", size = 2152284, upload-time = "2025-05-03T16:59:31.598Z" },
|
{ url = "https://files.pythonhosted.org/packages/90/c1/7d6e9f661552619d93484dca57f8ad25fb6199d5b7d85b32d05b4d686849/zeroconf-0.147.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47acc2d5117240afcb231dc669007ac1c25122094300aa1226143f16cff96406", size = 2005232, upload-time = "2025-09-05T21:02:44.232Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/12/42b990cb7ad997eb9f9fff15c61abff022adc44f5d1e96bd712ed6cd85ab/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b4acc76063cc379774db407dce0263616518bb5135057eb5eeafc447b3c05a81", size = 2498559, upload-time = "2025-05-03T16:59:33.444Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/4c/0f526a1d6653dcb3c461c760dbf72d8983be2a7a791bcd19e5a3ab38697d/zeroconf-0.147.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5bcf519c047a37c48684fdc8e2b5560be696ec1fbbc0099809fd9b36a6d4d2d4", size = 2310241, upload-time = "2025-09-05T21:02:45.998Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/f9/080619bfcfc353deeb8cf7e813eaf73e8e28ff9a8ca7b97b9f0ecbf4d1d6/zeroconf-0.147.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:acc5334cb6cb98db3917bf9a3d6b6b7fdd205f8a74fd6f4b885abb4f61098857", size = 2456548, upload-time = "2025-05-03T16:59:35.721Z" },
|
{ url = "https://files.pythonhosted.org/packages/2d/43/f994811d1d452a7f2f74287f65701c4d002105249f68b78117ef9077bb8e/zeroconf-0.147.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b0ab523d7be5bebf58aeada1ca7f5ae41c1ebbe69da8435bbf2c2581b4fb3ac", size = 2237360, upload-time = "2025-09-05T21:02:48.282Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/b6/a25b703f418200edd6932d56bbd32cbd087b828579cf223348fa778fb1ff/zeroconf-0.147.0-cp313-cp313-win32.whl", hash = "sha256:7c52c523aa756e67bf18d46db298a5964291f7d868b4a970163432e7d745b992", size = 1427188, upload-time = "2025-05-03T16:59:38.756Z" },
|
{ url = "https://files.pythonhosted.org/packages/a1/de/531cbe5226c8a564c9deb50387767072b8516b7bf6a862491e3d5f907207/zeroconf-0.147.2-cp313-cp313-win32.whl", hash = "sha256:de22c0306b35ef0ee23f4deaa9204f0ffc3600125c63cc277fc5a52b7021aaf6", size = 1290700, upload-time = "2025-09-05T21:02:50.021Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/e1/ba463435cdb0b38088eae56d508ec6128b9012f58cedab145b1b77e51316/zeroconf-0.147.0-cp313-cp313-win_amd64.whl", hash = "sha256:60f623af0e45fba69f5fe80d7b300c913afe7928fb43f4b9757f0f76f80f0d82", size = 1655531, upload-time = "2025-05-03T16:59:40.65Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/d6/db459936c2eb64349c72e34a8951e91f0cbb310f4a3a185d78e6106448dd/zeroconf-0.147.2-cp313-cp313-win_amd64.whl", hash = "sha256:c30a551d9a1df428ecce7816659314470f2bf36957a15905ff40560595bf61e9", size = 1528181, upload-time = "2025-09-05T21:02:51.901Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user