Skip to content

Commit 455f342

Browse files
sambacc: add a URL opener type
Add a URLOpener based on python's urllib, but with non-HTTP(s) protocols disabled. The URLOpener can be extended later with other remote storage protocols. Signed-off-by: John Mulligan <[email protected]>
1 parent 8d3a2e5 commit 455f342

File tree

1 file changed

+88
-0
lines changed

1 file changed

+88
-0
lines changed

sambacc/url_opener.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#
2+
# sambacc: a samba container configuration tool
3+
# Copyright (C) 2023 John Mulligan
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>
17+
#
18+
19+
import errno
20+
import http
21+
import typing
22+
import urllib.error
23+
import urllib.request
24+
25+
from .opener import SchemeNotSupported
26+
27+
28+
class _UnknownHandler(urllib.request.BaseHandler):
29+
def unknown_open(self, req: urllib.request.Request) -> None:
30+
raise SchemeNotSupported(req.full_url)
31+
32+
33+
class URLOpener:
34+
"""An Opener type used for fetching remote resources named in
35+
a pseudo-URL (URI-like) style.
36+
By default works like urllib.urlopen but only for HTTP(S).
37+
38+
Example:
39+
>>> uo = URLOpener()
40+
>>> res = uo.open("http://abc.example.org/foo/x.json")
41+
>>> res.read()
42+
"""
43+
44+
# this list is similar to the defaults found in build_opener
45+
# but only for http/https handlers. No FTP, etc.
46+
_handlers = [
47+
urllib.request.ProxyHandler,
48+
urllib.request.HTTPHandler,
49+
urllib.request.HTTPDefaultErrorHandler,
50+
urllib.request.HTTPRedirectHandler,
51+
urllib.request.HTTPErrorProcessor,
52+
urllib.request.HTTPSHandler,
53+
_UnknownHandler,
54+
]
55+
56+
def __init__(self) -> None:
57+
self._opener = urllib.request.OpenerDirector()
58+
for handler in self._handlers:
59+
self._opener.add_handler(handler())
60+
61+
def open(self, url: str) -> typing.IO:
62+
try:
63+
return self._opener.open(url)
64+
except ValueError as err:
65+
# too bad urllib doesn't use a specific subclass of ValueError here
66+
if "unknown url type" in str(err):
67+
raise SchemeNotSupported(url) from err
68+
raise
69+
except urllib.error.HTTPError as err:
70+
_map_errno(err)
71+
raise
72+
73+
74+
_EMAP = {
75+
http.HTTPStatus.NOT_FOUND.value: errno.ENOENT,
76+
http.HTTPStatus.UNAUTHORIZED.value: errno.EPERM,
77+
}
78+
79+
80+
def _map_errno(err: urllib.error.HTTPError) -> None:
81+
"""While HTTPError is an OSError, it often doesn't have an errno set.
82+
Since our callers care about the errno, do a best effort mapping of
83+
some HTTP statuses to errnos.
84+
"""
85+
if getattr(err, "errno", None) is not None:
86+
return
87+
status = int(getattr(err, "status", -1))
88+
setattr(err, "errno", _EMAP.get(status, None))

0 commit comments

Comments
 (0)