ソースを参照

feat: add `session.get_info()` to get session info

wangweimin 5 年 前
コミット
fe6b0b0b64

+ 8 - 3
pywebio/platform/aiohttp.py

@@ -2,12 +2,14 @@ import asyncio
 import fnmatch
 import logging
 from functools import partial
-from urllib.parse import urlparse
 from os import path, listdir
+from urllib.parse import urlparse
+
 from aiohttp import web
 
 from .tornado import open_webbrowser_on_server_started
 from ..session import CoroutineBasedSession, ThreadBasedSession, register_session_implement_for_target, AbstractSession
+from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, STATIC_PATH
 
 logger = logging.getLogger(__name__)
@@ -63,11 +65,14 @@ def _webio_handler(target, session_cls, websocket_settings, check_origin_func=_i
             ioloop.create_task(ws.close())
             logger.debug("WebSocket closed from session")
 
+        session_info = get_session_info_from_headers(request.headers)
         if session_cls is CoroutineBasedSession:
-            session = CoroutineBasedSession(target, on_task_command=send_msg_to_client,
+            session = CoroutineBasedSession(target, session_info=session_info,
+                                            on_task_command=send_msg_to_client,
                                             on_session_close=close_from_session)
         elif session_cls is ThreadBasedSession:
-            session = ThreadBasedSession(target, on_task_command=send_msg_to_client,
+            session = ThreadBasedSession(target, session_info=session_info,
+                                         on_task_command=send_msg_to_client,
                                          on_session_close=close_from_session, loop=ioloop)
         else:
             raise RuntimeError("Don't support session type:%s" % session_cls)

+ 3 - 1
pywebio/platform/httpbased.py

@@ -22,6 +22,7 @@ from typing import Dict
 import time
 
 from ..session import CoroutineBasedSession, AbstractSession, register_session_implement_for_target
+from ..session.base import get_session_info_from_headers
 from ..utils import random_str, LRUDict
 
 
@@ -146,7 +147,8 @@ class HttpHandler:
 
             webio_session_id = random_str(24)
             context.set_header('webio-session-id', webio_session_id)
-            webio_session = self.session_cls(self.target)
+            session_info = get_session_info_from_headers(context.request_headers())
+            webio_session = self.session_cls(self.target, session_info=session_info)
             cls._webio_sessions[webio_session_id] = webio_session
         elif request_headers['webio-session-id'] not in cls._webio_sessions:  # WebIOSession deleted
             context.set_content([dict(command='close_session')], json_type=True)

+ 6 - 2
pywebio/platform/tornado.py

@@ -16,6 +16,7 @@ from tornado.websocket import WebSocketHandler
 
 from ..session import CoroutineBasedSession, ThreadBasedSession, ScriptModeSession, \
     register_session_implement_for_target, AbstractSession
+from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, wait_host_port, STATIC_PATH
 
 logger = logging.getLogger(__name__)
@@ -78,11 +79,14 @@ def _webio_handler(target, session_cls, check_origin_func=_is_same_site):
 
             self._close_from_session_tag = False  # 由session主动关闭连接
 
+            session_info = get_session_info_from_headers(self.request.headers)
             if session_cls is CoroutineBasedSession:
-                self.session = CoroutineBasedSession(target, on_task_command=self.send_msg_to_client,
+                self.session = CoroutineBasedSession(target, session_info=session_info,
+                                                     on_task_command=self.send_msg_to_client,
                                                      on_session_close=self.close_from_session)
             elif session_cls is ThreadBasedSession:
-                self.session = ThreadBasedSession(target, on_task_command=self.send_msg_to_client,
+                self.session = ThreadBasedSession(target, session_info=session_info,
+                                                  on_task_command=self.send_msg_to_client,
                                                   on_session_close=self.close_from_session,
                                                   loop=asyncio.get_event_loop())
             else:

+ 36 - 1
pywebio/session/__init__.py

@@ -1,4 +1,6 @@
 r"""
+
+.. autofunction:: get_info
 .. autofunction:: run_async
 .. autofunction:: run_asyncio_coroutine
 .. autofunction:: register_thread
@@ -21,7 +23,7 @@ from ..utils import iscoroutinefunction, isgeneratorfunction, run_as_function, t
 # 当前进程中正在使用的会话实现的列表
 _active_session_cls = []
 
-__all__ = ['run_async', 'run_asyncio_coroutine', 'register_thread', 'hold', 'defer_call']
+__all__ = ['run_async', 'run_asyncio_coroutine', 'register_thread', 'hold', 'defer_call', 'get_info']
 
 
 def register_session_implement_for_target(target_func):
@@ -175,3 +177,36 @@ def defer_call(func):
     """
     get_current_session().defer_call(func)
     return func
+
+
+def get_info():
+    """ 获取当前会话的相关信息
+
+    :return: 表示会话信息的对象,属性有:
+
+       * ``user_agent`` : 表示用户浏览器信息的对象,属性有
+
+            * ``is_mobile`` (bool): 用户使用的设备是否为手机 (比如 iPhone, Android phones, Blackberry, Windows Phone 等设备)
+            * ``is_tablet`` (bool): 用户使用的设备是否为平板 (比如 iPad, Kindle Fire, Nexus 7 等设备)
+            * ``is_pc`` (bool): 用户使用的设备是否为桌面电脑 (比如运行 Windows, OS X, Linux 的设备)
+            * ``is_touch_capable`` (bool): 用户使用的设备是否支持触控
+
+            * ``browser.family`` (str): 浏览器家族. 比如 'Mobile Safari'
+            * ``browser.version`` (tuple): 浏览器版本元组. 比如 (5, 1)
+            * ``browser.version_string`` (str): 浏览器版本字符串. 比如 '5.1'
+
+            * ``os.family`` (str): 操作系统家族. 比如 'iOS'
+            * ``os.version`` (tuple): 操作系统版本元组. 比如 (5, 1)
+            * ``os.version_string`` (str): 操作系统版本字符串. 比如 '5.1'
+
+            * ``device.family`` (str): 设备家族. 比如 'iPhone'
+            * ``device.brand`` (str): 设备品牌. 比如 'Apple'
+            * ``device.model`` (str): 设备幸好. 比如 'iPhone'
+
+       * ``user_language`` (str): 用户操作系统使用的语言. 比如 ``'zh-CN'``
+       * ``server_host`` (str): 当前会话的服务器host,包含域名和端口,端口为80时可以被省略
+       * ``origin`` : 当前用户的页面地址. 包含 协议、主机、端口 部分. 比如 ``'http://localhost:8080'`` .
+         只在当用户的页面地址不在当前服务器下(即 主机、端口部分和 ``server_host`` 不一致)时有值.
+    返回值的 ``user_agent`` 属性是通过user_agents库进行解析生成的。参见 https://github.com/selwin/python-user-agents#usage
+    """
+    return get_current_session().info

+ 33 - 2
pywebio/session/base.py

@@ -1,7 +1,14 @@
+import user_agents
+from ..utils import ObjectDict
+
+
 class AbstractSession:
     """
     会话对象,由Backend创建
 
+    属性:
+        info 表示会话信息的对象
+
     由Task在当前Session上下文中调用:
         get_current_session
         get_current_task_id
@@ -27,6 +34,7 @@ class AbstractSession:
         后端Backend在接收到用户浏览器的数据后,会通过调用 ``send_client_event`` 来通知会话,进而由Session驱动协程的运行。
         Task内在调用输入输出函数后,会调用 ``send_task_command`` 向会话发送输入输出消息指令, Session将其保存并留给后端Backend处理。
     """
+    info = object()
 
     @staticmethod
     def active_session_count() -> int:
@@ -40,9 +48,10 @@ class AbstractSession:
     def get_current_task_id():
         raise NotImplementedError
 
-    def __init__(self, target, on_task_command=None, on_session_close=None, **kwargs):
+    def __init__(self, target, session_info, on_task_command=None, on_session_close=None, **kwargs):
         """
         :param target:
+        :param session_info: 会话信息。可以通过 Session.info 访问
         :param on_task_command: Backend向ession注册的处理函数,当 Session 收到task发送的command时调用
         :param on_session_close: Backend向Session注册的处理函数,当 Session task 执行结束时调用 *
         :param kwargs:
@@ -88,4 +97,26 @@ class AbstractSession:
 
         :param func: 话结束时调用的函数
         """
-        raise NotImplementedError
+        raise NotImplementedError
+
+
+def get_session_info_from_headers(headers):
+    """从Http请求头中获取会话信息
+
+    :param headers: 字典类型的Http请求头
+    :return: 表示会话信息的对象,属性有:
+
+       * ``user_agent`` : 用户浏览器信息。可用字段见 https://github.com/selwin/python-user-agents#usage
+       * ``user_language`` : 用户操作系统使用的语言
+       * ``server_host`` : 当前会话的服务器host,包含域名和端口,端口为80时可以被省略
+       * ``origin`` : 当前用户的页面地址. 包含 协议、主机、端口 部分. 比如 ``'http://localhost:8080'`` .
+         只在当用户的页面地址不在当前服务器下(即 主机、端口部分和 ``server_host`` 不一致)时有值.
+    """
+    ua_str = headers.get('User-Agent', '')
+    ua = user_agents.parse(ua_str)
+    user_language = headers.get('Accept-Language', '').split(',', 1)[0].split(' ', 1)[0].split(';', 1)[0]
+    server_host = headers.get('Host', '')
+    origin = headers.get('Origin', '')
+    session_info = ObjectDict(user_agent=ua, user_language=user_language,
+                              server_host=server_host, origin=origin)
+    return session_info

+ 2 - 1
pywebio/session/coroutinebased.py

@@ -66,7 +66,7 @@ class CoroutineBasedSession(AbstractSession):
             raise RuntimeError("No current task found in context!")
         return _context.current_task_id
 
-    def __init__(self, target, on_task_command=None, on_session_close=None):
+    def __init__(self, target, session_info, on_task_command=None, on_session_close=None):
         """
         :param target: 协程函数
         :param on_task_command: 由协程内发给session的消息的处理函数
@@ -77,6 +77,7 @@ class CoroutineBasedSession(AbstractSession):
 
         CoroutineBasedSession._active_session_cnt += 1
 
+        self.info = session_info
         self._on_task_command = on_task_command or (lambda _: None)
         self._on_session_close = on_session_close or (lambda: None)
 

+ 8 - 3
pywebio/session/threadbased.py

@@ -7,7 +7,8 @@ from functools import wraps
 
 from .base import AbstractSession
 from ..exceptions import SessionNotFoundException, SessionClosedException, SessionException
-from ..utils import random_str, LimitedSizeQueue, isgeneratorfunction, iscoroutinefunction, catch_exp_call, get_function_name
+from ..utils import random_str, LimitedSizeQueue, isgeneratorfunction, iscoroutinefunction, catch_exp_call, \
+    get_function_name
 
 logger = logging.getLogger(__name__)
 
@@ -54,7 +55,7 @@ class ThreadBasedSession(AbstractSession):
         tname = getattr(tname, '__name__', tname)
         return '%s-%s' % (tname, id(thread))
 
-    def __init__(self, target, on_task_command=None, on_session_close=None, loop=None):
+    def __init__(self, target, session_info, on_task_command=None, on_session_close=None, loop=None):
         """
         :param target: 会话运行的函数
         :param on_task_command: 当Task内发送Command给session的时候触发的处理函数
@@ -68,6 +69,7 @@ class ThreadBasedSession(AbstractSession):
 
         ThreadBasedSession._active_session_cnt += 1
 
+        self.info = session_info
         self._on_task_command = on_task_command or (lambda _: None)
         self._on_session_close = on_session_close or (lambda: None)
         self._loop = loop
@@ -316,8 +318,10 @@ class ScriptModeSession(ThreadBasedSession):
 
     instance = None
 
-    def __init__(self, thread, on_task_command=None, loop=None):
+    def __init__(self, thread, session_info, on_task_command=None, loop=None):
         """
+
+        :param thread: 第一次调用PyWebIO交互函数的线程 todo 貌似本参数并不必要
         :param on_task_command: 会话结束的处理函数。后端Backend在相应on_session_close时关闭连接时,
             需要保证会话内的所有消息都传送到了客户端
         :param loop: 事件循环。若 on_task_command 或者on_session_close中有调用使用asyncio事件循环的调用,
@@ -329,6 +333,7 @@ class ScriptModeSession(ThreadBasedSession):
 
         ThreadBasedSession._active_session_cnt += 1
 
+        self.info = session_info
         self._on_task_command = on_task_command or (lambda _: None)
         self._on_session_close = lambda: None
         self._loop = loop

+ 14 - 0
pywebio/utils.py

@@ -16,6 +16,20 @@ project_dir = dirname(abspath(__file__))
 STATIC_PATH = '%s/html' % project_dir
 
 
+class ObjectDict(dict):
+    """
+    Object like dict, every dict[key] can visite by dict.key
+
+    If dict[key] is `Get`, calculate it's value.
+    """
+
+    def __getattr__(self, name):
+        ret = self.__getitem__(name)
+        if hasattr(ret, '__get__'):
+            return ret.__get__(self, ObjectDict)
+        return ret
+
+
 def catch_exp_call(func, logger):
     """运行函数,将捕获异常记录到日志
 

+ 1 - 0
requirements.txt

@@ -1,4 +1,5 @@
 tornado>=4.3.0
+python-user-agents
 
 # extra support
 flask

+ 1 - 0
setup.py

@@ -77,6 +77,7 @@ setup(
     ],
     install_requires=[
         'tornado>=4.3.0',  # After this version, the new async/await keywords in Python 3.5 are supported
+        'python-user-agents',
     ],
     extras_require=extras_require,
     project_urls={

+ 26 - 0
test/template.py

@@ -126,6 +126,32 @@ def basic_output():
     put_text('to remove', anchor='to_remove')
     remove('to_remove')
 
+    session_info = get_info()
+    put_markdown(rf"""### 会话信息
+    ```
+    * `user_agent`:
+        * `is_mobile` (bool): {session_info.user_agent.is_mobile}
+        * `is_tablet` (bool): {session_info.user_agent.is_tablet}
+        * `is_pc` (bool): {session_info.user_agent.is_pc}
+        * `is_touch_capable` (bool): {session_info.user_agent.is_touch_capable}
+
+        * `browser.family` (str): {session_info.user_agent.browser.family}
+        * `browser.version` (tuple): {session_info.user_agent.browser.version}
+        * `browser.version_string` (str): {session_info.user_agent.browser.version_string}
+
+        * `os.family` (str): {session_info.user_agent.os.family}
+        * `os.version` (tuple): {session_info.user_agent.os.version}
+        * `os.version_string` (str): {session_info.user_agent.os.version_string}
+
+        * `device.family` (str): {session_info.user_agent.device.family}
+        * `device.brand` (str): {session_info.user_agent.device.brand}
+        * `device.model` (str): {session_info.user_agent.device.model}
+    * `user_language` (str): {session_info.user_language}
+    * `server_host` (str): {session_info.server_host}
+    * `origin` (str): {session_info.origin}
+    ```
+    """, strip_indent=4)
+
 
 def background_output():
     put_text("Background output", anchor='background')