mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 16:51:58 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
223
odoo-bringout-oca-ocb-base/odoo/tools/_vendor/send_file.py
Normal file
223
odoo-bringout-oca-ocb-base/odoo/tools/_vendor/send_file.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"""
|
||||
Vendored copy of the werkzeug.utils.send_file function defined in
|
||||
werkzeug2 which is packaged in Debian 12 "Bookworm" and Ubuntu 22.04
|
||||
"Jammy". Odoo is compatible with werkzeug2 since saas-15.4.
|
||||
|
||||
This vendored copy is deprecated, only present to ensure backward
|
||||
compatibility with older operating systems.
|
||||
|
||||
:copyright: 2007 Pallets
|
||||
:license: BSD-3-Clause
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import typing as t
|
||||
import unicodedata
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from zlib import adler32
|
||||
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.exceptions import RequestedRangeNotSatisfiable
|
||||
from werkzeug.urls import url_quote
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.wsgi import wrap_file
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_file(
|
||||
path_or_file: t.Union[os.PathLike, str, t.IO[bytes]],
|
||||
environ: "WSGIEnvironment",
|
||||
mimetype: t.Optional[str] = None,
|
||||
as_attachment: bool = False,
|
||||
download_name: t.Optional[str] = None,
|
||||
conditional: bool = True,
|
||||
etag: t.Union[bool, str] = True,
|
||||
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
|
||||
max_age: t.Optional[
|
||||
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
|
||||
] = None,
|
||||
use_x_sendfile: bool = False,
|
||||
response_class: t.Optional[t.Type["Response"]] = None,
|
||||
_root_path: t.Optional[t.Union[os.PathLike, str]] = None,
|
||||
) -> "Response":
|
||||
"""Send the contents of a file to the client.
|
||||
|
||||
The first argument can be a file path or a file-like object. Paths
|
||||
are preferred in most cases because Werkzeug can manage the file and
|
||||
get extra information from the path. Passing a file-like object
|
||||
requires that the file is opened in binary mode, and is mostly
|
||||
useful when building a file in memory with :class:`io.BytesIO`.
|
||||
|
||||
Never pass file paths provided by a user. The path is assumed to be
|
||||
trusted, so a user could craft a path to access a file you didn't
|
||||
intend.
|
||||
|
||||
If the WSGI server sets a ``file_wrapper`` in ``environ``, it is
|
||||
used, otherwise Werkzeug's built-in wrapper is used. Alternatively,
|
||||
if the HTTP server supports ``X-Sendfile``, ``use_x_sendfile=True``
|
||||
will tell the server to send the given path, which is much more
|
||||
efficient than reading it in Python.
|
||||
|
||||
:param path_or_file: The path to the file to send, relative to the
|
||||
current working directory if a relative path is given.
|
||||
Alternatively, a file-like object opened in binary mode. Make
|
||||
sure the file pointer is seeked to the start of the data.
|
||||
:param environ: The WSGI environ for the current request.
|
||||
:param mimetype: The MIME type to send for the file. If not
|
||||
provided, it will try to detect it from the file name.
|
||||
:param as_attachment: Indicate to a browser that it should offer to
|
||||
save the file instead of displaying it.
|
||||
:param download_name: The default name browsers will use when saving
|
||||
the file. Defaults to the passed file name.
|
||||
:param conditional: Enable conditional and range responses based on
|
||||
request headers. Requires passing a file path and ``environ``.
|
||||
:param etag: Calculate an ETag for the file, which requires passing
|
||||
a file path. Can also be a string to use instead.
|
||||
:param last_modified: The last modified time to send for the file,
|
||||
in seconds. If not provided, it will try to detect it from the
|
||||
file path.
|
||||
:param max_age: How long the client should cache the file, in
|
||||
seconds. If set, ``Cache-Control`` will be ``public``, otherwise
|
||||
it will be ``no-cache`` to prefer conditional caching.
|
||||
:param use_x_sendfile: Set the ``X-Sendfile`` header to let the
|
||||
server to efficiently send the file. Requires support from the
|
||||
HTTP server. Requires passing a file path.
|
||||
:param response_class: Build the response using this class. Defaults
|
||||
to :class:`~werkzeug.wrappers.Response`.
|
||||
:param _root_path: Do not use. For internal use only. Use
|
||||
:func:`send_from_directory` to safely send files under a path.
|
||||
"""
|
||||
if response_class is None:
|
||||
response_class = Response
|
||||
|
||||
path = None
|
||||
file = None
|
||||
size = None
|
||||
mtime = None
|
||||
headers = Headers()
|
||||
|
||||
if isinstance(path_or_file, (os.PathLike, str)) or hasattr(
|
||||
path_or_file, "__fspath__"
|
||||
):
|
||||
|
||||
# Flask will pass app.root_path, allowing its send_file wrapper
|
||||
# to not have to deal with paths.
|
||||
if _root_path is not None:
|
||||
path = os.path.join(_root_path, path_or_file)
|
||||
else:
|
||||
path = os.path.abspath(path_or_file)
|
||||
|
||||
stat = os.stat(path)
|
||||
size = stat.st_size
|
||||
mtime = stat.st_mtime
|
||||
else:
|
||||
file = path_or_file
|
||||
|
||||
if download_name is None and path is not None:
|
||||
download_name = os.path.basename(path)
|
||||
|
||||
if mimetype is None:
|
||||
if download_name is None:
|
||||
raise TypeError(
|
||||
"Unable to detect the MIME type because a file name is"
|
||||
" not available. Either set 'download_name', pass a"
|
||||
" path instead of a file, or set 'mimetype'."
|
||||
)
|
||||
|
||||
mimetype, encoding = mimetypes.guess_type(download_name)
|
||||
|
||||
if mimetype is None:
|
||||
mimetype = "application/octet-stream"
|
||||
|
||||
# Don't send encoding for attachments, it causes browsers to
|
||||
# save decompress tar.gz files.
|
||||
if encoding is not None and not as_attachment:
|
||||
headers.set("Content-Encoding", encoding)
|
||||
if use_x_sendfile and path is not None:
|
||||
headers["X-Accel-Charset"] = encoding
|
||||
|
||||
if download_name is not None:
|
||||
try:
|
||||
download_name.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
simple = unicodedata.normalize("NFKD", download_name)
|
||||
simple = simple.encode("ascii", "ignore").decode("ascii")
|
||||
quoted = url_quote(download_name, safe="")
|
||||
names = {"filename": simple, "filename*": f"UTF-8''{quoted}"}
|
||||
else:
|
||||
names = {"filename": download_name}
|
||||
|
||||
value = "attachment" if as_attachment else "inline"
|
||||
headers.set("Content-Disposition", value, **names)
|
||||
elif as_attachment:
|
||||
raise TypeError(
|
||||
"No name provided for attachment. Either set"
|
||||
" 'download_name' or pass a path instead of a file."
|
||||
)
|
||||
|
||||
if use_x_sendfile and path is not None:
|
||||
headers["X-Sendfile"] = path
|
||||
data = None
|
||||
else:
|
||||
if file is None:
|
||||
file = open(path, "rb") # type: ignore
|
||||
elif isinstance(file, io.BytesIO):
|
||||
size = file.getbuffer().nbytes
|
||||
elif isinstance(file, io.TextIOBase):
|
||||
raise ValueError("Files must be opened in binary mode or use BytesIO.")
|
||||
|
||||
data = wrap_file(environ, file)
|
||||
|
||||
rv = response_class(
|
||||
data, mimetype=mimetype, headers=headers, direct_passthrough=True
|
||||
)
|
||||
|
||||
if size is not None:
|
||||
rv.content_length = size
|
||||
|
||||
if last_modified is not None:
|
||||
rv.last_modified = last_modified # type: ignore
|
||||
elif mtime is not None:
|
||||
rv.last_modified = mtime # type: ignore
|
||||
|
||||
rv.cache_control.no_cache = True
|
||||
|
||||
# Flask will pass app.get_send_file_max_age, allowing its send_file
|
||||
# wrapper to not have to deal with paths.
|
||||
if callable(max_age):
|
||||
max_age = max_age(path)
|
||||
|
||||
if max_age is not None:
|
||||
if max_age > 0:
|
||||
rv.cache_control.no_cache = None
|
||||
rv.cache_control.public = True
|
||||
|
||||
rv.cache_control.max_age = max_age
|
||||
rv.expires = int(time() + max_age) # type: ignore
|
||||
|
||||
if isinstance(etag, str):
|
||||
rv.set_etag(etag)
|
||||
elif etag and path is not None:
|
||||
check = adler32(path.encode("utf-8")) & 0xFFFFFFFF
|
||||
rv.set_etag(f"{mtime}-{size}-{check}")
|
||||
|
||||
if conditional:
|
||||
try:
|
||||
rv = rv.make_conditional(environ, accept_ranges=True, complete_length=size)
|
||||
except RequestedRangeNotSatisfiable:
|
||||
if file is not None:
|
||||
file.close()
|
||||
|
||||
raise
|
||||
|
||||
# Some x-sendfile implementations incorrectly ignore the 304
|
||||
# status code and send the file anyway.
|
||||
if rv.status_code == 304:
|
||||
rv.headers.pop("x-sendfile", None)
|
||||
|
||||
return rv
|
||||
250
odoo-bringout-oca-ocb-base/odoo/tools/_vendor/sessions.py
Normal file
250
odoo-bringout-oca-ocb-base/odoo/tools/_vendor/sessions.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
r"""
|
||||
Vendored copy of https://github.com/pallets/werkzeug/blob/2b2c4c3dd3cf7389e9f4aa06371b7332257c6289/src/werkzeug/contrib/sessions.py
|
||||
|
||||
werkzeug.contrib was removed from werkzeug 1.0. sessions (and secure
|
||||
cookies) were moved to the secure-cookies package. Problem is distros
|
||||
are starting to update werkzeug to 1.0 without having secure-cookies
|
||||
(e.g. Arch has done so, Debian has updated python-werkzeug in
|
||||
"experimental"), which will be problematic once that starts trickling
|
||||
down onto more stable distros and people start deploying that.
|
||||
|
||||
Edited some to fix imports and remove some compatibility things
|
||||
(mostly PY2) and the unnecessary (to us) SessionMiddleware
|
||||
|
||||
:copyright: 2007 Pallets
|
||||
:license: BSD-3-Clause
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from hashlib import sha1
|
||||
from os import path, replace as rename
|
||||
from odoo.tools.misc import pickle
|
||||
from time import time
|
||||
|
||||
from werkzeug.datastructures import CallbackDict
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_sha1_re = re.compile(r"^[a-f0-9]{40}$")
|
||||
|
||||
|
||||
def generate_key(salt=None):
|
||||
if salt is None:
|
||||
salt = repr(salt).encode("ascii")
|
||||
return sha1(b"".join([salt, str(time()).encode("ascii"), os.urandom(30)])).hexdigest()
|
||||
|
||||
|
||||
class ModificationTrackingDict(CallbackDict):
|
||||
__slots__ = ("modified", "on_update")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def on_update(self):
|
||||
self.modified = True
|
||||
|
||||
self.modified = False
|
||||
CallbackDict.__init__(self, on_update=on_update)
|
||||
dict.update(self, *args, **kwargs)
|
||||
|
||||
def copy(self):
|
||||
"""Create a flat copy of the dict."""
|
||||
missing = object()
|
||||
result = object.__new__(self.__class__)
|
||||
for name in self.__slots__:
|
||||
val = getattr(self, name, missing)
|
||||
if val is not missing:
|
||||
setattr(result, name, val)
|
||||
return result
|
||||
|
||||
def __copy__(self):
|
||||
return self.copy()
|
||||
|
||||
|
||||
class Session(ModificationTrackingDict):
|
||||
"""Subclass of a dict that keeps track of direct object changes. Changes
|
||||
in mutable structures are not tracked, for those you have to set
|
||||
`modified` to `True` by hand.
|
||||
"""
|
||||
|
||||
__slots__ = ModificationTrackingDict.__slots__ + ("sid", "new")
|
||||
|
||||
def __init__(self, data, sid, new=False):
|
||||
ModificationTrackingDict.__init__(self, data)
|
||||
self.sid = sid
|
||||
self.new = new
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s%s>" % (
|
||||
self.__class__.__name__,
|
||||
dict.__repr__(self),
|
||||
"*" if self.should_save else "",
|
||||
)
|
||||
|
||||
@property
|
||||
def should_save(self):
|
||||
"""True if the session should be saved.
|
||||
|
||||
.. versionchanged:: 0.6
|
||||
By default the session is now only saved if the session is
|
||||
modified, not if it is new like it was before.
|
||||
"""
|
||||
return self.modified
|
||||
|
||||
|
||||
class SessionStore(object):
|
||||
"""Baseclass for all session stores. The Werkzeug contrib module does not
|
||||
implement any useful stores besides the filesystem store, application
|
||||
developers are encouraged to create their own stores.
|
||||
|
||||
:param session_class: The session class to use. Defaults to
|
||||
:class:`Session`.
|
||||
"""
|
||||
|
||||
def __init__(self, session_class=None):
|
||||
if session_class is None:
|
||||
session_class = Session
|
||||
self.session_class = session_class
|
||||
|
||||
def is_valid_key(self, key):
|
||||
"""Check if a key has the correct format."""
|
||||
return _sha1_re.match(key) is not None
|
||||
|
||||
def generate_key(self, salt=None):
|
||||
"""Simple function that generates a new session key."""
|
||||
return generate_key(salt)
|
||||
|
||||
def new(self):
|
||||
"""Generate a new session."""
|
||||
return self.session_class({}, self.generate_key(), True)
|
||||
|
||||
def save(self, session):
|
||||
"""Save a session."""
|
||||
|
||||
def save_if_modified(self, session):
|
||||
"""Save if a session class wants an update."""
|
||||
if session.should_save:
|
||||
self.save(session)
|
||||
|
||||
def delete(self, session):
|
||||
"""Delete a session."""
|
||||
|
||||
def get(self, sid):
|
||||
"""Get a session for this sid or a new session object. This method
|
||||
has to check if the session key is valid and create a new session if
|
||||
that wasn't the case.
|
||||
"""
|
||||
return self.session_class({}, sid, True)
|
||||
|
||||
|
||||
#: used for temporary files by the filesystem session store
|
||||
_fs_transaction_suffix = ".__wz_sess"
|
||||
|
||||
|
||||
class FilesystemSessionStore(SessionStore):
|
||||
"""Simple example session store that saves sessions on the filesystem.
|
||||
This store works best on POSIX systems and Windows Vista / Windows
|
||||
Server 2008 and newer.
|
||||
|
||||
.. versionchanged:: 0.6
|
||||
`renew_missing` was added. Previously this was considered `True`,
|
||||
now the default changed to `False` and it can be explicitly
|
||||
deactivated.
|
||||
|
||||
:param path: the path to the folder used for storing the sessions.
|
||||
If not provided the default temporary directory is used.
|
||||
:param filename_template: a string template used to give the session
|
||||
a filename. ``%s`` is replaced with the
|
||||
session id.
|
||||
:param session_class: The session class to use. Defaults to
|
||||
:class:`Session`.
|
||||
:param renew_missing: set to `True` if you want the store to
|
||||
give the user a new sid if the session was
|
||||
not yet saved.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path=None,
|
||||
filename_template="werkzeug_%s.sess",
|
||||
session_class=None,
|
||||
renew_missing=False,
|
||||
mode=0o644,
|
||||
):
|
||||
SessionStore.__init__(self, session_class)
|
||||
if path is None:
|
||||
path = tempfile.gettempdir()
|
||||
self.path = path
|
||||
assert not filename_template.endswith(_fs_transaction_suffix), (
|
||||
"filename templates may not end with %s" % _fs_transaction_suffix
|
||||
)
|
||||
self.filename_template = filename_template
|
||||
self.renew_missing = renew_missing
|
||||
self.mode = mode
|
||||
|
||||
def get_session_filename(self, sid):
|
||||
# out of the box, this should be a strict ASCII subset but
|
||||
# you might reconfigure the session object to have a more
|
||||
# arbitrary string.
|
||||
return path.join(self.path, self.filename_template % sid)
|
||||
|
||||
def save(self, session):
|
||||
fn = self.get_session_filename(session.sid)
|
||||
fd, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix, dir=self.path)
|
||||
f = os.fdopen(fd, "wb")
|
||||
try:
|
||||
pickle.dump(dict(session), f, pickle.HIGHEST_PROTOCOL)
|
||||
finally:
|
||||
f.close()
|
||||
try:
|
||||
rename(tmp, fn)
|
||||
os.chmod(fn, self.mode)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def delete(self, session):
|
||||
fn = self.get_session_filename(session.sid)
|
||||
try:
|
||||
os.unlink(fn)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def get(self, sid):
|
||||
if not self.is_valid_key(sid):
|
||||
return self.new()
|
||||
try:
|
||||
f = open(self.get_session_filename(sid), "rb")
|
||||
except IOError:
|
||||
_logger.debug('Could not load session from disk. Use empty session.', exc_info=True)
|
||||
if self.renew_missing:
|
||||
return self.new()
|
||||
data = {}
|
||||
else:
|
||||
try:
|
||||
try:
|
||||
data = pickle.load(f, errors={})
|
||||
except Exception:
|
||||
_logger.debug('Could not load session data. Use empty session.', exc_info=True)
|
||||
data = {}
|
||||
finally:
|
||||
f.close()
|
||||
return self.session_class(data, sid, False)
|
||||
|
||||
def list(self):
|
||||
"""Lists all sessions in the store.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
"""
|
||||
before, after = self.filename_template.split("%s", 1)
|
||||
filename_re = re.compile(
|
||||
r"%s(.{5,})%s$" % (re.escape(before), re.escape(after))
|
||||
)
|
||||
result = []
|
||||
for filename in os.listdir(self.path):
|
||||
#: this is a session that is still being saved.
|
||||
if filename.endswith(_fs_transaction_suffix):
|
||||
continue
|
||||
match = filename_re.match(filename)
|
||||
if match is not None:
|
||||
result.append(match.group(1))
|
||||
return result
|
||||
204
odoo-bringout-oca-ocb-base/odoo/tools/_vendor/useragents.py
Normal file
204
odoo-bringout-oca-ocb-base/odoo/tools/_vendor/useragents.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.useragents
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides a helper to inspect user agent strings. This module
|
||||
is far from complete but should work for most of the currently available
|
||||
browsers.
|
||||
|
||||
|
||||
:copyright: 2007 Pallets
|
||||
:license: BSD-3-Clause
|
||||
|
||||
This package was vendored in odoo in order to prevent errors with werkzeug 2.1
|
||||
"""
|
||||
import re
|
||||
|
||||
|
||||
class UserAgentParser(object):
|
||||
"""A simple user agent parser. Used by the `UserAgent`."""
|
||||
|
||||
platforms = (
|
||||
("cros", "chromeos"),
|
||||
("iphone|ios", "iphone"),
|
||||
("ipad", "ipad"),
|
||||
(r"darwin|mac|os\s*x", "macos"),
|
||||
("win", "windows"),
|
||||
(r"android", "android"),
|
||||
("netbsd", "netbsd"),
|
||||
("openbsd", "openbsd"),
|
||||
("freebsd", "freebsd"),
|
||||
("dragonfly", "dragonflybsd"),
|
||||
("(sun|i86)os", "solaris"),
|
||||
(r"x11|lin(\b|ux)?", "linux"),
|
||||
(r"nintendo\s+wii", "wii"),
|
||||
("irix", "irix"),
|
||||
("hp-?ux", "hpux"),
|
||||
("aix", "aix"),
|
||||
("sco|unix_sv", "sco"),
|
||||
("bsd", "bsd"),
|
||||
("amiga", "amiga"),
|
||||
("blackberry|playbook", "blackberry"),
|
||||
("symbian", "symbian"),
|
||||
)
|
||||
browsers = (
|
||||
("googlebot", "google"),
|
||||
("msnbot", "msn"),
|
||||
("yahoo", "yahoo"),
|
||||
("ask jeeves", "ask"),
|
||||
(r"aol|america\s+online\s+browser", "aol"),
|
||||
("opera", "opera"),
|
||||
("edge", "edge"),
|
||||
("chrome|crios", "chrome"),
|
||||
("seamonkey", "seamonkey"),
|
||||
("firefox|firebird|phoenix|iceweasel", "firefox"),
|
||||
("galeon", "galeon"),
|
||||
("safari|version", "safari"),
|
||||
("webkit", "webkit"),
|
||||
("camino", "camino"),
|
||||
("konqueror", "konqueror"),
|
||||
("k-meleon", "kmeleon"),
|
||||
("netscape", "netscape"),
|
||||
(r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"),
|
||||
("lynx", "lynx"),
|
||||
("links", "links"),
|
||||
("Baiduspider", "baidu"),
|
||||
("bingbot", "bing"),
|
||||
("mozilla", "mozilla"),
|
||||
)
|
||||
|
||||
_browser_version_re = r"(?:%s)[/\sa-z(]*(\d+[.\da-z]+)?"
|
||||
_language_re = re.compile(
|
||||
r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|"
|
||||
r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platforms]
|
||||
self.browsers = [
|
||||
(b, re.compile(self._browser_version_re % a, re.I))
|
||||
for a, b in self.browsers
|
||||
]
|
||||
|
||||
def __call__(self, user_agent):
|
||||
for platform, regex in self.platforms: # noqa: B007
|
||||
match = regex.search(user_agent)
|
||||
if match is not None:
|
||||
break
|
||||
else:
|
||||
platform = None
|
||||
for browser, regex in self.browsers: # noqa: B007
|
||||
match = regex.search(user_agent)
|
||||
if match is not None:
|
||||
version = match.group(1)
|
||||
break
|
||||
else:
|
||||
browser = version = None
|
||||
match = self._language_re.search(user_agent)
|
||||
if match is not None:
|
||||
language = match.group(1) or match.group(2)
|
||||
else:
|
||||
language = None
|
||||
return platform, browser, version, language
|
||||
|
||||
|
||||
class UserAgent(object):
|
||||
"""Represents a user agent. Pass it a WSGI environment or a user agent
|
||||
string and you can inspect some of the details from the user agent
|
||||
string via the attributes. The following attributes exist:
|
||||
|
||||
.. attribute:: string
|
||||
|
||||
the raw user agent string
|
||||
|
||||
.. attribute:: platform
|
||||
|
||||
the browser platform. The following platforms are currently
|
||||
recognized:
|
||||
|
||||
- `aix`
|
||||
- `amiga`
|
||||
- `android`
|
||||
- `blackberry`
|
||||
- `bsd`
|
||||
- `chromeos`
|
||||
- `dragonflybsd`
|
||||
- `freebsd`
|
||||
- `hpux`
|
||||
- `ipad`
|
||||
- `iphone`
|
||||
- `irix`
|
||||
- `linux`
|
||||
- `macos`
|
||||
- `netbsd`
|
||||
- `openbsd`
|
||||
- `sco`
|
||||
- `solaris`
|
||||
- `symbian`
|
||||
- `wii`
|
||||
- `windows`
|
||||
|
||||
.. attribute:: browser
|
||||
|
||||
the name of the browser. The following browsers are currently
|
||||
recognized:
|
||||
|
||||
- `aol` *
|
||||
- `ask` *
|
||||
- `baidu` *
|
||||
- `bing` *
|
||||
- `camino`
|
||||
- `chrome`
|
||||
- `edge`
|
||||
- `firefox`
|
||||
- `galeon`
|
||||
- `google` *
|
||||
- `kmeleon`
|
||||
- `konqueror`
|
||||
- `links`
|
||||
- `lynx`
|
||||
- `mozilla`
|
||||
- `msie`
|
||||
- `msn`
|
||||
- `netscape`
|
||||
- `opera`
|
||||
- `safari`
|
||||
- `seamonkey`
|
||||
- `webkit`
|
||||
- `yahoo` *
|
||||
|
||||
(Browsers marked with a star (``*``) are crawlers.)
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
the version of the browser
|
||||
|
||||
.. attribute:: language
|
||||
|
||||
the language of the browser
|
||||
"""
|
||||
|
||||
_parser = UserAgentParser()
|
||||
|
||||
def __init__(self, environ_or_string):
|
||||
if isinstance(environ_or_string, dict):
|
||||
environ_or_string = environ_or_string.get("HTTP_USER_AGENT", "")
|
||||
self.string = environ_or_string
|
||||
self.platform, self.browser, self.version, self.language = self._parser(
|
||||
environ_or_string
|
||||
)
|
||||
|
||||
def to_header(self):
|
||||
return self.string
|
||||
|
||||
def __str__(self):
|
||||
return self.string
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.browser)
|
||||
|
||||
__bool__ = __nonzero__
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r/%s>" % (self.__class__.__name__, self.browser, self.version)
|
||||
Loading…
Add table
Add a link
Reference in a new issue