Parse Accept-Language header

This commit is contained in:
Kovid Goyal 2015-06-02 21:59:04 +05:30
parent bf0909669d
commit 48b6e3d42f
5 changed files with 94 additions and 19 deletions

View File

@ -154,6 +154,7 @@ class HTTPRequest(Connection):
request_handler = None
static_cache = None
translator_cache = None
def __init__(self, *args, **kwargs):
Connection.__init__(self, *args, **kwargs)

View File

@ -19,7 +19,9 @@ from calibre.srv.loop import WRITE
from calibre.srv.errors import HTTPSimpleResponse
from calibre.srv.http_request import HTTPRequest, read_headers
from calibre.srv.sendfile import file_metadata, sendfile_to_socket_async, CannotSendfile, SendfileInterrupted
from calibre.srv.utils import MultiDict, http_date, HTTP1, HTTP11, socket_errors_socket_closed
from calibre.srv.utils import (
MultiDict, http_date, HTTP1, HTTP11, socket_errors_socket_closed,
sort_q_values, get_translator_for_lang)
from calibre.utils.monotonic import monotonic
Range = namedtuple('Range', 'start stop size')
@ -71,21 +73,19 @@ def parse_if_none_match(val): # {{{
# }}}
def acceptable_encoding(val, allowed=frozenset({'gzip'})): # {{{
def enc(x):
e, r = x.partition(';')[::2]
p, v = r.partition('=')[::2]
q = 1.0
if p == 'q' and v:
try:
q = float(v)
except Exception:
pass
return e.lower(), q
for x in sort_q_values(val):
x = x.lower()
if x in allowed:
return x
# }}}
emap = dict(enc(x.strip()) for x in val.split(','))
acceptable = sorted(set(emap) & allowed, key=emap.__getitem__, reverse=True)
if acceptable:
return acceptable[0]
def preferred_lang(val, get_translator_for_lang): # {{{
for x in sort_q_values(val):
x = x.lower()
found, lang, translator = get_translator_for_lang(x)
if found:
return x
return 'en'
# }}}
def get_ranges(headervalue, content_length): # {{{
@ -180,9 +180,13 @@ def get_range_parts(ranges, content_type, content_length): # {{{
class RequestData(object): # {{{
def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol, static_cache, opts, remote_addr, remote_port):
self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders, self.response_protocol, self.static_cache = (
method, path, query, inheaders, request_body_file, outheaders, response_protocol, static_cache
def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol,
static_cache, opts, remote_addr, remote_port, translator_cache):
(self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders,
self.response_protocol, self.static_cache, self.translator_cache) = (
method, path, query, inheaders, request_body_file, outheaders,
response_protocol, static_cache, translator_cache
)
self.remote_addr, self.remote_port = remote_addr, remote_port
self.opts = opts
@ -196,6 +200,12 @@ class RequestData(object): # {{{
def read(self, size=-1):
return self.request_body_file.read(size)
def get_translator(self, bcp_47_code):
return get_translator_for_lang(self.translator_cache, bcp_47_code)
def get_preferred_language(self):
return preferred_lang(self.outheaders.get('Accept-Language'), self.get_translator)
# }}}
class ReadableOutput(object):
@ -322,7 +332,7 @@ class HTTPConnection(HTTPRequest):
data = RequestData(
self.method, self.path, self.query, inheaders, request_body_file,
outheaders, self.response_protocol, self.static_cache, self.opts,
self.remote_addr, self.remote_port
self.remote_addr, self.remote_port, self.translator_cache
)
self.queue_job(self.run_request_handler, data)
@ -545,10 +555,12 @@ class HTTPConnection(HTTPRequest):
def create_http_handler(handler):
static_cache = {}
translator_cache = {}
@wraps(handler)
def wrapper(*args, **kwargs):
ans = HTTPConnection(*args, **kwargs)
ans.request_handler = handler
ans.static_cache = static_cache
ans.translator_cache = translator_cache
return ans
return wrapper

View File

@ -66,6 +66,18 @@ class TestHTTP(BaseTest):
test('Priority', '1;q=0.5, 2;q=0.75, 3;q=1.0', '3', {'1', '2', '3'})
# }}}
def test_accept_language(self): # {{{
'Test parsing of Accept-Language'
from calibre.srv.http_response import preferred_lang
def test(name, val, ans):
self.ae(preferred_lang(val, lambda x:(True, x, None)), ans, name + ' failed')
test('Empty field', '', 'en')
test('Simple', 'de', 'de')
test('Case insensitive', 'Es', 'es')
test('Multiple', 'fr, es', 'fr')
test('Priority', 'en;q=0.1, de;q=0.7, fr;q=0.5', 'de')
# }}}
def test_range_parsing(self): # {{{
'Test parsing of Range header'
from calibre.srv.http_response import get_ranges

View File

@ -12,10 +12,12 @@ from urlparse import parse_qs
import repr as reprlib
from email.utils import formatdate
from operator import itemgetter
from future_builtins import map
from calibre import prints
from calibre.constants import iswindows
from calibre.utils.filenames import atomic_rename
from calibre.utils.localization import get_translator
from calibre.utils.socket_inheritance import set_socket_inherit
from calibre.utils.logging import ThreadSafeLog
@ -169,6 +171,22 @@ def create_sock_pair(port=0):
return client_sock, srv_sock
def sort_q_values(header_val):
'Get sorted items from an HTTP header of type: a;q=0.5, b;q=0.7...'
if not header_val:
return []
def item(x):
e, r = x.partition(';')[::2]
p, v = r.partition('=')[::2]
q = 1.0
if p == 'q' and v:
try:
q = max(0.0, min(1.0, float(v.strip())))
except Exception:
pass
return e.strip(), q
return tuple(map(itemgetter(0), sorted(map(item, header_val.split(',')), key=itemgetter(1), reverse=True)))
def eintr_retry_call(func, *args, **kwargs):
while True:
try:
@ -178,6 +196,14 @@ def eintr_retry_call(func, *args, **kwargs):
continue
raise
def get_translator_for_lang(cache, bcp_47_code):
try:
return cache[bcp_47_code]
except KeyError:
pass
cache[bcp_47_code] = ans = get_translator(bcp_47_code)
return ans
# Logging {{{
class ServerLog(ThreadSafeLog):

View File

@ -107,6 +107,30 @@ def get_all_translators():
buf = cStringIO.StringIO(zf.read(mpath + '/messages.mo'))
yield lang, GNUTranslations(buf)
def get_single_translator(mpath):
from zipfile import ZipFile
with ZipFile(P('localization/locales.zip', allow_user_override=False), 'r') as zf:
buf = cStringIO.StringIO(zf.read(mpath + '/messages.mo'))
return GNUTranslations(buf)
def get_translator(bcp_47_code):
parts = bcp_47_code.replace('-', '_').split('_')[:2]
parts[0] = lang_as_iso639_1(parts[0].lower())
if len(parts) > 1:
parts[1] = parts[1].upper()
lang = '_'.join(parts)
lang = {'pt':'pt_BR', 'zh':'zh_CN'}.get(lang, lang)
available = available_translations()
found = True
if lang not in available:
lang = {'pt':'pt_BR', 'zh':'zh_CN'}.get(parts[0], parts[0])
if lang not in available:
lang = get_lang()
found = False
if lang == 'en':
return found, lang, NullTranslations()
return found, lang, get_single_translator(lang)
lcdata = {
u'abday': (u'Sun', u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat'),
u'abmon': (u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', u'Aug', u'Sep', u'Oct', u'Nov', u'Dec'),