mirror of
https://github.com/agentrhq/authsome.git
synced 2026-09-19 01:34:19 +08:00
chore: satisfy pylint ruff checks
This commit is contained in:
@@ -80,6 +80,8 @@ These rules govern all changes to this codebase — apply them without exception
|
||||
|
||||
**No hallucinated APIs.** If unsure a method or parameter exists, search the codebase before using it.
|
||||
|
||||
**Use real runtime types.** Do not hide missing imports or circular dependencies behind quoted annotations, `TYPE_CHECKING` imports, or postponed annotations. If an annotation names a class from another module, import it normally and let import-time failures reveal architectural cycles. Fix those cycles at the ownership boundary. Use `Self` for same-class return types.
|
||||
|
||||
**Read before write.** Understand the existing implementation before modifying it.
|
||||
|
||||
**Prefer reversible changes.** Avoid destructive operations without explicit user confirmation.
|
||||
|
||||
@@ -72,6 +72,9 @@ If there are multiple valid approaches, say so and present the tradeoffs. Silent
|
||||
**No hallucinated APIs.**
|
||||
If you're unsure a method, parameter, or module exists, search for it in the codebase before using it. Plausible-sounding code that doesn't run wastes more time than asking first.
|
||||
|
||||
**Use real runtime types.**
|
||||
Do not hide missing imports or circular dependencies behind quoted annotations, `TYPE_CHECKING` imports, or postponed annotations. If an annotation names a class from another module, import it normally and let import-time failures reveal architectural cycles. Fix those cycles at the ownership boundary. Use `Self` for same-class return types.
|
||||
|
||||
**Read before write.**
|
||||
Understand the existing implementation before modifying it. Assumptions about structure lead to subtle bugs that are expensive to diagnose.
|
||||
|
||||
|
||||
+8
-2
@@ -84,7 +84,8 @@ line-length = 120
|
||||
target-version = "py313"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [# pycodestyle
|
||||
select = [
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
@@ -95,9 +96,14 @@ select = [# pycodestyle
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I"]
|
||||
"I",
|
||||
# pylint
|
||||
"PL"]
|
||||
ignore = ['B008']
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**/*.py" = ["PLC0415"]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 20
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def emit(event: AuditEvent) -> AuditEvent:
|
||||
return event
|
||||
|
||||
|
||||
def emit_event(
|
||||
def emit_event( # noqa: PLR0913
|
||||
event: str,
|
||||
*,
|
||||
source: AuditSource = "internal",
|
||||
|
||||
@@ -15,7 +15,7 @@ from authsome.utils import utc_now
|
||||
class ApiKeyFlow(AuthFlow):
|
||||
"""Stores a user-provided API key as a connection record."""
|
||||
|
||||
async def begin(
|
||||
async def begin( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -32,7 +32,7 @@ class ApiKeyFlow(AuthFlow):
|
||||
runtime_session.state = "waiting_for_user"
|
||||
runtime_session.payload["input_required"] = "api_key"
|
||||
|
||||
async def resume(
|
||||
async def resume( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
|
||||
@@ -36,7 +36,7 @@ class AuthFlow(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def begin(
|
||||
async def begin( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -55,7 +55,7 @@ class AuthFlow(ABC):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def resume(
|
||||
async def resume( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
|
||||
@@ -33,7 +33,7 @@ _DEFAULT_TIMEOUT = 300.0
|
||||
class BrowserFlow(AuthFlow):
|
||||
"""Cookie-based browser SSO — reads Chrome's on-disk cookie database."""
|
||||
|
||||
async def begin(
|
||||
async def begin( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -55,7 +55,7 @@ class BrowserFlow(AuthFlow):
|
||||
runtime_session.payload["ttl_from_cookie"] = cfg.ttl_from_cookie
|
||||
runtime_session.payload["ttl_hours"] = cfg.ttl_hours
|
||||
|
||||
async def resume(
|
||||
async def resume( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
import requests as http_client
|
||||
from fastapi import status
|
||||
|
||||
from authsome.auth.flows.base import AuthFlow, FlowResult
|
||||
from authsome.auth.models.connection import AccountInfo, ConnectionRecord, ProviderClientRecord
|
||||
@@ -23,7 +24,7 @@ class DcrPkceFlow(AuthFlow):
|
||||
|
||||
callback_port: int = 7999
|
||||
|
||||
async def begin(
|
||||
async def begin( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -74,7 +75,7 @@ class DcrPkceFlow(AuthFlow):
|
||||
if client_secret:
|
||||
runtime_session.payload["internal_client_secret"] = client_secret
|
||||
|
||||
async def resume(
|
||||
async def resume( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -171,7 +172,7 @@ class DcrPkceFlow(AuthFlow):
|
||||
]:
|
||||
try:
|
||||
resp = http_client.get(url, timeout=15)
|
||||
if resp.status_code == 200:
|
||||
if resp.status_code == status.HTTP_200_OK:
|
||||
reg_endpoint = resp.json().get("registration_endpoint")
|
||||
if reg_endpoint:
|
||||
return reg_endpoint
|
||||
@@ -221,7 +222,7 @@ class DcrPkceFlow(AuthFlow):
|
||||
return client_id, reg_data.get("client_secret")
|
||||
|
||||
@staticmethod
|
||||
async def _exchange_code(
|
||||
async def _exchange_code( # noqa: PLR0913
|
||||
*,
|
||||
provider: ProviderDefinition,
|
||||
auth_code: str,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""OAuth2 Device Authorization Grant (RFC 8628)."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from fastapi import status
|
||||
from loguru import logger
|
||||
|
||||
from authsome.auth.flows.base import AuthFlow, FlowResult
|
||||
@@ -23,7 +25,7 @@ _MAX_POLL_DURATION = 900
|
||||
class DeviceCodeFlow(AuthFlow):
|
||||
"""OAuth2 Device Authorization Grant — headless flow."""
|
||||
|
||||
async def begin(
|
||||
async def begin( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -67,7 +69,7 @@ class DeviceCodeFlow(AuthFlow):
|
||||
runtime_session.payload["expires_in"] = str(expires_in)
|
||||
runtime_session.payload["internal_scopes"] = json.dumps(effective_scopes)
|
||||
|
||||
async def resume(
|
||||
async def resume( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -119,7 +121,7 @@ class DeviceCodeFlow(AuthFlow):
|
||||
)
|
||||
|
||||
error = data.get("error", "")
|
||||
if error == "authorization_pending" or error == "slow_down":
|
||||
if error in {"authorization_pending", "slow_down"}:
|
||||
return None
|
||||
elif error == "access_denied":
|
||||
raise AuthenticationFailedError("User denied the authorization request", provider=provider.name)
|
||||
@@ -159,7 +161,7 @@ class DeviceCodeFlow(AuthFlow):
|
||||
"Device authorization response was not valid JSON", provider=provider.name
|
||||
) from exc
|
||||
|
||||
async def poll_for_token(
|
||||
async def poll_for_token( # noqa: PLR0912, PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
client_id: str | None,
|
||||
@@ -168,8 +170,6 @@ class DeviceCodeFlow(AuthFlow):
|
||||
interval: int,
|
||||
expires_in: int,
|
||||
) -> dict[str, Any]:
|
||||
import asyncio
|
||||
|
||||
assert provider.oauth is not None
|
||||
poll_interval = max(interval, 1)
|
||||
|
||||
@@ -212,7 +212,7 @@ class DeviceCodeFlow(AuthFlow):
|
||||
logger.warning("Token poll response was not JSON, retrying...")
|
||||
continue
|
||||
|
||||
if resp.status_code == 200 and "access_token" in data:
|
||||
if resp.status_code == status.HTTP_200_OK and "access_token" in data:
|
||||
return data
|
||||
|
||||
error = data.get("error", "")
|
||||
|
||||
@@ -23,7 +23,7 @@ class PkceFlow(AuthFlow):
|
||||
|
||||
callback_port: int = 7999 # TODO: Remove hardcoded ports, better to keep a global in config file
|
||||
|
||||
async def begin(
|
||||
async def begin( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -65,7 +65,7 @@ class PkceFlow(AuthFlow):
|
||||
runtime_session.payload["internal_state"] = state
|
||||
runtime_session.payload["internal_scopes"] = json.dumps(effective_scopes)
|
||||
|
||||
async def resume(
|
||||
async def resume( # noqa: PLR0913
|
||||
self,
|
||||
provider: ProviderDefinition,
|
||||
identity: str | None,
|
||||
@@ -133,7 +133,7 @@ class PkceFlow(AuthFlow):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _exchange_code(
|
||||
async def _exchange_code( # noqa: PLR0913
|
||||
*,
|
||||
provider: ProviderDefinition,
|
||||
auth_code: str,
|
||||
|
||||
@@ -4,6 +4,8 @@ from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
_MIN_VERSION_PARTS = 2
|
||||
|
||||
|
||||
# TODO: This is generic package level function, move it there
|
||||
def current_spec_version() -> int:
|
||||
@@ -13,7 +15,7 @@ def current_spec_version() -> int:
|
||||
except PackageNotFoundError:
|
||||
return 0
|
||||
parts = package_version.split(".")
|
||||
if len(parts) < 2:
|
||||
if len(parts) < _MIN_VERSION_PARTS:
|
||||
return 0
|
||||
try:
|
||||
return int(parts[1])
|
||||
|
||||
@@ -55,7 +55,7 @@ class AuthSessionStore:
|
||||
self._sessions: dict[str, AuthSession] = {}
|
||||
self._state_index: dict[str, str] = {}
|
||||
|
||||
async def create(
|
||||
async def create( # noqa: PLR0913
|
||||
self,
|
||||
*,
|
||||
provider: str,
|
||||
|
||||
@@ -99,7 +99,7 @@ def validate_provider_definition(definition: ProviderDefinition) -> None:
|
||||
)
|
||||
|
||||
|
||||
def required_inputs(
|
||||
def required_inputs( # noqa: PLR0913
|
||||
*,
|
||||
provider: ProviderDefinition,
|
||||
flow_type: FlowType,
|
||||
|
||||
@@ -11,7 +11,9 @@ from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import status
|
||||
|
||||
import authsome.errors as err_mod
|
||||
from authsome.cli.identity import (
|
||||
RuntimeIdentity,
|
||||
load_runtime_identity,
|
||||
@@ -53,15 +55,11 @@ def raise_for_error(response: httpx.Response) -> None:
|
||||
obj = None
|
||||
try:
|
||||
data = response.json()
|
||||
if response.status_code == 401 and data.get("detail") == "Unknown identity handle":
|
||||
import authsome.errors as err_mod
|
||||
|
||||
if response.status_code == status.HTTP_401_UNAUTHORIZED and data.get("detail") == "Unknown identity handle":
|
||||
raise err_mod.IdentityNotRegisteredError("current identity") from exc
|
||||
error_name = data.get("error")
|
||||
message = data.get("message")
|
||||
if error_name and message:
|
||||
import authsome.errors as err_mod
|
||||
|
||||
exc_cls = getattr(err_mod, error_name, None)
|
||||
if exc_cls and issubclass(exc_cls, err_mod.AuthsomeError):
|
||||
obj = exc_cls.__new__(exc_cls)
|
||||
@@ -121,7 +119,7 @@ class AuthsomeApiClient:
|
||||
content=body_bytes if body is not None else None,
|
||||
headers=headers,
|
||||
)
|
||||
if protected and _retry and response.status_code == 401:
|
||||
if protected and _retry and response.status_code == status.HTTP_401_UNAUTHORIZED:
|
||||
try:
|
||||
detail = response.json().get("detail", "")
|
||||
except Exception:
|
||||
@@ -167,15 +165,15 @@ class AuthsomeApiClient:
|
||||
async def _check_server_registration(self, runtime: RuntimeIdentity) -> None:
|
||||
"""Verify registration with the server; register and claim if needed."""
|
||||
try:
|
||||
status = await self.get_identity_status(runtime.handle)
|
||||
identity_status = await self.get_identity_status(runtime.handle)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response.status_code != 404:
|
||||
if exc.response.status_code != status.HTTP_404_NOT_FOUND:
|
||||
raise
|
||||
status = await self.register_identity(runtime.handle, runtime.did)
|
||||
identity_status = await self.register_identity(runtime.handle, runtime.did)
|
||||
|
||||
reg_status = status.get("registration_status", "")
|
||||
reg_status = identity_status.get("registration_status", "")
|
||||
if reg_status == "claim_required":
|
||||
claim_url = status.get("claim_url", "")
|
||||
claim_url = identity_status.get("claim_url", "")
|
||||
if claim_url:
|
||||
self._open_claim_url(claim_url)
|
||||
await self._poll_claim_completion(runtime.handle)
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
|
||||
import click
|
||||
|
||||
from authsome.auth.models.connection import ConnectionRecord
|
||||
from authsome.cli.context import ContextObj
|
||||
from authsome.cli.helpers import auth_command
|
||||
from authsome.utils import redact
|
||||
@@ -35,7 +36,6 @@ async def inspect_connection(ctx_obj: ContextObj, provider: str, connection: str
|
||||
actx = await ctx_obj.initialize()
|
||||
await actx.runtime_client.get_provider(provider)
|
||||
record_dict = await actx.runtime_client.get_connection(provider, connection)
|
||||
from authsome.auth.models.connection import ConnectionRecord
|
||||
|
||||
record = ConnectionRecord.model_validate(record_dict)
|
||||
data = redact(record)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Root CLI commands."""
|
||||
|
||||
import sys
|
||||
import webbrowser
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
@@ -8,6 +9,7 @@ import click
|
||||
from loguru import logger
|
||||
|
||||
from authsome import FlowType
|
||||
from authsome.auth.flows.browser import BrowserFlow
|
||||
from authsome.auth.models.enums import AuthType
|
||||
from authsome.auth.models.provider import ProviderDefinition
|
||||
from authsome.cli.context import ContextObj
|
||||
@@ -17,6 +19,7 @@ from authsome.cli.helpers import (
|
||||
_scan_resolve_should_import,
|
||||
auth_command,
|
||||
)
|
||||
from authsome.cli.identity import ensure_local_identity
|
||||
from authsome.config import get_authsome_config
|
||||
from authsome.paths import get_client_log_path
|
||||
from authsome.utils import connection_is_active
|
||||
@@ -58,7 +61,7 @@ def _build_login_json_payload(session_info: dict[str, Any], provider: str, conne
|
||||
@click.option("--base-url", metavar="URL", help="Override provider API base URL (e.g. for self-hosted enterprise).")
|
||||
@click.option("--force", is_flag=True, help="Overwrite an existing connection if it already exists.")
|
||||
@auth_command
|
||||
async def login(
|
||||
async def login( # noqa: PLR0913
|
||||
ctx_obj: ContextObj,
|
||||
provider: str,
|
||||
connection: str,
|
||||
@@ -88,14 +91,11 @@ async def login(
|
||||
|
||||
if action_type == "open_url":
|
||||
auth_url = next_action["url"]
|
||||
import webbrowser
|
||||
|
||||
with suppress(Exception):
|
||||
webbrowser.open(auth_url)
|
||||
|
||||
elif action_type == "browser":
|
||||
from authsome.auth.flows.browser import BrowserFlow
|
||||
|
||||
credentials = await BrowserFlow.run_login(next_action, provider)
|
||||
session_info = await actx.runtime_client.resume_login_session(session_info["id"], credentials=credentials)
|
||||
login_result = _build_login_json_payload(session_info, provider, connection)
|
||||
@@ -115,7 +115,7 @@ async def login(
|
||||
@click.option("--connection", default="default", metavar="NAME", help="Connection name.")
|
||||
@click.option("--import", "auto_import", is_flag=True, help="Import detected keys without interactive prompt.")
|
||||
@auth_command
|
||||
async def scan(ctx_obj: ContextObj, connection: str, auto_import: bool) -> None:
|
||||
async def scan(ctx_obj: ContextObj, connection: str, auto_import: bool) -> None: # noqa: PLR0912, PLR0915
|
||||
"""Scan env files and process env for provider API keys.
|
||||
|
||||
Returns a drift report by default unless ``--import`` is also passed.
|
||||
@@ -270,8 +270,6 @@ async def run(ctx_obj: ContextObj, command: tuple[str]) -> None:
|
||||
@auth_command
|
||||
async def init(ctx_obj: ContextObj) -> None:
|
||||
"""Initialize local storage and register a fresh profile."""
|
||||
from authsome.cli.identity import ensure_local_identity
|
||||
|
||||
home = get_authsome_config().home
|
||||
identity = ensure_local_identity(home)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
|
||||
import click
|
||||
|
||||
from authsome.cli import daemon_control
|
||||
from authsome.cli.client import resolve_daemon_url
|
||||
from authsome.cli.context import ContextObj
|
||||
from authsome.cli.daemon_control import (
|
||||
@@ -18,6 +19,7 @@ from authsome.cli.daemon_control import (
|
||||
)
|
||||
from authsome.cli.helpers import auth_command
|
||||
from authsome.server.config import get_server_config
|
||||
from authsome.server.daemon import serve
|
||||
|
||||
|
||||
@click.group(name="daemon")
|
||||
@@ -40,8 +42,6 @@ def daemon() -> None:
|
||||
@click.option("--reload", is_flag=True, help="Enable auto-reload on code changes.")
|
||||
def daemon_serve(host: str, port: int, reload: bool) -> None:
|
||||
"""Run the daemon in the foreground."""
|
||||
from authsome.server.daemon import serve
|
||||
|
||||
serve(host=host, port=port, reload=reload)
|
||||
|
||||
|
||||
@@ -152,10 +152,9 @@ async def daemon_status_cmd(ctx_obj: ContextObj) -> None:
|
||||
@auth_command
|
||||
async def daemon_logs(ctx_obj: ContextObj, lines: int) -> None:
|
||||
"""Show daemon log output."""
|
||||
from authsome.cli.daemon_control import LOG_FILE
|
||||
|
||||
if not LOG_FILE.exists():
|
||||
ctx_obj.print_json({"log_file": str(LOG_FILE), "entries": []})
|
||||
log_file = daemon_control.LOG_FILE
|
||||
if not log_file.exists():
|
||||
ctx_obj.print_json({"log_file": str(log_file), "entries": []})
|
||||
return
|
||||
entries = LOG_FILE.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:]
|
||||
ctx_obj.print_json({"log_file": str(LOG_FILE), "entries": entries})
|
||||
entries = log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:]
|
||||
ctx_obj.print_json({"log_file": str(log_file), "entries": entries})
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import click
|
||||
|
||||
from authsome.cli.config import load_client_config, save_client_config
|
||||
from authsome.cli.context import ContextObj
|
||||
from authsome.cli.helpers import auth_command
|
||||
from authsome.cli.identity import create_identity, load_identity
|
||||
from authsome.config import get_authsome_config
|
||||
|
||||
|
||||
@@ -17,8 +19,6 @@ def profile() -> None:
|
||||
@auth_command
|
||||
async def profile_create(ctx_obj: ContextObj, handle: str | None) -> None:
|
||||
"""Create a local profile keypair."""
|
||||
from authsome.cli.identity import create_identity
|
||||
|
||||
home = get_authsome_config().home
|
||||
identity_meta = create_identity(home, handle)
|
||||
|
||||
@@ -38,9 +38,6 @@ async def profile_create(ctx_obj: ContextObj, handle: str | None) -> None:
|
||||
@auth_command
|
||||
async def profile_use(ctx_obj: ContextObj, handle: str) -> None:
|
||||
"""Select the active local profile."""
|
||||
from authsome.cli.config import load_client_config, save_client_config
|
||||
from authsome.cli.identity import load_identity
|
||||
|
||||
home = get_authsome_config().home
|
||||
identity_meta = load_identity(home, handle)
|
||||
save_client_config(home, load_client_config(home).model_copy(update={"active_identity": identity_meta.handle}))
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from authsome import __version__
|
||||
from authsome.cli.client import (
|
||||
AuthsomeApiClient,
|
||||
is_managed_local_daemon_url,
|
||||
@@ -45,8 +47,6 @@ async def is_daemon_responsive() -> bool:
|
||||
|
||||
def is_port_occupied() -> bool:
|
||||
"""Return whether the configured daemon port is currently occupied by any listening process."""
|
||||
import socket
|
||||
|
||||
host, port = _resolved_host_port()
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(0.2)
|
||||
@@ -170,7 +170,6 @@ async def daemon_status() -> dict[str, Any]:
|
||||
async def _is_ready(client: AuthsomeApiClient) -> bool:
|
||||
try:
|
||||
health = await client.health()
|
||||
from authsome import __version__
|
||||
|
||||
if health.get("version") != __version__:
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""CLI specific utilities for authsome."""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import ipaddress
|
||||
import os
|
||||
import sys
|
||||
@@ -15,15 +17,14 @@ from authsome.auth.models.provider import ProviderDefinition
|
||||
from authsome.cli.context import ContextObj, common_options, pass_ctx
|
||||
from authsome.utils import format_error_code
|
||||
|
||||
_MIN_QUOTED_VALUE_LENGTH = 2
|
||||
|
||||
|
||||
def handle_errors(func):
|
||||
"""Catch exceptions and return structured JSON errors."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(ctx_obj: ContextObj, *args, **kwargs):
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
try:
|
||||
if inspect.iscoroutinefunction(func):
|
||||
return asyncio.run(func(ctx_obj, *args, **kwargs))
|
||||
@@ -62,7 +63,7 @@ def setup_logging(verbose: bool, log_file: Path | None) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_provider_endpoints(definition: Any) -> list[tuple[str, str, bool]]:
|
||||
def _validate_provider_endpoints(definition: Any) -> list[tuple[str, str, bool]]: # noqa: PLR0912
|
||||
"""Extract and validate provider endpoints for security."""
|
||||
endpoints_to_check: list[tuple[str, str, bool]] = []
|
||||
if definition.oauth:
|
||||
@@ -137,7 +138,7 @@ def _load_dotenv(path: Path) -> dict[str, str]:
|
||||
value = value.strip()
|
||||
if not key:
|
||||
continue
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
if len(value) >= _MIN_QUOTED_VALUE_LENGTH and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
value = value[1:-1]
|
||||
values[key] = value
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
import certifi
|
||||
from loguru import logger
|
||||
|
||||
from authsome.auth.models.provider import ProviderDefinition
|
||||
from authsome.cli.config import load_client_config, save_client_config
|
||||
from authsome.config import get_authsome_config
|
||||
from authsome.proxy.certs import ensure_local_proxy_ca
|
||||
@@ -95,7 +97,6 @@ class ProxyRunner:
|
||||
},
|
||||
}
|
||||
connected_names = {entry["name"] for entry in connections_data["connections"]}
|
||||
from authsome.auth.models.provider import ProviderDefinition
|
||||
|
||||
for source_providers in connections_data["by_source"].values():
|
||||
for provider_dict in source_providers:
|
||||
@@ -121,8 +122,6 @@ class ProxyRunner:
|
||||
logger.warning("Mitmproxy CA cert not found at {}; HTTPS may fail", mitm_ca)
|
||||
return None
|
||||
|
||||
import certifi
|
||||
|
||||
system_ca_path = Path(certifi.where())
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix="authsome-ca-", suffix=".pem", text=True)
|
||||
|
||||
@@ -13,6 +13,7 @@ from pydantic import BaseModel, Field
|
||||
_ED25519_MULTICODEC_PREFIX = b"\xed\x01"
|
||||
_DID_KEY_PREFIX = "did:key:z"
|
||||
_HANDLE_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$")
|
||||
_ED25519_KEY_BYTES = 32
|
||||
|
||||
# TODO: The list is very small, will start creating conflicts soon. Use a library like randomword.
|
||||
_ADJECTIVES = (
|
||||
@@ -100,7 +101,7 @@ def public_key_from_did_key(did: str) -> Ed25519PublicKey:
|
||||
if not decoded.startswith(_ED25519_MULTICODEC_PREFIX):
|
||||
raise ValueError("did:key does not use the Ed25519 multicodec prefix")
|
||||
raw_key = decoded[len(_ED25519_MULTICODEC_PREFIX) :]
|
||||
if len(raw_key) != 32:
|
||||
if len(raw_key) != _ED25519_KEY_BYTES:
|
||||
raise ValueError("Ed25519 did:key public key must be 32 bytes")
|
||||
return Ed25519PublicKey.from_public_bytes(raw_key)
|
||||
|
||||
@@ -119,7 +120,7 @@ def private_key_from_hex(value: str) -> Ed25519PrivateKey:
|
||||
raw = bytes.fromhex(value.strip())
|
||||
except ValueError as exc:
|
||||
raise ValueError("Malformed Ed25519 private key hex") from exc
|
||||
if len(raw) != 32:
|
||||
if len(raw) != _ED25519_KEY_BYTES:
|
||||
raise ValueError("Ed25519 private key must be 32 bytes")
|
||||
return Ed25519PrivateKey.from_private_bytes(raw)
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def body_sha256(body: bytes) -> str:
|
||||
return hashlib.sha256(body).hexdigest()
|
||||
|
||||
|
||||
def create_proof_jwt(
|
||||
def create_proof_jwt( # noqa: PLR0913
|
||||
*,
|
||||
private_key: Ed25519PrivateKey,
|
||||
issuer: str,
|
||||
@@ -72,7 +72,7 @@ def create_proof_jwt(
|
||||
return jwt.encode(payload, private_key, algorithm="EdDSA")
|
||||
|
||||
|
||||
def validate_proof_jwt(
|
||||
def validate_proof_jwt( # noqa: PLR0913
|
||||
*,
|
||||
token: str,
|
||||
method: str,
|
||||
|
||||
@@ -5,9 +5,10 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
from mitmproxy.certs import CertStore
|
||||
|
||||
|
||||
def ensure_local_proxy_ca() -> bool:
|
||||
def ensure_local_proxy_ca() -> bool: # noqa: PLR0911
|
||||
"""Ensure the mitmproxy CA is generated and trusted in the macOS login keychain.
|
||||
|
||||
Go's crypto/x509 on macOS uses the native Security framework and ignores
|
||||
@@ -24,6 +25,7 @@ def ensure_local_proxy_ca() -> bool:
|
||||
check = subprocess.run(
|
||||
["security", "find-certificate", "-c", "mitmproxy", str(keychain)],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
if check.returncode == 0:
|
||||
@@ -34,8 +36,6 @@ def ensure_local_proxy_ca() -> bool:
|
||||
ca_cert_path = confdir / "mitmproxy-ca-cert.pem"
|
||||
if not ca_cert_path.exists():
|
||||
try:
|
||||
from mitmproxy.certs import CertStore
|
||||
|
||||
CertStore.from_store(confdir, "mitmproxy", 2048)
|
||||
logger.debug("Generated mitmproxy CA certificate at {}", ca_cert_path)
|
||||
except Exception as exc:
|
||||
@@ -57,6 +57,7 @@ def ensure_local_proxy_ca() -> bool:
|
||||
str(ca_cert_path),
|
||||
],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol, Self
|
||||
from typing import Any, Protocol, Self, get_args
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from loguru import logger
|
||||
@@ -15,6 +15,7 @@ from mitmproxy import http
|
||||
from mitmproxy.options import Options
|
||||
from mitmproxy.tools.dump import DumpMaster
|
||||
|
||||
from authsome.auth.models.provider import ProviderDefinition
|
||||
from authsome.config import get_authsome_config
|
||||
from authsome.proxy.config import ProxyMode
|
||||
from authsome.proxy.router import RouteMatch, RouteResolution
|
||||
@@ -119,13 +120,12 @@ class ProxyRouter:
|
||||
return self.resolve(scheme, host, port, path).match
|
||||
|
||||
@staticmethod
|
||||
async def _build_routes(
|
||||
async def _build_routes( # noqa: PLR0912
|
||||
client: ProxyClient,
|
||||
scope: str = "connected",
|
||||
) -> tuple[dict[str, tuple[_RouteTarget, ...]], tuple[_RegexRouteTarget, ...]]:
|
||||
routes_by_host: dict[str, list[_RouteTarget]] = {}
|
||||
regex_routes: list[_RegexRouteTarget] = []
|
||||
from authsome.auth.models.provider import ProviderDefinition
|
||||
|
||||
if hasattr(client, "proxy_routes"):
|
||||
try:
|
||||
@@ -609,8 +609,6 @@ def start_proxy_server(
|
||||
dashboard_url: str | None = None,
|
||||
) -> RunningProxy:
|
||||
"""Start a mitmproxy DumpMaster in a background thread."""
|
||||
from typing import get_args
|
||||
|
||||
if mode not in get_args(ProxyMode):
|
||||
raise ValueError(f"Invalid proxy mode {mode!r}, expected one of {get_args(ProxyMode)}")
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
from posthog import Posthog
|
||||
from posthog import Posthog, identify_context, new_context
|
||||
|
||||
from authsome.server.config import get_server_config
|
||||
|
||||
@@ -15,7 +15,6 @@ def capture_event(identity: str, event: str, properties: dict[str, Any]) -> None
|
||||
ph = _client
|
||||
if ph is None:
|
||||
return
|
||||
from posthog import identify_context, new_context
|
||||
|
||||
with new_context():
|
||||
identify_context(identity)
|
||||
@@ -28,7 +27,7 @@ def init_posthog() -> Posthog | None:
|
||||
Returns the client when analytics is enabled, otherwise None so the daemon
|
||||
can run without emitting telemetry.
|
||||
"""
|
||||
global _client
|
||||
global _client # noqa: PLW0603
|
||||
|
||||
settings = get_server_config()
|
||||
if not settings.analytics_enabled:
|
||||
@@ -47,7 +46,7 @@ def init_posthog() -> Posthog | None:
|
||||
|
||||
def shutdown_posthog() -> None:
|
||||
"""Flush pending events and shut down the PostHog client."""
|
||||
global _client
|
||||
global _client # noqa: PLW0603
|
||||
if _client is not None:
|
||||
_client.shutdown()
|
||||
_client = None
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from importlib.resources import files
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
@@ -70,12 +70,12 @@ def create_app() -> FastAPI:
|
||||
|
||||
@app.exception_handler(AuthsomeError)
|
||||
def authsome_error_handler(request: Request, exc: AuthsomeError) -> JSONResponse:
|
||||
status_code = 400
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
exc_name = exc.__class__.__name__
|
||||
if exc_name in ("ConnectionNotFoundError", "ProviderNotFoundError", "IdentityNotFoundError"):
|
||||
status_code = 404
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
elif exc_name == "CredentialMissingError":
|
||||
status_code = 401
|
||||
status_code = status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
@@ -89,7 +89,9 @@ def create_app() -> FastAPI:
|
||||
|
||||
@app.exception_handler(IdentityRegistrationError)
|
||||
def identity_registration_error_handler(request: Request, exc: IdentityRegistrationError) -> JSONResponse:
|
||||
return JSONResponse(status_code=409, content={"error": "IdentityRegistrationError", "message": str(exc)})
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_409_CONFLICT, content={"error": "IdentityRegistrationError", "message": str(exc)}
|
||||
)
|
||||
|
||||
@app.exception_handler(UiAuthRequiredError)
|
||||
def ui_auth_required_handler(request: Request, exc: UiAuthRequiredError):
|
||||
@@ -97,7 +99,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
@app.get("/claim/{token}", include_in_schema=False)
|
||||
def claim_page_redirect(token: str) -> RedirectResponse:
|
||||
return RedirectResponse(url=f"/claim?token={token}", status_code=307)
|
||||
return RedirectResponse(url=f"/claim?token={token}", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
|
||||
|
||||
app.include_router(auth_browser_router)
|
||||
app.include_router(health_router, prefix="/api")
|
||||
|
||||
@@ -13,6 +13,9 @@ from authsome.auth.models.connection import (
|
||||
)
|
||||
from authsome.vault import Vault
|
||||
|
||||
_VAULT_KEY_PARTS = 3
|
||||
_MIN_CONNECTION_SCHEMA_VERSION = 2
|
||||
|
||||
|
||||
class StoreKeyParts(NamedTuple):
|
||||
"""Parsed components of a credential store key."""
|
||||
@@ -52,7 +55,7 @@ def build_store_key(
|
||||
)
|
||||
|
||||
|
||||
def parse_store_key(key: str) -> StoreKeyParts:
|
||||
def parse_store_key(key: str) -> StoreKeyParts: # noqa: PLR0911
|
||||
"""Parse a credential store key into its components."""
|
||||
if key.startswith("provider:") and key.endswith(":definition"):
|
||||
provider = key[len("provider:") : -len(":definition")]
|
||||
@@ -64,7 +67,7 @@ def parse_store_key(key: str) -> StoreKeyParts:
|
||||
|
||||
if key.startswith("vault:"):
|
||||
parts = key.split(":", 2)
|
||||
if len(parts) < 3:
|
||||
if len(parts) < _VAULT_KEY_PARTS:
|
||||
return StoreKeyParts()
|
||||
vault = parts[1]
|
||||
remainder = parts[2]
|
||||
@@ -206,6 +209,6 @@ class CredentialRepository:
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Corrupt record at key {}", key)
|
||||
return None
|
||||
if data.get("schema_version", 1) < 2:
|
||||
if data.get("schema_version", 1) < _MIN_CONNECTION_SCHEMA_VERSION:
|
||||
return None
|
||||
return ConnectionRecord.model_validate(data)
|
||||
|
||||
@@ -26,7 +26,7 @@ from authsome.auth.models.connection import (
|
||||
)
|
||||
from authsome.auth.models.enums import AuthType, ConnectionStatus, ExportFormat, FlowType
|
||||
from authsome.auth.models.provider import ProviderDefinition
|
||||
from authsome.auth.sessions import AuthSession
|
||||
from authsome.auth.sessions import AuthSession, AuthSessionStatus
|
||||
from authsome.auth.utils import (
|
||||
export_name_part,
|
||||
normalize_base_url,
|
||||
@@ -66,7 +66,7 @@ class CredentialService:
|
||||
Coordinates provider lookup, auth flows, credential persistence, and policy checks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
*,
|
||||
credentials: CredentialRepository,
|
||||
@@ -241,7 +241,7 @@ class CredentialService:
|
||||
"""
|
||||
return await self._credentials.get_provider_client(provider)
|
||||
|
||||
async def update_provider_configuration(
|
||||
async def update_provider_configuration( # noqa: PLR0912
|
||||
self,
|
||||
provider: str,
|
||||
inputs: dict[str, str],
|
||||
@@ -539,8 +539,6 @@ class CredentialService:
|
||||
|
||||
async def background_resume(self, session: AuthSession) -> None:
|
||||
"""Resume a flow in a background thread."""
|
||||
from authsome.auth.sessions import AuthSessionStatus
|
||||
|
||||
try:
|
||||
await self.resume_login_flow(session, {})
|
||||
session.state = AuthSessionStatus.COMPLETED
|
||||
@@ -698,9 +696,12 @@ class CredentialService:
|
||||
connection_name = connection_record["connection_name"]
|
||||
exported = await self._export_connection_values(provider_name, connection_name)
|
||||
for env_name, env_value in exported.items():
|
||||
if env_name in values:
|
||||
env_name = self._disambiguate_export_name(env_name, provider_name, connection_name, values)
|
||||
values[env_name] = env_value
|
||||
resolved_env_name = (
|
||||
self._disambiguate_export_name(env_name, provider_name, connection_name, values)
|
||||
if env_name in values
|
||||
else env_name
|
||||
)
|
||||
values[resolved_env_name] = env_value
|
||||
return values
|
||||
|
||||
return await self._export_connection_values(provider, connection)
|
||||
@@ -800,7 +801,7 @@ class CredentialService:
|
||||
raise CredentialMissingError("No API key stored in connection record", provider=record.provider)
|
||||
return record.api_key
|
||||
|
||||
async def _get_oauth_token(self, record: ConnectionRecord, provider: str, connection: str) -> str:
|
||||
async def _get_oauth_token(self, record: ConnectionRecord, provider: str, connection: str) -> str: # noqa: PLR0912
|
||||
if record.access_token is None:
|
||||
raise CredentialMissingError("No access token stored", provider=provider)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
|
||||
from authsome.auth.sessions import AuthSession, AuthSessionStore
|
||||
from authsome.identity.principal import PrincipalRole
|
||||
@@ -118,10 +118,10 @@ async def verify_pop_caller(request: Request) -> ResolvedOwnership:
|
||||
"""
|
||||
authorization = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing PoP authorization header")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing PoP authorization header")
|
||||
scheme, _, token = authorization.partition(" ")
|
||||
if scheme != POP_AUTH_SCHEME or not token:
|
||||
raise HTTPException(status_code=401, detail="Expected Authorization: PoP <jwt>")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Expected Authorization: PoP <jwt>")
|
||||
|
||||
body = await request.body()
|
||||
# htu is path-only (not full URI) by design — the daemon is local-only,
|
||||
@@ -138,18 +138,20 @@ async def verify_pop_caller(request: Request) -> ResolvedOwnership:
|
||||
replay_cache=request.app.state.proof_replay_cache,
|
||||
)
|
||||
except (ProofValidationError, ValueError) as exc:
|
||||
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
||||
|
||||
registration = await request.app.state.store.identity_registry.resolve(claims.subject)
|
||||
if registration is None:
|
||||
raise HTTPException(status_code=401, detail="Unknown identity handle")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unknown identity handle")
|
||||
if registration.did != claims.issuer:
|
||||
raise HTTPException(status_code=401, detail="Identity issuer does not match registered DID")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Identity issuer does not match registered DID"
|
||||
)
|
||||
|
||||
try:
|
||||
resolved = await _resolve_identity_ownership(request, claims.subject)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
||||
|
||||
request.state.identity = claims.subject
|
||||
request.state.did = claims.issuer
|
||||
@@ -172,7 +174,7 @@ async def get_admin_auth_service(
|
||||
auth: CredentialService = Depends(get_protected_auth_service),
|
||||
) -> CredentialService:
|
||||
if auth.principal_role != PrincipalRole.ADMIN:
|
||||
raise HTTPException(status_code=403, detail="Admin role required")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required")
|
||||
return auth
|
||||
|
||||
|
||||
@@ -188,7 +190,7 @@ async def get_daemon_or_browser_auth_service(request: Request) -> CredentialServ
|
||||
principal_id=getattr(request.state, "ui_principal_id", None),
|
||||
)
|
||||
if auth is None:
|
||||
raise HTTPException(status_code=401, detail="Missing or invalid browser session")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing or invalid browser session")
|
||||
return auth
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
|
||||
from authsome.auth.input_provider import InputField
|
||||
@@ -48,7 +48,7 @@ async def _load_session_or_404(sessions: AuthSessionStore, session_id: str) -> A
|
||||
try:
|
||||
return await sessions.get(session_id)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found") from exc
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found") from exc
|
||||
|
||||
|
||||
def _event_actor(session: AuthSession) -> str:
|
||||
@@ -145,7 +145,7 @@ async def get_session(
|
||||
) -> AuthSessionResponse:
|
||||
session = await _load_session_or_404(sessions, session_id)
|
||||
if session.identity != auth.identity:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found")
|
||||
return _session_response(session, server_base_url)
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ async def resume_session(
|
||||
) -> AuthSessionResponse:
|
||||
session = await _load_session_or_404(sessions, session_id)
|
||||
if session.identity != auth.identity:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found")
|
||||
try:
|
||||
record = await auth.resume_login_flow(session, body.data)
|
||||
if record is None:
|
||||
@@ -182,13 +182,13 @@ async def oauth_callback(
|
||||
) -> Response:
|
||||
state = request.query_params.get("state")
|
||||
if not state:
|
||||
return RedirectResponse("/auth/success?error=missing_state", status_code=303)
|
||||
return RedirectResponse("/auth/success?error=missing_state", status_code=status.HTTP_303_SEE_OTHER)
|
||||
try:
|
||||
session = await sessions.get_by_oauth_state(state)
|
||||
except KeyError:
|
||||
return RedirectResponse("/auth/success?error=session_expired", status_code=303)
|
||||
return RedirectResponse("/auth/success?error=session_expired", status_code=status.HTTP_303_SEE_OTHER)
|
||||
if not await _ensure_browser_session_identity(request, session):
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
return RedirectResponse("/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
callback_data = dict(request.query_params)
|
||||
auth = await require_auth_service(
|
||||
request,
|
||||
@@ -203,10 +203,14 @@ async def oauth_callback(
|
||||
except Exception as exc:
|
||||
_mark_failed(session, str(exc))
|
||||
await sessions.save(session)
|
||||
return RedirectResponse(build_auth_success_url(server_base_url, session.session_id), status_code=303)
|
||||
return RedirectResponse(
|
||||
build_auth_success_url(server_base_url, session.session_id), status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
if return_url := session.payload.get("return_url"):
|
||||
return RedirectResponse(str(return_url), status_code=303)
|
||||
return RedirectResponse(build_auth_success_url(server_base_url, session.session_id), status_code=303)
|
||||
return RedirectResponse(str(return_url), status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(
|
||||
build_auth_success_url(server_base_url, session.session_id), status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/input")
|
||||
@@ -219,9 +223,9 @@ async def get_session_input(
|
||||
try:
|
||||
session = await sessions.get(session_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found") from None
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found") from None
|
||||
if not await _ensure_browser_session_identity(request, session):
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
return RedirectResponse("/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
auth = await require_auth_service(
|
||||
request,
|
||||
identity=session.identity,
|
||||
@@ -258,14 +262,14 @@ async def get_session_device_code(
|
||||
try:
|
||||
session = await sessions.get(session_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found") from None
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found") from None
|
||||
if not await _ensure_browser_session_identity(request, session):
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
return RedirectResponse("/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
user_code = session.payload.get("user_code")
|
||||
verification_uri = session.payload.get("verification_uri")
|
||||
verification_uri_complete = session.payload.get("verification_uri_complete")
|
||||
if not user_code or not verification_uri:
|
||||
raise HTTPException(status_code=400, detail="This session does not have a device code")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="This session does not have a device code")
|
||||
auth = await require_auth_service(
|
||||
request,
|
||||
identity=session.identity,
|
||||
@@ -291,9 +295,9 @@ async def get_browser_session_status(
|
||||
try:
|
||||
session = await sessions.get(session_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found") from None
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found") from None
|
||||
if not await _ensure_browser_session_identity(request, session):
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
return RedirectResponse("/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
return {
|
||||
"id": session.session_id,
|
||||
"provider": session.provider,
|
||||
@@ -338,7 +342,7 @@ async def submit_browser_input(
|
||||
)
|
||||
|
||||
|
||||
async def _submit_session_input(
|
||||
async def _submit_session_input( # noqa: PLR0911
|
||||
*,
|
||||
session_id: str,
|
||||
request: Request,
|
||||
@@ -349,9 +353,9 @@ async def _submit_session_input(
|
||||
try:
|
||||
session = await sessions.get(session_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="Authentication session not found") from None
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Authentication session not found") from None
|
||||
if not await _ensure_browser_session_identity(request, session):
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
return RedirectResponse("/login", status_code=status.HTTP_303_SEE_OTHER)
|
||||
auth = await require_auth_service(
|
||||
request,
|
||||
identity=session.identity,
|
||||
@@ -369,8 +373,10 @@ async def _submit_session_input(
|
||||
session.status_message = "Provider configuration updated"
|
||||
await sessions.save(session)
|
||||
if return_url := session.payload.get("return_url"):
|
||||
return RedirectResponse(str(return_url), status_code=303)
|
||||
return RedirectResponse(build_auth_success_url(server_base_url, session.session_id), status_code=303)
|
||||
return RedirectResponse(str(return_url), status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(
|
||||
build_auth_success_url(server_base_url, session.session_id), status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
await auth.save_inputs(session, inputs)
|
||||
|
||||
@@ -380,8 +386,10 @@ async def _submit_session_input(
|
||||
_mark_completed(session)
|
||||
await sessions.save(session)
|
||||
if return_url := session.payload.get("return_url"):
|
||||
return RedirectResponse(str(return_url), status_code=303)
|
||||
return RedirectResponse(build_auth_success_url(server_base_url, session.session_id), status_code=303)
|
||||
return RedirectResponse(str(return_url), status_code=status.HTTP_303_SEE_OTHER)
|
||||
return RedirectResponse(
|
||||
build_auth_success_url(server_base_url, session.session_id), status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
session.payload["callback_url_override"] = build_callback_url(server_base_url)
|
||||
await auth.begin_login_flow(
|
||||
@@ -395,16 +403,20 @@ async def _submit_session_input(
|
||||
background_tasks.add_task(auth.background_resume, session)
|
||||
if session.payload.get("user_code") and session.payload.get("verification_uri"):
|
||||
await sessions.save(session)
|
||||
return RedirectResponse(url=build_device_url(server_base_url, session.session_id), status_code=303)
|
||||
return RedirectResponse(
|
||||
url=build_device_url(server_base_url, session.session_id), status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
await sessions.index_oauth_state(session)
|
||||
|
||||
auth_url = session.payload.get("auth_url")
|
||||
if auth_url:
|
||||
await sessions.save(session)
|
||||
return RedirectResponse(str(auth_url), status_code=303)
|
||||
return RedirectResponse(str(auth_url), status_code=status.HTTP_303_SEE_OTHER)
|
||||
await sessions.save(session)
|
||||
return RedirectResponse(build_auth_success_url(server_base_url, session.session_id), status_code=303)
|
||||
return RedirectResponse(
|
||||
build_auth_success_url(server_base_url, session.session_id), status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
|
||||
def _session_response(session: AuthSession, server_base_url: str) -> AuthSessionResponse:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Identity registration routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from authsome.server.analytics import capture_event
|
||||
@@ -37,27 +37,29 @@ async def list_identities(
|
||||
@router.post("/register")
|
||||
async def register_identity(body: RegisterIdentityRequest, request: Request) -> dict[str, str]:
|
||||
try:
|
||||
status = await request.app.state.identity_bootstrap.register_identity(handle=body.handle, did=body.did)
|
||||
registration_status = await request.app.state.identity_bootstrap.register_identity(
|
||||
handle=body.handle, did=body.did
|
||||
)
|
||||
except IdentityRegistrationError:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
capture_event(
|
||||
status.identity,
|
||||
registration_status.identity,
|
||||
"identity registered",
|
||||
{
|
||||
"registration_status": status.registration_status,
|
||||
"principal_id": status.principal_id or None,
|
||||
"registration_status": registration_status.registration_status,
|
||||
"principal_id": registration_status.principal_id or None,
|
||||
},
|
||||
)
|
||||
return status.to_payload()
|
||||
return registration_status.to_payload()
|
||||
|
||||
|
||||
@router.get("/{handle}")
|
||||
async def get_identity_status(handle: str, request: Request) -> dict[str, str]:
|
||||
status = await request.app.state.identity_bootstrap.get_identity_status(handle=handle)
|
||||
if status is None:
|
||||
raise HTTPException(status_code=404, detail="Identity not found")
|
||||
payload = status.to_payload()
|
||||
registration_status = await request.app.state.identity_bootstrap.get_identity_status(handle=handle)
|
||||
if registration_status is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Identity not found")
|
||||
payload = registration_status.to_payload()
|
||||
payload.pop("status", None)
|
||||
return payload
|
||||
|
||||
@@ -5,7 +5,7 @@ from contextlib import suppress
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Request, Response
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from authsome import audit
|
||||
@@ -33,8 +33,8 @@ router = APIRouter(tags=["ui"], include_in_schema=False)
|
||||
def _redirect(request: Request, url: str) -> Response:
|
||||
"""Redirect normally, or via htmx full-page redirect for form compatibility."""
|
||||
if request.headers.get("HX-Request") == "true":
|
||||
return Response(status_code=204, headers={"HX-Redirect": url})
|
||||
return RedirectResponse(url=url, status_code=303)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT, headers={"HX-Redirect": url})
|
||||
return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
class UiAuthRequiredError(Exception):
|
||||
@@ -55,7 +55,9 @@ async def _resolve_ui_auth(request: Request, *, next_url: str | None = None) ->
|
||||
return auth
|
||||
|
||||
target = _account_auth_next_url(next_url or request.query_params.get("next") or request.url.path)
|
||||
raise UiAuthRequiredError(RedirectResponse(url=_account_auth_entry_url(target), status_code=303))
|
||||
raise UiAuthRequiredError(
|
||||
RedirectResponse(url=_account_auth_entry_url(target), status_code=status.HTTP_303_SEE_OTHER)
|
||||
)
|
||||
|
||||
|
||||
def require_ui_auth(next_url: str | None = None) -> Callable[[Request], Awaitable[CredentialService]]:
|
||||
@@ -101,7 +103,7 @@ def _account_auth_next_url(value: Any) -> str:
|
||||
|
||||
|
||||
@router.post("/auth/providers/{provider_name}/connect", include_in_schema=False)
|
||||
async def connect_provider(
|
||||
async def connect_provider( # noqa: PLR0913
|
||||
provider_name: str,
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
@@ -232,11 +234,11 @@ async def register_account(
|
||||
except ValueError as exc:
|
||||
return RedirectResponse(
|
||||
url=f"/login?{urlencode({'next': next_url, 'error': str(exc), 'tab': 'register'})}",
|
||||
status_code=303,
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
capture_event(session.email, "account_registered", {"principal_id": session.principal_id})
|
||||
response = RedirectResponse(url=next_url, status_code=303)
|
||||
response = RedirectResponse(url=next_url, status_code=status.HTTP_303_SEE_OTHER)
|
||||
_set_ui_session_cookie(response, session.token, ui_sessions, server_base_url)
|
||||
return response
|
||||
|
||||
@@ -258,12 +260,12 @@ async def login_account(
|
||||
audit.emit_event("account.login_failed", status="failure", reason="invalid_credentials")
|
||||
return RedirectResponse(
|
||||
url=f"/login?{urlencode({'next': next_url, 'error': str(exc), 'tab': 'login'})}",
|
||||
status_code=303,
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
audit.emit_event("account.login", principal_id=session.principal_id, status="success")
|
||||
capture_event(session.email, "account_logged_in", {"principal_id": session.principal_id})
|
||||
response = RedirectResponse(url=next_url, status_code=303)
|
||||
response = RedirectResponse(url=next_url, status_code=status.HTTP_303_SEE_OTHER)
|
||||
_set_ui_session_cookie(response, session.token, ui_sessions, server_base_url)
|
||||
return response
|
||||
|
||||
@@ -277,12 +279,16 @@ async def claim_identity_confirm(
|
||||
try:
|
||||
pending = ui_sessions.get_pending_claim(token)
|
||||
except KeyError:
|
||||
return RedirectResponse(url=f"/claim?{urlencode({'token': token, 'error': 'expired'})}", status_code=303)
|
||||
return RedirectResponse(
|
||||
url=f"/claim?{urlencode({'token': token, 'error': 'expired'})}", status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
await resolve_ui_request_identity(request)
|
||||
principal_id = getattr(request.state, "ui_principal_id", None)
|
||||
if not principal_id:
|
||||
return RedirectResponse(url=f"/login?{urlencode({'next': f'/claim?token={token}'})}", status_code=303)
|
||||
return RedirectResponse(
|
||||
url=f"/login?{urlencode({'next': f'/claim?token={token}'})}", status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
|
||||
pending = ui_sessions.consume_pending_claim(token)
|
||||
await request.app.state.ownership_resolver.claim_identity_for_principal(
|
||||
@@ -291,4 +297,4 @@ async def claim_identity_confirm(
|
||||
)
|
||||
request.app.state.ownership_cache.pop(pending.identity, None)
|
||||
capture_event(pending.identity, "identity_claimed", {"principal_id": principal_id})
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import keyring
|
||||
from loguru import logger
|
||||
|
||||
from authsome.server.config import get_server_config
|
||||
@@ -85,16 +86,12 @@ class ServerSecretResolver:
|
||||
|
||||
def _read_keyring(self) -> str | None:
|
||||
try:
|
||||
import keyring
|
||||
|
||||
return keyring.get_password(_KEYRING_SERVICE, self._spec.keyring_username)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _write_keyring(self, value: str) -> bool:
|
||||
try:
|
||||
import keyring
|
||||
|
||||
keyring.set_password(_KEYRING_SERVICE, self._spec.keyring_username, value)
|
||||
return True
|
||||
except Exception:
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Literal
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiosqlite
|
||||
import asyncpg
|
||||
|
||||
from authsome.server.config import get_server_config
|
||||
|
||||
@@ -132,8 +133,6 @@ async def open_store_database(config: StoreDatabaseConfig) -> StoreDatabase:
|
||||
await initialize_schema(database)
|
||||
return database
|
||||
|
||||
import asyncpg
|
||||
|
||||
connection = await asyncpg.connect(config.dsn)
|
||||
database = StoreDatabase(config=config, connection=connection)
|
||||
await initialize_schema(database)
|
||||
@@ -205,7 +204,7 @@ async def initialize_schema(database: StoreDatabase) -> None:
|
||||
|
||||
async def create_server_store(home: Path | None = None, database_url: str | None = None):
|
||||
"""Create the server-owned relational Store."""
|
||||
from authsome.server.store.repositories import (
|
||||
from authsome.server.store.repositories import ( # noqa: PLC0415
|
||||
AuditEventRegistry,
|
||||
IdentityClaimRegistry,
|
||||
IdentityRegistry,
|
||||
|
||||
@@ -114,7 +114,7 @@ class AuditEventRegistry:
|
||||
|
||||
def configure_exporter(self, loop: asyncio.AbstractEventLoop | None = None):
|
||||
"""Configure the process OTel logger provider to export audit logs to Store."""
|
||||
global _audit_logger_provider
|
||||
global _audit_logger_provider # noqa: PLW0603
|
||||
|
||||
exporter = _StoreAuditExporter(self, loop or asyncio.get_running_loop())
|
||||
with _audit_provider_lock:
|
||||
|
||||
+42
-33
@@ -1,11 +1,34 @@
|
||||
"""Shared utility functions for authsome."""
|
||||
|
||||
import ctypes
|
||||
import getpass
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from ctypes import wintypes
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from authsome.auth.models.connection import Sensitive
|
||||
from authsome.errors import AuthsomeError
|
||||
|
||||
SECONDS_PER_MINUTE = 60
|
||||
MINUTES_PER_HOUR = 60
|
||||
HOURS_PER_DAY = 24
|
||||
EXIT_SUCCESS = 0
|
||||
EXIT_GENERAL_ERROR = 1
|
||||
EXIT_AUTHENTICATION_FAILED = 2
|
||||
EXIT_CONNECTION_NOT_FOUND = 3
|
||||
EXIT_PROVIDER_NOT_FOUND = 4
|
||||
EXIT_CREDENTIAL_MISSING = 5
|
||||
EXIT_CONNECTION_ALREADY_EXISTS = 6
|
||||
EXIT_PROVIDER_ALREADY_REGISTERED = 7
|
||||
EXIT_ENDPOINT_UNREACHABLE = 8
|
||||
EXIT_DAEMON_UNAVAILABLE = 9
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
"""Return the current UTC datetime."""
|
||||
@@ -21,17 +44,16 @@ def to_rfc3339(dt: datetime) -> str:
|
||||
|
||||
def format_duration(total_seconds: int) -> str:
|
||||
"""Return a compact readable string for a duration in seconds."""
|
||||
if total_seconds < 0:
|
||||
total_seconds = 0
|
||||
if total_seconds < 60:
|
||||
total_seconds = max(total_seconds, 0)
|
||||
if total_seconds < SECONDS_PER_MINUTE:
|
||||
return f"{total_seconds}s"
|
||||
minutes = total_seconds // 60
|
||||
if minutes < 60:
|
||||
minutes = total_seconds // SECONDS_PER_MINUTE
|
||||
if minutes < MINUTES_PER_HOUR:
|
||||
return f"{minutes}m"
|
||||
hours = minutes // 60
|
||||
if hours < 24:
|
||||
hours = minutes // MINUTES_PER_HOUR
|
||||
if hours < HOURS_PER_DAY:
|
||||
return f"{hours}h"
|
||||
days = hours // 24
|
||||
days = hours // HOURS_PER_DAY
|
||||
return f"{days}d"
|
||||
|
||||
|
||||
@@ -63,9 +85,6 @@ def redact(record: Any, redacted_value: str = "***REDACTED***") -> dict[str, Any
|
||||
Uses get_type_hints(include_extras=True) to detect Annotated[..., Sensitive()]
|
||||
fields and replaces their values with redacted_value before display.
|
||||
"""
|
||||
import typing
|
||||
|
||||
from authsome.auth.models.connection import Sensitive
|
||||
|
||||
data = record.model_dump(mode="json")
|
||||
try:
|
||||
@@ -81,14 +100,11 @@ def redact(record: Any, redacted_value: str = "***REDACTED***") -> dict[str, Any
|
||||
return data
|
||||
|
||||
|
||||
def require_os_auth(action_name: str) -> bool:
|
||||
def require_os_auth(action_name: str) -> bool: # noqa: PLR0911
|
||||
"""
|
||||
Prompt the user for OS-level authentication (e.g., Touch ID on macOS)
|
||||
before allowing a sensitive action. Returns True if authenticated, False otherwise.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if sys.platform == "darwin":
|
||||
prompt = f"Authsome requires authentication to {action_name}."
|
||||
script = f'do shell script "echo authenticated" with prompt "{prompt}" with administrator privileges'
|
||||
@@ -102,8 +118,6 @@ def require_os_auth(action_name: str) -> bool:
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
elif sys.platform.startswith("linux"):
|
||||
import shutil
|
||||
|
||||
if shutil.which("pkexec"):
|
||||
try:
|
||||
subprocess.run(["pkexec", "true"], check=True, capture_output=True)
|
||||
@@ -119,11 +133,6 @@ def require_os_auth(action_name: str) -> bool:
|
||||
return False
|
||||
return False
|
||||
elif sys.platform == "win32":
|
||||
import ctypes
|
||||
import getpass
|
||||
import os
|
||||
from ctypes import wintypes
|
||||
|
||||
try:
|
||||
password = getpass.getpass(f"Authsome requires authentication to {action_name}. Password: ")
|
||||
if not password:
|
||||
@@ -189,25 +198,25 @@ def connection_is_active(connection: dict[str, Any]) -> bool:
|
||||
return datetime.now(UTC) < expiry
|
||||
|
||||
|
||||
def format_error_code(exc: Exception) -> int:
|
||||
def format_error_code(exc: Exception) -> int: # noqa: PLR0911
|
||||
"""Return a numerical exit code representing the exception type."""
|
||||
if exc.__class__.__name__ == "DaemonUnavailableError":
|
||||
return 9
|
||||
return EXIT_DAEMON_UNAVAILABLE
|
||||
if not isinstance(exc, AuthsomeError | FileExistsError):
|
||||
return 1
|
||||
return EXIT_GENERAL_ERROR
|
||||
exc_name = exc.__class__.__name__
|
||||
if exc_name in ("AuthenticationFailedError", "InputCancelledError"):
|
||||
return 2
|
||||
return EXIT_AUTHENTICATION_FAILED
|
||||
if exc_name == "ConnectionNotFoundError":
|
||||
return 3
|
||||
return EXIT_CONNECTION_NOT_FOUND
|
||||
if exc_name in ("ProviderNotFoundError", "OperationNotAllowedError"):
|
||||
return 4
|
||||
return EXIT_PROVIDER_NOT_FOUND
|
||||
if exc_name in ("CredentialMissingError", "TokenExpiredError", "RefreshFailedError"):
|
||||
return 5
|
||||
return EXIT_CREDENTIAL_MISSING
|
||||
if exc_name == "ConnectionAlreadyExistsError":
|
||||
return 6
|
||||
return EXIT_CONNECTION_ALREADY_EXISTS
|
||||
if exc_name in ("ProviderAlreadyRegisteredError", "FileExistsError"):
|
||||
return 7
|
||||
return EXIT_PROVIDER_ALREADY_REGISTERED
|
||||
if exc_name == "EndpointUnreachableError":
|
||||
return 8
|
||||
return 1
|
||||
return EXIT_ENDPOINT_UNREACHABLE
|
||||
return EXIT_GENERAL_ERROR
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_read_chrome_cookies_attaches_ttl_from_cookie(monkeypatch):
|
||||
yield FakeCookie(".www.linkedin.com", "li_at", "token", 1_800_000_000)
|
||||
yield FakeCookie(".www.linkedin.com", "bcookie", "other", 1_800_000_000)
|
||||
|
||||
monkeypatch.setitem(sys.modules, "browser_cookie3", type("M", (), {"chrome": staticmethod(lambda: FakeJar())}))
|
||||
monkeypatch.setitem(sys.modules, "browser_cookie3", type("M", (), {"chrome": staticmethod(FakeJar)}))
|
||||
monkeypatch.setattr(mod.time, "time", lambda: 1_700_000_000)
|
||||
|
||||
result = mod.read_chrome_cookies([".linkedin.com"], ttl_from_cookie="li_at")
|
||||
@@ -90,7 +90,7 @@ def test_read_chrome_cookies_omits_expiry_when_ttl_cookie_is_session(monkeypatch
|
||||
def __iter__(self):
|
||||
yield FakeCookie(".www.linkedin.com", "li_at", "token", None)
|
||||
|
||||
monkeypatch.setitem(sys.modules, "browser_cookie3", type("M", (), {"chrome": staticmethod(lambda: FakeJar())}))
|
||||
monkeypatch.setitem(sys.modules, "browser_cookie3", type("M", (), {"chrome": staticmethod(FakeJar)}))
|
||||
monkeypatch.setattr(mod.time, "time", lambda: 1_700_000_000)
|
||||
|
||||
result = mod.read_chrome_cookies([".linkedin.com"], ttl_from_cookie="li_at")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for BrowserFlow begin/resume/refresh."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for authentication flows."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
import pytest
|
||||
|
||||
from authsome.auth.flows.api_key import ApiKeyFlow
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for authsome data models."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
from authsome.auth.models.config import current_spec_version
|
||||
from authsome.auth.models.connection import (
|
||||
ConnectionRecord,
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import status
|
||||
|
||||
from authsome.cli.client import AuthsomeApiClient
|
||||
from authsome.cli.config import ClientConfig, load_client_config, save_client_config
|
||||
@@ -172,12 +173,12 @@ async def test_unregistered_identity_registers_on_first_use(monkeypatch, tmp_pat
|
||||
calls.append((method, url))
|
||||
response = Mock()
|
||||
if f"/api/identities/{identity.handle}" in url and method == "GET":
|
||||
response.status_code = 404
|
||||
response.status_code = status.HTTP_404_NOT_FOUND
|
||||
response.raise_for_status.side_effect = httpx.HTTPStatusError(
|
||||
"Not Found", request=Mock(), response=Mock(status_code=404)
|
||||
"Not Found", request=Mock(), response=Mock(status_code=status.HTTP_404_NOT_FOUND)
|
||||
)
|
||||
else:
|
||||
response.status_code = 200
|
||||
response.status_code = status.HTTP_200_OK
|
||||
response.raise_for_status.return_value = None
|
||||
if url.endswith("/api/identities/register"):
|
||||
response.json.return_value = {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the `authsome doctor` command."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for `authsome connections inspect`."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
import json
|
||||
|
||||
from authsome.cli.main import cli
|
||||
|
||||
@@ -4,6 +4,8 @@ These functions have no I/O and require no mocking — they are tested
|
||||
directly to ensure the formatting, duration, and error-code logic is correct.
|
||||
"""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for `authsome login`."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
import json
|
||||
|
||||
from authsome.cli.main import cli
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for `authsome logout`."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
import json
|
||||
|
||||
from authsome.cli.main import cli
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for `authsome provider revoke`."""
|
||||
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
import json
|
||||
|
||||
from authsome.cli.main import cli
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# ruff: noqa: PLR2004
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi import status
|
||||
|
||||
from authsome.auth.models.connection import ConnectionRecord
|
||||
from authsome.auth.models.enums import AuthType, ConnectionStatus
|
||||
@@ -536,7 +537,7 @@ class TestAuthProxyAddon:
|
||||
finally:
|
||||
patcher.stop()
|
||||
|
||||
assert flow.response.status_code == 403
|
||||
assert flow.response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert flow.response.content == b"Forbidden by Authsome proxy policy"
|
||||
auth.record_audit_event.assert_awaited_once()
|
||||
event = auth.record_audit_event.await_args.args[0]
|
||||
@@ -561,7 +562,7 @@ class TestAuthProxyAddon:
|
||||
finally:
|
||||
patcher.stop()
|
||||
|
||||
assert flow.response.status_code == 403
|
||||
assert flow.response.status_code == status.HTTP_403_FORBIDDEN
|
||||
body = flow.response.content.decode("utf-8")
|
||||
assert "openai" in body
|
||||
assert "authsome login openai" in body
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from authsome.audit import emit_event
|
||||
@@ -13,7 +14,7 @@ from tests.server.test_pop_auth import _auth_header
|
||||
def _claim_identity(client: TestClient, tmp_path: Path, handle: str, *, email: str) -> None:
|
||||
identity = create_identity(tmp_path, handle)
|
||||
response = client.post("/api/identities/register", json={"handle": identity.handle, "did": identity.did})
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
claim_url = urlparse(response.json()["claim_url"])
|
||||
token = parse_qs(claim_url.query)["token"][0]
|
||||
claim_path = f"/api/claim/{token}"
|
||||
@@ -22,8 +23,8 @@ def _claim_identity(client: TestClient, tmp_path: Path, handle: str, *, email: s
|
||||
data={"email": email, "password": "password-1", "next": claim_path},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert registered.status_code == 303
|
||||
assert client.post(f"{claim_path}/confirm", follow_redirects=False).status_code == 303
|
||||
assert registered.status_code == status.HTTP_303_SEE_OTHER
|
||||
assert client.post(f"{claim_path}/confirm", follow_redirects=False).status_code == status.HTTP_303_SEE_OTHER
|
||||
|
||||
|
||||
def test_audit_events_endpoint_returns_internal_events_for_admin(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -44,7 +45,7 @@ def test_audit_events_endpoint_returns_internal_events_for_admin(monkeypatch, tm
|
||||
headers=_auth_header(tmp_path, "GET", "/api/audit/events?limit=10"),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
entries = response.json()["entries"]
|
||||
assert entries[0]["event"] == "login"
|
||||
assert entries[0]["identity"] == "steady-wisely-boldly-0042"
|
||||
@@ -72,7 +73,7 @@ def test_external_audit_post_is_enriched_from_pop_identity(monkeypatch, tmp_path
|
||||
headers=_auth_header(tmp_path, "GET", "/api/audit/events?limit=10"),
|
||||
)
|
||||
|
||||
assert posted.status_code == 200
|
||||
assert posted.status_code == status.HTTP_200_OK
|
||||
entries = response.json()["entries"]
|
||||
assert entries[0]["event"] == "proxy_deny"
|
||||
assert entries[0]["source"] == "external"
|
||||
@@ -115,11 +116,11 @@ def test_admin_sees_all_audit_events_and_user_sees_only_own_principal(monkeypatc
|
||||
headers=_auth_header(tmp_path, "GET", "/api/audit/events"),
|
||||
)
|
||||
|
||||
assert admin_response.status_code == 200
|
||||
assert admin_response.status_code == status.HTTP_200_OK
|
||||
admin_events = {entry["event"] for entry in admin_response.json()["entries"]}
|
||||
assert {"admin_event", "user_event"}.issubset(admin_events)
|
||||
|
||||
assert user_response.status_code == 200
|
||||
assert user_response.status_code == status.HTTP_200_OK
|
||||
user_entries = user_response.json()["entries"]
|
||||
assert "user_event" in {entry["event"] for entry in user_entries}
|
||||
assert "admin_event" not in {entry["event"] for entry in user_entries}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from authsome.auth.models.enums import FlowType
|
||||
@@ -41,7 +42,7 @@ def test_get_session_rejects_other_identity(monkeypatch, tmp_path: Path) -> None
|
||||
|
||||
with TestClient(app) as client:
|
||||
owner_registration = client.post("/api/identities/register", json={"handle": owner.handle, "did": owner.did})
|
||||
assert owner_registration.status_code == 200
|
||||
assert owner_registration.status_code == status.HTTP_200_OK
|
||||
register_and_claim_identity(client, tmp_path, stranger.handle, email="stranger@example.com")
|
||||
session = asyncio.run(
|
||||
client.app.state.auth_sessions.create(
|
||||
@@ -63,7 +64,7 @@ def test_get_session_rejects_other_identity(monkeypatch, tmp_path: Path) -> None
|
||||
),
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json()["detail"] == "Authentication session not found"
|
||||
|
||||
|
||||
@@ -75,12 +76,12 @@ def test_resume_session_rejects_other_identity(monkeypatch, tmp_path: Path) -> N
|
||||
|
||||
with TestClient(app) as client:
|
||||
owner_registration = client.post("/api/identities/register", json={"handle": owner.handle, "did": owner.did})
|
||||
assert owner_registration.status_code == 200
|
||||
assert owner_registration.status_code == status.HTTP_200_OK
|
||||
stranger_registration = client.post(
|
||||
"/api/identities/register",
|
||||
json={"handle": stranger.handle, "did": stranger.did},
|
||||
)
|
||||
assert stranger_registration.status_code == 200
|
||||
assert stranger_registration.status_code == status.HTTP_200_OK
|
||||
session = asyncio.run(
|
||||
client.app.state.auth_sessions.create(
|
||||
provider="github",
|
||||
@@ -102,7 +103,7 @@ def test_resume_session_rejects_other_identity(monkeypatch, tmp_path: Path) -> N
|
||||
),
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json()["detail"] == "Proof JWT body hash does not match request"
|
||||
|
||||
|
||||
@@ -130,5 +131,5 @@ def test_sessions_do_not_survive_app_recreation(monkeypatch, tmp_path: Path) ->
|
||||
headers=_auth_header(tmp_path, "GET", f"/api/auth/sessions/{session_id}", handle=owner.handle),
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json()["detail"] == "Authentication session not found"
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from authsome.auth.models.connection import ConnectionRecord
|
||||
@@ -14,7 +15,7 @@ from authsome.server.app import create_app
|
||||
from authsome.server.credential_repository import build_store_key
|
||||
|
||||
|
||||
def _auth_header(
|
||||
def _auth_header( # noqa: PLR0913
|
||||
tmp_path: Path,
|
||||
method: str,
|
||||
path: str,
|
||||
@@ -45,18 +46,18 @@ def register_and_claim_identity(
|
||||
"""Register an identity and drive the browser claim flow through to acceptance."""
|
||||
identity = create_identity(tmp_path, handle)
|
||||
response = client.post("/api/identities/register", json={"handle": identity.handle, "did": identity.did})
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
claim_url = urlparse(response.json()["claim_url"])
|
||||
token = parse_qs(claim_url.query)["token"][0]
|
||||
claim_path = f"/api/claim/{token}"
|
||||
assert client.get(claim_path).status_code == 200
|
||||
assert client.get(claim_path).status_code == status.HTTP_200_OK
|
||||
registered = client.post(
|
||||
"/api/auth/register",
|
||||
data={"email": email, "password": "password-1", "next": claim_path},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert registered.status_code == 303
|
||||
assert client.post(f"{claim_path}/confirm", follow_redirects=False).status_code == 303
|
||||
assert registered.status_code == status.HTTP_303_SEE_OTHER
|
||||
assert client.post(f"{claim_path}/confirm", follow_redirects=False).status_code == status.HTTP_303_SEE_OTHER
|
||||
|
||||
|
||||
def test_whoami_requires_pop(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -65,7 +66,7 @@ def test_whoami_requires_pop(monkeypatch, tmp_path: Path) -> None:
|
||||
with TestClient(create_app()) as client:
|
||||
response = client.get("/api/whoami")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_whoami_accepts_valid_pop_and_scopes_identity(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -75,7 +76,7 @@ def test_whoami_accepts_valid_pop_and_scopes_identity(monkeypatch, tmp_path: Pat
|
||||
register_and_claim_identity(client, tmp_path, "steady-wisely-boldly-0042")
|
||||
response = client.get("/api/whoami", headers=_auth_header(tmp_path, "GET", "/api/whoami"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["identity"] == "steady-wisely-boldly-0042"
|
||||
assert response.json()["principal_id"].startswith("principal_")
|
||||
assert response.json()["vault_id"].startswith("vault_")
|
||||
@@ -94,11 +95,11 @@ def test_health_and_ready_report_encryption_details(monkeypatch, tmp_path: Path)
|
||||
health_response = client.get("/api/health")
|
||||
ready_response = client.get("/api/ready", headers=_auth_header(tmp_path, "GET", "/api/ready"))
|
||||
|
||||
assert health_response.status_code == 200
|
||||
assert health_response.status_code == status.HTTP_200_OK
|
||||
assert health_response.json()["configured_encryption_mode"] == "aes-256-gcm"
|
||||
assert health_response.json()["effective_encryption_source"] == "aes-256-gcm"
|
||||
assert "Argon2id" in health_response.json()["encryption_backend"]
|
||||
assert ready_response.status_code == 200
|
||||
assert ready_response.status_code == status.HTTP_200_OK
|
||||
assert ready_response.json()["configured_encryption_mode"] == "aes-256-gcm"
|
||||
assert ready_response.json()["effective_encryption_source"] == "aes-256-gcm"
|
||||
assert "Argon2id" in ready_response.json()["encryption_backend"]
|
||||
@@ -111,7 +112,7 @@ def test_registration_requires_claim(monkeypatch, tmp_path: Path) -> None:
|
||||
with TestClient(create_app()) as client:
|
||||
response = client.post("/api/identities/register", json={"handle": identity.handle, "did": identity.did})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["registration_status"] == "claim_required"
|
||||
assert "/claim?" in response.json()["claim_url"]
|
||||
|
||||
@@ -124,7 +125,7 @@ def test_whoami_rejects_wrong_path_claim(monkeypatch, tmp_path: Path) -> None:
|
||||
client.post("/api/identities/register", json={"handle": identity.handle, "did": identity.did})
|
||||
response = client.get("/api/whoami", headers=_auth_header(tmp_path, "GET", "/api/connections"))
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_whoami_rejects_unknown_subject(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -133,7 +134,7 @@ def test_whoami_rejects_unknown_subject(monkeypatch, tmp_path: Path) -> None:
|
||||
with TestClient(create_app()) as client:
|
||||
response = client.get("/api/whoami", headers=_auth_header(tmp_path, "GET", "/api/whoami"))
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_whoami_rejects_registered_handle_with_wrong_issuer(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -155,7 +156,7 @@ def test_whoami_rejects_registered_handle_with_wrong_issuer(monkeypatch, tmp_pat
|
||||
),
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_identity_registration_rejects_duplicate_handle_different_did(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -165,11 +166,12 @@ def test_identity_registration_rejects_duplicate_handle_different_did(monkeypatc
|
||||
|
||||
with TestClient(create_app()) as client:
|
||||
assert (
|
||||
client.post("/api/identities/register", json={"handle": first.handle, "did": first.did}).status_code == 200
|
||||
client.post("/api/identities/register", json={"handle": first.handle, "did": first.did}).status_code
|
||||
== status.HTTP_200_OK
|
||||
)
|
||||
response = client.post("/api/identities/register", json={"handle": first.handle, "did": second.did})
|
||||
|
||||
assert response.status_code == 409
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
|
||||
|
||||
def test_identity_registration_rejects_duplicate_did_different_handle(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -179,14 +181,14 @@ def test_identity_registration_rejects_duplicate_did_different_handle(monkeypatc
|
||||
with TestClient(create_app()) as client:
|
||||
assert (
|
||||
client.post("/api/identities/register", json={"handle": identity.handle, "did": identity.did}).status_code
|
||||
== 200
|
||||
== status.HTTP_200_OK
|
||||
)
|
||||
response = client.post(
|
||||
"/api/identities/register",
|
||||
json={"handle": "rapid-brightly-firmly-0007", "did": identity.did},
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
|
||||
|
||||
def test_ready_uses_active_identity_connections_for_warning_check(monkeypatch, tmp_path: Path) -> None:
|
||||
@@ -214,6 +216,6 @@ def test_ready_uses_active_identity_connections_for_warning_check(monkeypatch, t
|
||||
|
||||
response = client.get("/api/ready", headers=_auth_header(tmp_path, "GET", "/api/ready"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["checks"]["connections"] == "ok"
|
||||
assert "no active provider connections found" not in response.json()["warnings"]
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from authsome.cli.identity import create_identity
|
||||
@@ -12,21 +13,21 @@ from tests.server.test_pop_auth import _auth_header
|
||||
def _register_identity(client: TestClient, tmp_path: Path, handle: str, *, email: str = "dev@example.com") -> None:
|
||||
identity = create_identity(tmp_path, handle)
|
||||
response = client.post("/api/identities/register", json={"handle": identity.handle, "did": identity.did})
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
claim_url = response.json().get("claim_url")
|
||||
if claim_url:
|
||||
parsed = urlparse(claim_url)
|
||||
token = parse_qs(parsed.query)["token"][0]
|
||||
claim_path = f"/api/claim/{token}"
|
||||
assert client.get(claim_path).status_code == 200
|
||||
assert client.get(claim_path).status_code == status.HTTP_200_OK
|
||||
registered = client.post(
|
||||
"/api/auth/register",
|
||||
data={"email": email, "password": "password-1", "next": claim_path},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert registered.status_code == 303
|
||||
assert registered.status_code == status.HTTP_303_SEE_OTHER
|
||||
claimed = client.post(f"{claim_path}/confirm", follow_redirects=False)
|
||||
assert claimed.status_code == 303
|
||||
assert claimed.status_code == status.HTTP_303_SEE_OTHER
|
||||
|
||||
|
||||
def _register_admin_then_user(client: TestClient, tmp_path: Path, user_handle: str) -> None:
|
||||
@@ -44,7 +45,7 @@ def test_non_admin_revoke_is_rejected(monkeypatch, tmp_path: Path) -> None:
|
||||
headers=_auth_header(tmp_path, "POST", "/api/connections/github/revoke"),
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json()["detail"] == "Admin role required"
|
||||
|
||||
|
||||
@@ -58,7 +59,7 @@ def test_non_admin_remove_is_rejected(monkeypatch, tmp_path: Path) -> None:
|
||||
headers=_auth_header(tmp_path, "DELETE", "/api/providers/github"),
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json()["detail"] == "Admin role required"
|
||||
|
||||
|
||||
@@ -86,7 +87,7 @@ def test_non_admin_register_provider_is_rejected(monkeypatch, tmp_path: Path) ->
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json()["detail"] == "Admin role required"
|
||||
|
||||
|
||||
@@ -114,5 +115,5 @@ def test_first_principal_admin_can_register_provider(monkeypatch, tmp_path: Path
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["status"] == "ok"
|
||||
|
||||
@@ -688,11 +688,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.29.0"
|
||||
version = "3.29.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1214,7 +1214,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "posthog"
|
||||
version = "7.16.3"
|
||||
version = "7.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backoff" },
|
||||
@@ -1222,9 +1222,9 @@ dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/d4/6e1c24823c515683cb6f74bd1dcc3fbe55f60a27743c1694bdc61c5db5d0/posthog-7.16.3.tar.gz", hash = "sha256:ff8972813c836ae4fcb634b499cf06d643bc3c4f49931218fe142e7aa8a39810", size = 226535, upload-time = "2026-06-01T13:24:07.781Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/53/9abb7c2ec8acfcafd488a379f5937d248bf290f9fe8c0a2edc023063b7e9/posthog-7.17.0.tar.gz", hash = "sha256:25bcd488a2b2359b0ae969ffcba75ecb5fd0287ad3f32fffe645659d543dbf17", size = 229049, upload-time = "2026-06-03T15:14:41.202Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0b/562afe6f037ab3dde06d2bbe33f1de9f8516ac22f882ad263e4e0bfb665b/posthog-7.16.3-py3-none-any.whl", hash = "sha256:f0cbaf25ac06211b87c0a43500673fa2d8de86eb1edb75b8bf688f1b884878ce", size = 264300, upload-time = "2026-06-01T13:24:06.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/fe/49358d2a9ebde6538ace96785e5b59c4df302ab0b72fef6010625cb77c06/posthog-7.17.0-py3-none-any.whl", hash = "sha256:b2735701effba2f8f4ccf58b0b49fbaaf7d4d0544fb6d6b7547cd7ccde5d4f43", size = 267430, upload-time = "2026-06-03T15:14:39.432Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1546,24 +1546,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.30"
|
||||
version = "0.0.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
version = "312"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1738,27 +1741,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.42"
|
||||
version = "0.0.43"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/91/5b5ec4ed8721c18be8d9611778d7c07723cd755676f03b41bf0ea0caa5d3/ty-0.0.42.tar.gz", hash = "sha256:70f5553ac678fc63558d4d77b08a18a68a228f44be2a2fe1afc3f5988db662e7", size = 5769116, upload-time = "2026-06-01T19:40:32.869Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/7c/2df5136ad7c0db69a3973b0b19da8f52bfacdc453c7dffc832d1bf7d23ff/ty-0.0.42-py3-none-linux_armv6l.whl", hash = "sha256:c08a0066610c13627b7d7ad758adb96ca99685791e641eb26837e20803851c53", size = 11544141, upload-time = "2026-06-01T19:40:41.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/82/96cf406d39d8976e825361a27e332224445812793060ac9506d8a5d32b39/ty-0.0.42-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3e944ee4e3d5cdaf70e4ea87f9dd474cc3db612837b50a3ce57afa8da400ecc2", size = 11283538, upload-time = "2026-06-01T19:40:26.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/fe/813b60b9332df835c16c05859ff5aac1896593d01b638ba0e461ede415ac/ty-0.0.42-py3-none-macosx_11_0_arm64.whl", hash = "sha256:603085306e4aac2ce592b39119a4b49ebf8b780cd394e2cfc7dbf3fd8228f954", size = 10711874, upload-time = "2026-06-01T19:40:28.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/64/1f609265be0302ce0f51aa03a27636d018947a76100ff1405258d8445e6c/ty-0.0.42-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a58f17834d7f078c49326a01111a5aac16c979a774b98cdfd8e2350068316676", size = 11213021, upload-time = "2026-06-01T19:40:45.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/c0/f147b2fde7cd01b5f77682937e85a5cdfe35f48b3f1d0f41021024cbd927/ty-0.0.42-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e6ed1027f313202c5c74e376007d1eb5d214494299beb0ea047078b8ad307d40", size = 11321604, upload-time = "2026-06-01T19:40:55.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/72/74a5e68a9bd194681f15c4aac7a0dfab378e76e7a107e8ecd93971a22377/ty-0.0.42-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:063838d2360c1d2c065b45ca76a56ecd6df07fff6570813e74183236559e16d9", size = 11802178, upload-time = "2026-06-01T19:40:19.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/9f/06a31dc9cc91faa2cb8a4bf2f0ba5f7a9a96a4828fb8434338682954ca86/ty-0.0.42-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3f9ed508dfae4cbc943d7766324dd9c57ac8302c8543505fc29cae8ed425fe9", size = 12358436, upload-time = "2026-06-01T19:40:48.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/33/a5bb1afcb671e0b9197f007264682b22f1063bf0e83c151eeaf9958f9047/ty-0.0.42-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fe1eb7d98472ac56ac19fec51c6ed8fe56d86ea0d232a11a127e8c62c882a66", size = 11997849, upload-time = "2026-06-01T19:40:42.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8d/560e4ec4c2f69e68fa094bb93cd19b5eb92c9732d2e0f5e7cc3accde84c4/ty-0.0.42-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de75f9e78bfa81209f2f297528758977cfd4518ba35ef45a0acb516c892a27a5", size = 11869087, upload-time = "2026-06-01T19:40:24.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/3794db15199c03eda60961690046a56c4fbf5d8ef073c82fe2402c851b8c/ty-0.0.42-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c63281f2f1d4df339117fcd4a6dfe17cb999f84eafe707b30e9ebbe26f0bb54a", size = 12059000, upload-time = "2026-06-01T19:40:34.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/9a/02b61cc65ecbd90f18bd02178845c5b756c78fb92643571e77df52c6eed8/ty-0.0.42-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:19e3856477f25255f772851fa7f16f5356c4e1927324d074d49c7bc9a9b211e1", size = 11195698, upload-time = "2026-06-01T19:40:37.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/d6/3927335c956b06a806269dcd2e5b46bc4284b0a95ecc6b0246094a1de28c/ty-0.0.42-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f0f4acac9028264cee5ea0b88229df0b9b2586fc917dbadb6ee35a0e99e8b06", size = 11353487, upload-time = "2026-06-01T19:40:47.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/87/79e7ae4f5f9fb3bea5f3cfac2b4f8c60e905f13962e3c0d97f8b51a5bff6/ty-0.0.42-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae244c84e30fdf2bb1a3cbf2b973da8aa535e57c701f12db44b2939604586c04", size = 11463474, upload-time = "2026-06-01T19:40:30.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/f9/73305ead1b3ccc3d79c04258877db2cd7908c6a6c2060d4e598deca384d2/ty-0.0.42-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2430434f4a52bec0da552ff6a061dcc1c5d11973259248679a1146d776c12f37", size = 11961710, upload-time = "2026-06-01T19:40:39.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/04/52b7325dad8d1a86653f90240208f3e3657bed5e3c8144eed367a0f736b0/ty-0.0.42-py3-none-win32.whl", hash = "sha256:984a55c2fe63b40dac03f5a144b99033c7ed720eb7611787e3f0bd49af8dcf12", size = 10783897, upload-time = "2026-06-01T19:40:22.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/d2/3d2d61255c76c0843766f00b39290115c23cc1cd4fcb0471d84a48f482f1/ty-0.0.42-py3-none-win_amd64.whl", hash = "sha256:f7afd81b10b377d9d4ce6aad355a4f47fd37d47f443118c01ca6e79d46fe6608", size = 11878640, upload-time = "2026-06-01T19:40:50.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/91/1eb0c1e3d558707ead7424f8bfd89b58f42e576714cbed7ad46dcceef34a/ty-0.0.42-py3-none-win_arm64.whl", hash = "sha256:4068c24b0b264fc9f1901e06b97988a041fcaa36c90f18d7747f05124701c7b3", size = 11202335, upload-time = "2026-06-01T19:40:53.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1805,15 +1808,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.48.0"
|
||||
version = "0.49.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user