Coverage for src/bioimageio/spec/_internal/url.py: 95%
74 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
1from __future__ import annotations
3from contextlib import nullcontext
4from typing import Any, ClassVar
6import httpx
7import pydantic
8from loguru import logger
9from pydantic import RootModel
10from typing_extensions import Literal, assert_never
12from . import warning_levels
13from ._settings import settings
14from .field_warning import issue_warning
15from .root_url import RootHttpUrl
16from .validation_context import get_validation_context
19def _validate_url(url: str | pydantic.HttpUrl) -> pydantic.HttpUrl:
20 return _validate_url_impl(url, request_mode="head", timeout=settings.http_timeout)
23def _validate_url_impl(
24 url: str | pydantic.HttpUrl,
25 request_mode: Literal["head", "get_stream", "get"],
26 timeout: float,
27) -> pydantic.HttpUrl:
28 url = str(url)
29 context = get_validation_context()
30 if url in context.known_files:
31 return pydantic.HttpUrl(url)
33 val_url = url
35 if url.startswith(("http://example.com", "https://example.com")):
36 return pydantic.HttpUrl(url)
38 if url.startswith("https://colab.research.google.com/github/"):
39 # get requests for colab returns 200 even if the source notebook does not exists.
40 # We therefore validate the url to the notebbok instead (for github notebooks)
41 val_url = url.replace(
42 "https://colab.research.google.com/github/", "https://github.com/"
43 )
44 elif url.startswith("https://colab.research.google.com/"):
45 # TODO: improve validation of non-github colab urls
46 issue_warning(
47 "colab urls currently pass even if the notebook url was not found. Cannot fully validate {value}",
48 value=url,
49 severity=warning_levels.INFO,
50 )
52 try:
53 if request_mode in ("head", "get"):
54 request_ctxt = nullcontext(
55 httpx.request(
56 request_mode.upper(),
57 val_url,
58 timeout=timeout,
59 follow_redirects=True,
60 )
61 )
62 elif request_mode == "get_stream":
63 request_ctxt = httpx.stream(
64 "GET", val_url, timeout=timeout, follow_redirects=True
65 )
66 else:
67 assert_never(request_mode)
69 with request_ctxt as r:
70 status_code = r.status_code
71 reason = r.reason_phrase
72 location = r.headers.get("location")
74 except (
75 httpx.InvalidURL,
76 httpx.TooManyRedirects,
77 ) as e:
78 raise ValueError(f"Invalid URL '{url}': {e}")
79 except httpx.RequestError as e:
80 issue_warning(
81 "Failed to validate URL '{value}': {error}\nrequest: {request}",
82 value=url,
83 msg_context={"error": str(e), "request": e.request},
84 )
85 except Exception as e:
86 issue_warning(
87 "Failed to validate URL '{value}': {error}",
88 value=url,
89 msg_context={"error": str(e)},
90 )
91 else:
92 if status_code == 200 or status_code in (302, 303): # ok
93 pass
94 elif status_code in (301, 308):
95 issue_warning(
96 "URL redirected ({status_code}): consider updating {value} with new"
97 + " location: {location}",
98 value=url,
99 severity=warning_levels.INFO,
100 msg_context={
101 "status_code": status_code,
102 "location": location,
103 },
104 )
105 elif request_mode == "head":
106 return _validate_url_impl(url, request_mode="get_stream", timeout=timeout)
107 elif request_mode == "get_stream":
108 return _validate_url_impl(url, request_mode="get", timeout=timeout)
109 elif request_mode == "get":
110 issue_warning(
111 "{status_code}: {reason} ({value})",
112 value=url,
113 severity=(
114 warning_levels.INFO
115 if status_code == 405 # may be returned due to a captcha
116 else warning_levels.WARNING
117 ),
118 msg_context={
119 "status_code": status_code,
120 "reason": reason,
121 },
122 )
123 else:
124 assert_never(request_mode)
126 context.known_files[url] = None
127 return pydantic.HttpUrl(url)
130class HttpUrl(RootHttpUrl):
131 """A URL with the HTTP or HTTPS scheme."""
133 root_model: ClassVar[type[RootModel[Any]]] = RootModel[pydantic.HttpUrl]
134 _exists: bool | None = None
136 def _after_validator(self):
137 value = super()._after_validator()
138 if get_validation_context().perform_io_checks:
139 _ = value.exists()
141 return value
143 def exists(self):
144 """True if URL is available"""
145 if self._exists is None:
146 ctxt = get_validation_context()
147 try:
148 with ctxt.replace(warning_level=warning_levels.WARNING):
149 self._validated = _validate_url(self._validated)
150 except Exception as e:
151 if ctxt.log_warnings:
152 logger.info(e)
154 self._exists = False
155 else:
156 self._exists = True
158 return self._exists