diff --git a/pyproject.toml b/pyproject.toml index 442ee1670..5324b86d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,13 @@ dependencies = [ "sqlalchemy>=2.0.0", "psycopg[binary]>=3.2,<4", "aiosqlite>=0.19.0", - "python-jose[cryptography]>=3.3.0", + # NOTE: python-jose was removed here. It was never imported by this + # codebase -- the only "from jose import ..." occurrence is inside a string + # template in backend/code_generator.py, which writes its own + # requirements.txt for the project it generates. Declaring it dragged in + # ecdsa, whose GHSA-wj6h-64fc-37mp has no patched release. passlib below is + # the same template-only case but carries no advisory; it can be dropped + # too if generated-project deps are confirmed self-contained. "passlib[bcrypt]>=1.7.4", "python-decouple>=3.8", "structlog>=23.2.0", @@ -91,6 +97,10 @@ dev = [ "ipykernel>=6.25.0", "notebook>=7.0.0", "locust==2.46.0", + # Test-time only. TestGeneratedFastAPIBehaviour executes the FastAPI app + # that code_generator emits, and that app imports jwt. EventRelay itself + # never imports it, so it does not belong in the runtime dependencies. + "pyjwt>=2.10.1", ] docs = [ "mkdocs>=1.5.0", diff --git a/requirements.txt b/requirements.txt index ddb75dacc..1e7d37739 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,9 @@ aiosqlite>=0.19.0 alembic>=1.12.0 # Security & Auth -python-jose[cryptography]>=3.3.0 +# python-jose removed: never imported here (only inside a code_generator.py +# string template, which emits its own requirements.txt). It pulled in ecdsa, +# whose GHSA-wj6h-64fc-37mp has no patched release. passlib[bcrypt]>=1.7.4 # Configuration diff --git a/shared/libs/youtube_proxy.py b/shared/libs/youtube_proxy.py index 77c354095..1020a8434 100644 --- a/shared/libs/youtube_proxy.py +++ b/shared/libs/youtube_proxy.py @@ -12,6 +12,7 @@ import logging import os import random +import re import time import urllib.parse from dataclasses import dataclass @@ -72,20 +73,71 @@ def _get_transcript_proxy_config() -> GenericProxyConfig | None: return GenericProxyConfig(http_url=url, https_url=url) -def _redact_proxy_credentials(text: str) -> str: - """Strip user:pass credentials of the configured proxy URL from text.""" - url = os.getenv("WEBSHARE_PROXY_URL", "").strip() - if not url or url not in text: - return text +# Matches the ``user[:password]@`` userinfo segment of any URL. Kept byte-identical +# to the canonical copy in ``src/youtube_extension/utils/proxy.py``; see that module +# for the full rationale. In short: the classes exclude the authority delimiters +# (whitespace, "/", "?", "#") so paths and query strings containing "@" are never +# mistaken for credentials, but they permit a literal "@" so an unencoded one in +# the password is consumed whole rather than leaving the tail behind. +_USERINFO_RE = re.compile( + r"(?P[A-Za-z][A-Za-z0-9+.\-]*://)" + r"(?P[^\s/:?#]*)" + r"(?::(?P[^\s/?#]*))?" + r"@" +) + +_REDACTED = "***" +_UNPRINTABLE = "" +_REDACTION_FAILED = "" + + +def _redact_proxy_credentials(text: Any) -> str: + """Strip URL userinfo (``user:pass@``) from ``text``. + + Two passes: exact replacement of the configured ``WEBSHARE_PROXY_URL`` (which + preserves the host, so operators can still tell which proxy was in play), + then a generic ``scheme://user:pass@`` sweep that catches credentials never + matching the env value verbatim -- a normalised or percent-encoded form + echoed back by yt-dlp, a ``CalledProcessError`` repr of the argv, or a + different proxy variable such as ``HTTPS_PROXY``. + + Never raises: every caller here is an exception handler, where a failure + would mask the original error. + """ + if isinstance(text, str): + candidate = text + else: + try: + candidate = str(text) + except Exception: # noqa: BLE001 - a hostile __str__ must not propagate + return _UNPRINTABLE + try: - parsed = urllib.parse.urlparse(url) - netloc = parsed.hostname or "" - if parsed.port: - netloc = f"{netloc}:{parsed.port}" - redacted = parsed._replace(netloc=netloc).geturl() - except (ValueError, AttributeError): - redacted = "" - return text.replace(url, redacted) + return _redact(candidate) + except Exception: # noqa: BLE001 - never return text we cannot vouch for + return _REDACTION_FAILED + + +def _redact(text: str) -> str: + """Run the two redaction passes over an already-stringified ``text``.""" + url = os.getenv("WEBSHARE_PROXY_URL", "").strip() + if url and url in text: + try: + parsed = urllib.parse.urlparse(url) + netloc = parsed.hostname or "" + if parsed.port: + netloc = f"{netloc}:{parsed.port}" + redacted = parsed._replace(netloc=netloc).geturl() + except (ValueError, AttributeError): + redacted = "" + text = text.replace(url, redacted) + + def _mask(match: re.Match[str]) -> str: + if match.group("password") is None: + return f"{match.group('scheme')}{_REDACTED}@" + return f"{match.group('scheme')}{_REDACTED}:{_REDACTED}@" + + return _USERINFO_RE.sub(_mask, text) class YouTubeErrorType(Enum): """YouTube API specific error types""" diff --git a/src/youtube_extension/backend/code_generator.py b/src/youtube_extension/backend/code_generator.py index 614fe8490..5de39057c 100644 --- a/src/youtube_extension/backend/code_generator.py +++ b/src/youtube_extension/backend/code_generator.py @@ -463,9 +463,14 @@ async def _generate_python_api(self, project_path: Path, video_analysis: dict, f if "database" in features: requirements.append("sqlalchemy==2.0.23") if "authentication" in features: - # The generated main.py imports passlib.context; omitting it here - # produced a project that fails at import time. - requirements.append("python-jose[cryptography]==3.3.0") + # The generated main.py imports jwt and passlib.context; omitting + # either here produced a project that fails at import time. + # + # PyJWT rather than python-jose: python-jose depends on ecdsa, + # whose GHSA-wj6h-64fc-37mp has no patched release, so every + # generated project inherited an unfixable advisory. PyJWT is + # maintained and its encode/decode signatures are the same. + requirements.append("pyjwt>=2.10.1") requirements.append("passlib[bcrypt]==1.7.4") # Generate main.py @@ -1115,7 +1120,8 @@ def _generate_fastapi_main(self, title: str, features: list[str]) -> str: if "authentication" in features: scaffolding_endpoints.append("POST /auth/login") auth_imports = ''' -from jose import JWTError, jwt +import jwt +from jwt import PyJWTError from passlib.context import CryptContext''' auth_code = ''' # ─── Authentication ───────────────────────────────────────────────────────── @@ -1162,7 +1168,7 @@ def decode_access_token(token: str) -> dict: key = _require_secret_key() try: return jwt.decode(token, key, algorithms=[ALGORITHM]) - except JWTError as exc: + except PyJWTError as exc: raise HTTPException(status_code=401, detail="Invalid or expired token") from exc diff --git a/src/youtube_extension/utils/proxy.py b/src/youtube_extension/utils/proxy.py index def0da69e..f82a9481f 100644 --- a/src/youtube_extension/utils/proxy.py +++ b/src/youtube_extension/utils/proxy.py @@ -13,6 +13,7 @@ import logging import os +import re import urllib.parse from typing import Any @@ -28,6 +29,38 @@ _PROXY_ENV_VAR = "WEBSHARE_PROXY_URL" +# Matches the ``user[:password]@`` userinfo segment of any URL. +# +# The classes exclude whitespace, "/", "?" and "#" -- the delimiters that end a +# URL authority -- so a path, query, or fragment containing "@" (e.g. +# "https://example.com/a@b" or "https://example.com?e=a@b") is never mistaken +# for credentials, and a match can never span from one URL into the next. +# +# They deliberately *permit* a literal "@". RFC 3986 requires "@" inside +# userinfo to be percent-encoded, but real *_PROXY values routinely carry a raw +# one. Because the classes are greedy, the engine backtracks to the LAST "@" +# inside the authority, so an unencoded "@" in the password +# ("http://user:pa@ss@host") is consumed whole instead of the match stopping at +# the first separator and leaving the password tail behind. +# +# The user may be empty so credentials with no username ("http://:pass@host") +# are still redacted. +_USERINFO_RE = re.compile( + r"(?P[A-Za-z][A-Za-z0-9+.\-]*://)" + r"(?P[^\s/:?#]*)" + r"(?::(?P[^\s/?#]*))?" + r"@" +) + +_REDACTED = "***" + +# Returned when the input cannot be stringified, or when redaction itself +# fails. Both are fail-closed: emitting a fixed placeholder is preferable to +# raising (which would mask the original error) or to returning text we cannot +# guarantee is clean (which could leak the credential we are trying to strip). +_UNPRINTABLE = "" +_REDACTION_FAILED = "" + def get_proxy_url() -> str | None: """Return the validated Webshare proxy URL, or None for direct connection.""" @@ -67,17 +100,59 @@ def get_transcript_proxy_config() -> Any | None: return GenericProxyConfig(http_url=url, https_url=url) -def redact_proxy_credentials(text: str) -> str: - """Strip user:pass credentials of the configured proxy URL from text.""" - url = os.getenv(_PROXY_ENV_VAR, "").strip() - if not url or url not in text: - return text +def redact_proxy_credentials(text: Any) -> str: + """Strip URL userinfo (``user:pass@``) from ``text``. + + Two passes, because either alone is insufficient: + + 1. Exact replacement of the configured ``WEBSHARE_PROXY_URL`` -- preserves + the host so operators can still tell *which* proxy was in play. + 2. A generic ``scheme://user:pass@`` sweep -- catches credentials that never + match the env value verbatim: yt-dlp echoing a normalised or + percent-encoded form back on stderr, a ``CalledProcessError`` repr of the + argv, or a different proxy variable (``HTTPS_PROXY`` and friends) + entirely. + + Pass 1 alone was the previous behaviour and leaked in all of those cases, + because it bailed out whenever the configured URL did not appear in the text + byte-for-byte. + + Always returns a string and never raises; it is called from exception + handlers, where a failure would mask the original error. If ``text`` cannot + be stringified (a ``__str__`` that itself raises) or redaction fails, a + fixed non-sensitive placeholder is returned instead. + """ + if isinstance(text, str): + candidate = text + else: + try: + candidate = str(text) + except Exception: # noqa: BLE001 - a hostile __str__ must not propagate + return _UNPRINTABLE + try: - parsed = urllib.parse.urlparse(url) - netloc = parsed.hostname or "" - if parsed.port: - netloc = f"{netloc}:{parsed.port}" - redacted = parsed._replace(netloc=netloc).geturl() - except (ValueError, AttributeError): - redacted = "" - return text.replace(url, redacted) + return _redact(candidate) + except Exception: # noqa: BLE001 - never return text we cannot vouch for + return _REDACTION_FAILED + + +def _redact(text: str) -> str: + """Run the two redaction passes over an already-stringified ``text``.""" + url = os.getenv(_PROXY_ENV_VAR, "").strip() + if url and url in text: + try: + parsed = urllib.parse.urlparse(url) + netloc = parsed.hostname or "" + if parsed.port: + netloc = f"{netloc}:{parsed.port}" + redacted = parsed._replace(netloc=netloc).geturl() + except (ValueError, AttributeError): + redacted = "" + text = text.replace(url, redacted) + + def _mask(match: re.Match[str]) -> str: + if match.group("password") is None: + return f"{match.group('scheme')}{_REDACTED}@" + return f"{match.group('scheme')}{_REDACTED}:{_REDACTED}@" + + return _USERINFO_RE.sub(_mask, text) diff --git a/tests/unit/test_code_generator.py b/tests/unit/test_code_generator.py index aaf4456ab..fc9c09c11 100644 --- a/tests/unit/test_code_generator.py +++ b/tests/unit/test_code_generator.py @@ -1382,4 +1382,8 @@ def test_auth_projects_declare_their_password_hashing_dependency( requirements = (project_dir / "requirements.txt").read_text() assert "passlib" in requirements - assert "python-jose" in requirements + # PyJWT, not python-jose: the latter depends on ecdsa, whose + # GHSA-wj6h-64fc-37mp has no patched release, so every generated + # project inherited an unfixable advisory. + assert "pyjwt" in requirements + assert "python-jose" not in requirements diff --git a/tests/unit/test_proxy_utils.py b/tests/unit/test_proxy_utils.py new file mode 100644 index 000000000..8e24962ad --- /dev/null +++ b/tests/unit/test_proxy_utils.py @@ -0,0 +1,175 @@ +"""Regression tests for proxy credential redaction. + +``redact_proxy_credentials`` is called from exception handlers that log +subprocess and HTTP failures, so anything it fails to strip is written to logs +verbatim. Every ``LEAKED`` case below reproduces text that leaked before the +two-pass implementation landed. + +The helper exists in two copies -- the canonical +``src/youtube_extension/utils/proxy.py`` and the standalone +``shared/libs/youtube_proxy.py`` used by the importlib fallback path. Both are +exercised here, because a fix applied to only one of them leaves the other +leaking. +""" + +from __future__ import annotations + +import importlib.util +import pathlib +import sys + +import pytest + +from youtube_extension.utils.proxy import redact_proxy_credentials + +_PROXY_ENV_VAR = "WEBSHARE_PROXY_URL" +_CONFIGURED = "http://user:s3cr3t@proxy.internal:8080" + + +def _load_shared_copy(): + """Import ``shared/libs/youtube_proxy.py`` directly, without a package.""" + path = ( + pathlib.Path(__file__).resolve().parents[2] + / "shared" + / "libs" + / "youtube_proxy.py" + ) + name = "_shared_youtube_proxy" + if name in sys.modules: + return sys.modules[name] + spec = importlib.util.spec_from_file_location(name, path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + # Register before exec: the module uses ``from __future__ import + # annotations``, so its dataclass field annotations are strings that + # ``dataclasses`` resolves by looking the module up in ``sys.modules``. + sys.modules[name] = module + try: + spec.loader.exec_module(module) + except Exception: + del sys.modules[name] + raise + return module + + +@pytest.fixture(params=["canonical", "shared"]) +def redact(request): + """Both implementations, so neither can drift into leaking alone.""" + if request.param == "canonical": + return redact_proxy_credentials + return _load_shared_copy()._redact_proxy_credentials + + +@pytest.fixture(autouse=True) +def _configured_proxy(monkeypatch): + monkeypatch.setenv(_PROXY_ENV_VAR, _CONFIGURED) + + +# Each case is (text, secret-that-must-not-survive). +LEAKED = [ + pytest.param( + f"transcript fetch failed via {_CONFIGURED}", + "s3cr3t", + id="exact-configured-url", + ), + pytest.param( + f"ProxyError: {_CONFIGURED}/", + "s3cr3t", + id="trailing-slash-from-urllib3", + ), + # requests/urllib3 lowercase the host when re-rendering a URL, so the text + # no longer matches the env value byte-for-byte. + pytest.param( + "connect to http://user:s3cr3t@PROXY.INTERNAL:8080 refused", + "s3cr3t", + id="host-case-normalised", + ), + pytest.param( + "http://user:s3cr3t%40x@proxy.internal:8080", + "s3cr3t", + id="percent-encoded-password", + ), + # A different proxy variable entirely -- never equal to WEBSHARE_PROXY_URL, + # so the exact-match pass alone never touched it. + pytest.param( + "HTTPS_PROXY=http://bob:hunter2@corp.proxy:3128 connection refused", + "hunter2", + id="different-proxy-variable", + ), + # RFC 3986 requires "@" in userinfo to be percent-encoded, but real proxy + # values carry a raw one. Matching to the FIRST "@" left the tail behind. + pytest.param( + "http://user:pa@ss@proxy.internal:8080", + "ss@proxy", + id="raw-at-sign-in-password", + ), + pytest.param( + "socks5://:tokenz@proxy:1080", + "tokenz", + id="password-with-no-username", + ), + pytest.param( + "CalledProcessError: ['yt-dlp', '--proxy', 'http://u:p4ss@h:1']", + "p4ss", + id="argv-repr-from-subprocess", + ), +] + + +@pytest.mark.parametrize(("text", "secret"), LEAKED) +def test_credentials_never_survive_redaction(redact, text, secret): + assert secret not in redact(text) + + +def test_configured_proxy_host_is_preserved(redact): + """Operators must still be able to tell *which* proxy was in play.""" + result = redact(f"failed via {_CONFIGURED}") + assert "proxy.internal:8080" in result + assert "s3cr3t" not in result + + +# Text that must survive untouched -- over-redaction destroys diagnostics. +PRESERVED = [ + pytest.param("https://example.com/a@b", id="at-sign-in-path"), + pytest.param("https://example.com?e=a@b", id="at-sign-in-query"), + pytest.param("https://example.com#f=a@b", id="at-sign-in-fragment"), + pytest.param("connect to proxy.internal:8080 failed", id="bare-host-port"), + pytest.param("mailto is not a url: a@b.com", id="bare-email"), +] + + +@pytest.mark.parametrize("text", PRESERVED) +def test_non_credential_text_is_untouched(redact, text): + assert redact(text) == text + + +def test_match_never_spans_two_urls(redact): + """A greedy class must not swallow the gap between separate URLs.""" + result = redact("http://a:1@h1:1 and http://b:2@h2:2") + assert result == "http://***:***@h1:1 and http://***:***@h2:2" + + +def test_redaction_is_a_noop_when_no_proxy_configured(redact, monkeypatch): + """The generic sweep still applies with no WEBSHARE_PROXY_URL set.""" + monkeypatch.delenv(_PROXY_ENV_VAR, raising=False) + assert "hunter2" not in redact("http://bob:hunter2@corp.proxy:3128") + assert redact("no credentials here") == "no credentials here" + + +def test_hostile_str_does_not_propagate(redact): + """Called from except blocks -- raising here would mask the real error.""" + + class Hostile: + def __str__(self) -> str: + raise RuntimeError("boom") + + assert redact(Hostile()) == "" + + +def test_non_string_input_is_stringified(redact): + assert redact(12345) == "12345" + + +def test_exception_object_is_accepted(redact): + """The common call shape is redact(str(error)) -- accept the error too.""" + assert "s3cr3t" not in redact(RuntimeError(f"boom via {_CONFIGURED}")) diff --git a/uv.lock b/uv.lock index 40d16cc8c..b98ee61f0 100644 --- a/uv.lock +++ b/uv.lock @@ -1237,7 +1237,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version < '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" }, @@ -1272,43 +1272,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufft = [ - { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufft", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufile = [ - { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] curand = [ - { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-curand", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [[package]] @@ -1520,18 +1520,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, ] -[[package]] -name = "ecdsa" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" }, -] - [[package]] name = "envier" version = "0.6.1" @@ -1546,7 +1534,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2881,17 +2869,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "exceptiongroup" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -2920,17 +2908,17 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/49/04360f83b4d110195751b4171b75dc1cd7b97ba122b18da34b5828172d59/ipython-9.16.0.tar.gz", hash = "sha256:d2f92587b1ef51d84f934dffe05fabb9255f0038ed0a21426f2ea761e39ad09a", size = 4515375, upload-time = "2026-07-31T08:02:51.977Z" } wheels = [ @@ -2942,7 +2930,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -4532,7 +4520,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -4571,7 +4559,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -4583,7 +4571,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -4613,9 +4601,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -4627,7 +4615,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -4985,10 +4973,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -5063,10 +5051,10 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } wheels = [ @@ -5171,7 +5159,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -5859,6 +5847,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + [[package]] name = "pymdown-extensions" version = "11.0.1" @@ -6018,25 +6018,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/3d/26e14cf47c56c9ba3c3e12cae21f24716bc3182bb52260213ec0c819d0b9/python_engineio-4.13.4-py3-none-any.whl", hash = "sha256:272de73124e255d3d2bba6f86358c1a1ba618f938f337a0c868b60550fe38719", size = 60129, upload-time = "2026-07-31T10:30:54.637Z" }, ] -[[package]] -name = "python-jose" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ecdsa" }, - { name = "pyasn1" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, -] - -[package.optional-dependencies] -cryptography = [ - { name = "cryptography" }, -] - [[package]] name = "python-json-logger" version = "4.1.0" @@ -6822,18 +6803,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - [[package]] name = "ruff" version = "0.16.1" @@ -6891,10 +6860,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -6952,13 +6921,13 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "joblib" }, - { name = "narwhals" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "narwhals", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "threadpoolctl" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } wheels = [ @@ -7002,7 +6971,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -7063,7 +7032,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -7148,7 +7117,7 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [ @@ -8644,7 +8613,6 @@ dependencies = [ { name = "pydantic-settings" }, { name = "python-decouple" }, { name = "python-dotenv" }, - { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, { name = "pyyaml" }, { name = "qrcode", extra = ["pil"] }, @@ -8682,6 +8650,7 @@ dev = [ { name = "mypy" }, { name = "notebook" }, { name = "pre-commit" }, + { name = "pyjwt" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -8800,6 +8769,7 @@ requires-dist = [ { name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" }, { name = "pydantic", specifier = ">=2.5.0" }, { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "pyjwt", marker = "extra == 'dev'", specifier = ">=2.10.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, @@ -8808,7 +8778,6 @@ requires-dist = [ { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" }, { name = "python-decouple", specifier = ">=3.8" }, { name = "python-dotenv", specifier = ">=1.2.2" }, - { name = "python-jose", extras = ["cryptography"], specifier = ">=3.3.0" }, { name = "python-multipart", specifier = ">=0.0.31" }, { name = "pyyaml", specifier = ">=6.0.0" }, { name = "qrcode", extras = ["pil"], specifier = ">=7.0" },