Исходный код trassir_script_framework

# -*- coding: utf-8 -*-
"""
<parameters>
    <company>AATrubilin</company>
    <title>trassir_script_framework</title>
    <version>0.64</version>
</parameters>
"""

# _SERVICE_VERSION = 0.64
tbot_service = """
    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
"""

import re
import os
import sys
import ssl
import time
import host
import json

import pickle
import base64
import ftplib
import urllib
import urllib2
import httplib
import logging
import threading
import subprocess

if os.name == "nt":
    import winsound

from Queue import Queue
from functools import wraps
from collections import deque
from xml.etree import ElementTree
from __builtin__ import object as py_object
from datetime import datetime, date, timedelta
from logging.handlers import RotatingFileHandler

logger = logging.getLogger()


class ScriptError(Exception):
    """Base script exception"""

    _host_api = host

    def __init__(self, *args):
        super(ScriptError, self).__init__(*args)
        self._host_api.timeout(1, self.rise_from_thread)

    def rise_from_thread(self):
        raise self


class HostLogHandler(logging.Handler):
    """Trassir main log handler"""

    def __init__(self, host_api=host):
        super(HostLogHandler, self).__init__()
        self._host_api = host_api

    def emit(self, record):
        msg = self.format(record)
        self._host_api.log_message(msg)


class PopupHandler(logging.Handler):
    """Trassir popup handler"""

    def __init__(self, host_api=host):
        super(PopupHandler, self).__init__()
        self._host_api = host_api
        self._popups = {
            "CRITICAL": host_api.error,
            "FATAL": host_api.error,
            "ERROR": host_api.error,
            "WARN": host_api.alert,
            "WARNING": host_api.alert,
            "INFO": host_api.message,
            "DEBUG": host_api.message,
            "NOTSET": host_api.message,
        }

    def emit(self, record):
        msg = self.format(record)
        self._popups[record.levelname](msg)


class DuplicateFilter(logging.Filter):
    """Suppressing multiple messages with same content.

    Tracking last logged record and filter out any
    repeated (similar) records. Output something more rsyslog style.
    
    Example:
        --- The last message repeated 3 times
    """

    def __init__(self):
        super(DuplicateFilter, self).__init__()
        self._last_log = None
        self._last_log_count = 1

    def filter(self, record):
        record.duplicates = ""
        current_log = (record.module, record.levelno, record.msg)
        if current_log == self._last_log:
            self._last_log_count += 1
            return False
        else:
            if self._last_log_count > 1:
                record.duplicates = (
                    "--- The last message repeated %s times\n" % self._last_log_count
                )
            self._last_log = current_log
            self._last_log_count = 1
            return True


class BaseUtils:
    """Base utils for your scripts"""

    _host_api = host
    _FOLDERS = {obj[1]: obj[3] for obj in host.objects_list("Folder")}
    _TEXT_FILE_EXTENSIONS = [".txt", ".csv", ".log"]
    _LPR_FLAG_BITS = {
        "LPR_UP": 0x00001,
        "LPR_DOWN": 0x00002,
        "LPR_BLACKLIST": 0x00004,
        "LPR_WHITELIST": 0x00008,
        "LPR_INFO": 0x00010,
        "LPR_FIRST_LANE": 0x01000,
        "LPR_SECOND_LANE": 0x02000,
        "LPR_THIRD_LANE": 0x04000,
        "LPR_EXT_DB_ERROR": 0x00020,
        "LPR_CORRECTED": 0x00040,
    }
    _EVENT_STR_TO_INT = {
        "Border Crossed A -> B": -2010220362,
        "Border Crossed B -> A": 881900680,
        "Border %1 A-B Crossing": 1745631458,
        "Border %1 B-A Crossing": 1382034490,
        "Border %1 Unique Object A-B Crossing": -1764400102,
        "Border %1 Unique Object B-A Crossing": -755097134,
        "Connected To %1 under %2": -567223767,
        "Connection Established": 1689573124,
        "Connection Lost": -1739961019,
        "Deny: %1 (%2)": 1400866841,
        "Disconnected From %1": 854687023,
        "FACS Connected": 928164014,
        "FACS Disconnected": -528751441,
        "Face Detected": -145480902,
        "Face Recognized": 1904675878,
        "Fire Detected": -2095846277,
        "Fire Stopped": 1556160195,
        "HDD Broken": -359176531,
        "HDD Error": -2035571413,
        "HDD Restored": 2054776042,
        "Health Turns Bad": -1338064969,
        "Health Turns Good": 1737407416,
        "Input High to Low": 1260011944,
        "Input Low to High": 108469542,
        "Login Failed, %1 from %2": -1785217387,
        "Login Successful, %1 from %2": 1634136664,
        "Logout, %1 from %2": 334348171,
        "Motion Start": -1960416690,
        "Motion Stop": 452886769,
        "No Connection to Cloud": -1220531757,
        "Object Entered the Zone": -1484834142,
        "Object Left the Zone": 1838034845,
        "Output High to Low": -994975116,
        "Output Low to High": 842360770,
        "Pass: %1 (%2)": 1944146750,
        "Photo Detected": -220640968,
        "Script: %1": 865778551,
        "Shutdown": 390175606,
        "Signal Lost": -997068283,
        "Signal Restored": -1801421619,
        "Slow Down Detected": -438590449,
        "Software update to version %1 succeeded": 1188419157,
        "Startup": -37228692,
        "Tracked Object Left Zone %1": 456308509,
        "Tracked Unique Object Entered Zone %1": -1766980008,
    }
    _EVENT_INT_TO_STR = {v: k for k, v in _EVENT_STR_TO_INT.iteritems()}
    _IMAGE_EXT = [".png", ".jpg", ".jpeg", ".bmp"]
    _HTML_IMG_TEMPLATE = """<img src="data:image/png;base64,{img}" {attr}>"""

    _SCR_DEFAULT_NAMES = [
        "Yeni skript",
        "Unnamed Script",
        "უსახელო სკრიპტი",
        "Жаңа скрипт",
        "Script nou",
        "Новый скрипт",
        "Yeni skript dosyası",
        "Новий скрипт",
        "未命名脚本",
    ]

    def __init__(self):
        pass

    # noinspection PyUnusedLocal
    @staticmethod
    def do_nothing(*args, **kwargs):
        """Ничего не делает.

        Returns:
            :obj:`bool`: ``True``
        """
        return True

    @classmethod
    def run_as_thread_v2(cls, locked=False, daemon=True):
        """Декоратор для запуска функций в отдельном потоке.

        Args:
            locked (:obj:`bool`, optional): Если :obj:`True` - запускает поток с блокировкой
                доступа к ресурсам. По умолчанию :obj:`False`
            daemon (:obj:`bool`, optional): Устанавливает значение :obj:`threading.Thread.daemon`.
                По умолчанию :obj:`True`

        Examples:
            >>> import time
            >>>
            >>>
            >>> @BaseUtils.run_as_thread_v2()
            >>> def run_count_timer():
            ...     time.sleep(1)
            ...     host.stats()["run_count"] += 1
            >>>
            >>>
            >>> run_count_timer()
        """
        lock = threading.Lock()

        def wrapped(fn):
            @wraps(fn)
            def run(*args, **kwargs):
                def raise_exc(err):
                    # noinspection PyShadowingNames
                    args = list(err.args)
                    args[0] = "[{}]: {}".format(fn.__name__, args[0])
                    err.args = args
                    raise err

                def locked_fn(*args_, **kwargs_):
                    lock.acquire()
                    try:
                        return fn(*args_, **kwargs_)
                    except Exception as err:
                        cls._host_api.timeout(1, lambda: raise_exc(err))
                    finally:
                        lock.release()

                def unlocked_fn(*args_, **kwargs_):
                    try:
                        return fn(*args_, **kwargs_)
                    except Exception as err:
                        cls._host_api.timeout(1, lambda: raise_exc(err))

                t = threading.Thread(
                    target=locked_fn if locked else unlocked_fn,
                    args=args,
                    kwargs=kwargs,
                )
                t.daemon = daemon
                t.start()
                return t

            return run

        return wrapped

[документация] @staticmethod def run_as_thread(fn): """Декоратор для запуска функций в отдельном потоке. Returns: :obj:`threading.Thread`: Функция в отдельном потоке Examples: >>> import time >>> >>> >>> @BaseUtils.run_as_thread >>> def run_count_timer(): ... time.sleep(1) ... host.stats()["run_count"] += 1 >>> >>> >>> run_count_timer() """ @wraps(fn) def run(*args, **kwargs): t = threading.Thread(target=fn, args=args, kwargs=kwargs) t.daemon = True t.start() return t return run
@staticmethod def catch_request_exceptions(func): """Catch request errors""" @wraps(func) def wrapped(self, *args, **kwargs): try: return func(self, *args, **kwargs) except urllib2.HTTPError as e: return e.code, "HTTPError: {}".format(e.code) except urllib2.URLError as e: return e.reason, "URLError: {}".format(e.reason) except httplib.HTTPException as e: return e, "HTTPException: {}".format(e) except ssl.SSLError as e: return e.errno, "SSLError: {}".format(e) return wrapped
[документация] @staticmethod def win_encode_path(path): """Изменяет кодировку на ``"cp1251"`` для WinOS. Args: path (:obj:`str`): Путь до файла или папки Returns: :obj:`str`: Декодированый путь до файла или папки Examples: >>> path = r"D:/Shots/Скриншот.jpeg" >>> os.path.isfile(path) False >>> os.path.isfile(BaseUtils.win_encode_path(path)) True """ if os.name == "nt": try: path = path.decode("utf8") except (UnicodeDecodeError, UnicodeEncodeError): pass return path
[документация] @staticmethod def is_file_exists(file_path, tries=1): """Проверяет, существует ли файл. Проверка происходит в течении ``tries`` секунд. Warning: | Запускайте функцию только в отдельном потоке если ``tries > 1`` | Вторая и последующие проверки производятся с ``time.sleep(1)`` Args: file_path (:obj:`str`): Полный путь до файла tries (:obj:`int`, optional): Количество проверок. По умолчанию ``tries=1`` Returns: :obj:`bool`: ``True`` if file exists, ``False`` otherwise Examples: >>> BaseUtils.is_file_exists("_t1server.settings") True """ file_path_encoded = BaseUtils.win_encode_path(file_path) if os.path.isfile(file_path) or os.path.isfile(file_path_encoded): return True for x in xrange(tries - 1): time.sleep(1) if os.path.isfile(file_path) or os.path.isfile(file_path_encoded): return True return False
[документация] @staticmethod def is_folder_exists(folder): """Проверяет существование папки и доступ на запись. Args: folder (:obj:`str`): Путь к папке. Raises: IOError: Если папка не существует Examples: >>> BaseUtils.is_folder_exists("/test_path") IOError: Folder '/test_path' is not exists """ if not os.path.isdir(folder): raise IOError("Folder '{}' is not exists".format(folder)) readme_file = os.path.join(folder, "readme.txt") with open(readme_file, "w") as f: f.write( "If you see this file - Trassir script have no access to remove it!" ) os.remove(readme_file)
[документация] @classmethod def is_template_exists(cls, template_name): """Проверяет существование шаблона Args: template_name (:obj:`str`): Имя шаблона Returns: :obj:`bool`: :obj:`True` если шаблон существует, иначе :obj:`False` """ for tmpl_ in cls._host_api.settings("templates").ls(): if tmpl_.name == template_name: return True return False
[документация] @classmethod def cat(cls, filepath, check_ext=True): """Выводит на отображение текстовую инфомрацию. Tip: - *WinOS*: открывает файл программой по умолчанию - *TrassirOS*: открывает файл в терминале с помощью утилиты `cat` Note: | Доступные расширения файлов: ``[".txt", ".csv", ".log"]`` | Если открываете файл с другим расширением установите ``check_ext=False`` Args: filepath (:obj:`str`): Полный путь до файла check_ext (:obj:`bool`, optional): Если ``True`` - проверяет расширение файла. По умолчанию ``True`` Examples: >>> BaseUtils.cat("/home/trassir/ Trassir 3 License.txt") .. image:: images/base_utils.cat.png Raises: :class:`TypeError`: Если ``check_ext=True`` расширение файла нет в списке :obj:`_TEXT_FILE_EXTENSIONS` """ if check_ext: _, ext = os.path.splitext(filepath) if ext not in cls._TEXT_FILE_EXTENSIONS: raise TypeError( "Bad file extension: {}. To ignore this: set check_ext=False".format( ext ) ) if os.name == "nt": os.startfile(filepath) else: subprocess.Popen( [ "xterm -fg black -bg white -geometry 90x35 -fn " "-misc-fixed-medium-r-normal--18-120-100-100-c-90-iso10646-1 -e bash -c \"cat '{}'; " "read -n 1 -s -r -p '\n\nPress any key to exit'; exit\"".format( filepath ) ], shell=True, close_fds=True, )
@classmethod def _json_serializer(cls, data): """JSON serializer for objects not serializable by default""" if isinstance(data, (datetime, date)): return data.isoformat() elif isinstance(data, cls._host_api.ScriptHost.SE_Settings): return "settings('{}')".format(data.path) elif isinstance(data, cls._host_api.ScriptHost.SE_Object): return "object('{}')".format(data.guid) return type(data).__name__
[документация] @classmethod def to_json(cls, data, **kwargs): """Сериализация объекта в JSON стрку Note: Не вызывает ошибку при сериализации объектов :obj:`datetime`, :obj:`date`, :obj:`SE_Settings`, :obj:`SE_Object` Args: data (:obj:`obj`): Объект для сериализации Returns: :obj:`str`: JSON строка Examples: >>> obj = {"now": datetime.now()} >>> json.dumps(obj) TypeError: datetime.datetime(2019, 4, 2, 18, 01, 33, 881000) is not JSON serializable >>> BaseUtils.to_json(obj, indent=None) '{"now": "2019-04-02T18:01:33.881000"}' """ return json.dumps(data, default=cls._json_serializer, **kwargs)
[документация] @staticmethod def ts_to_dt(ts): """Конвертирует timestamp в :obj:`datetime` объект Args: ts (:obj:`int`): Timestamp Returns: :obj:`datetime`: Datetime объект Examples: >>> BaseUtils.ts_to_dt(1564109694242000) datetime.datetime(2019, 7, 26, 9, 54, 54, 242000) """ if ts > 1e10: ts_sec = int(ts / 1e6) ts_ms = int(ts - ts_sec * 1e6) else: ts_sec = int(ts) ts_ms = 0 return datetime.fromtimestamp(ts_sec) + timedelta(microseconds=ts_ms)
[документация] @staticmethod def dt_to_ts(dt): """Конвертирует :obj:`datetime` объект в trassir timestamp Args: dt (:obj:`datetime`): Datetime Returns: :obj:`int`: Trassir timestamp Examples: >>> BaseUtils.ts_to_dt(datetime(2019, 7, 26, 9, 54, 54, 242000)) 1564109694242000 """ return int(int(time.mktime(dt.timetuple())) * 1e6 + dt.microsecond)
[документация] @classmethod def lpr_flags_decode(cls, flags): """Преобразует флаги события AutoTrassir Приводит флаги события человекочитаемый список Note: Список доступных флагов: - ``LPR_UP`` - Направление движения вверх - ``LPR_DOWN`` - Направление движения вниз - ``LPR_BLACKLIST`` - Номер в черном списке - ``LPR_WHITELIST`` - Номер в черном списке - ``LPR_INFO`` - Номер в информационном списке - ``LPR_FIRST_LANE`` - Автомобиль двигается по первой полосе - ``LPR_SECOND_LANE`` - Автомобиль двигается по второй полосе - ``LPR_THIRD_LANE`` - Автомобиль двигается по третей полосе - ``LPR_EXT_DB_ERROR`` - Ошибка во внешнем списке - ``LPR_CORRECTED`` - Номер исправлен оператором Args: flags (:obj:`int`): Биты LPR события. Как правило аргумент :obj:`ev.flags` события :obj:`SE_LprEvent` AutoTrassir. Например :obj:`536870917` Returns: List[:obj:`str`]: Список флагов Examples: >>> BaseUtils.lpr_flags_decode(536870917) ['LPR_UP', 'LPR_BLACKLIST'] """ return [bit for bit, code in cls._LPR_FLAG_BITS.iteritems() if (flags & code)]
[документация] @classmethod def event_type_encode(cls, event_type): """Преобразует тип события :obj:`str` -> :obj:`int` Note: События в БД хранятся в :obj:`int`, в скриптах приходят в человекочитаемом, строковом формате. Args: event_type (:obj:`str`): Тип события как в скриптах. Examples: >>> BaseUtils.event_type_encode("Border Crossed A -> B") -2010220362 Returns: :obj:`int`: Тип события как в БД """ if not isinstance(event_type, str): raise TypeError("Expected str, got {}".format(type(event_type).__name__)) return cls._EVENT_STR_TO_INT.get(event_type)
[документация] @classmethod def event_type_decode(cls, event_type): """Преобразует тип события :obj:`int` -> :obj:`str` Note: События в БД хранятся в :obj:`int`, в скриптах приходят в человекочитаемом, строковом формате. Args: event_type (:obj:`int`): Тип события как в БД. Examples: >>> BaseUtils.event_type_encode(-2010220362) "Border Crossed A -> B" Returns: :obj:`str`: Тип события как в скриптах """ if not isinstance(event_type, int): raise TypeError("Expected int, got {}".format(type(event_type).__name__)) return cls._EVENT_INT_TO_STR.get(event_type)
[документация] @classmethod def image_to_base64(cls, image): """Создает base64 из изображения Args: image (:obj:`str`): Путь к изображению или изображение Returns: :obj:`str`: Base64 image Examples: >>> BaseUtils.image_to_base64(r"manual/en/cloud-devices-16.png") 'iVBORw0KGgoAAAANSUhEUgAAB1MAAAH0CAYAAABo5wRhAAAACXBIWXMAAC4jA...' >>> BaseUtils.image_to_base64(open(r"manual/en/cloud-devices-16.png", "rb").read()) 'iVBORw0KGgoAAAANSUhEUgAAB1MAAAH0CAYAAABo5wRhAAAACXBIWXMAAC4jA...' """ _, ext = os.path.splitext(image) if ext.lower() in cls._IMAGE_EXT: image = cls.win_encode_path(image) if not BaseUtils.is_file_exists(image): return "" with open(image, "rb") as image_file: image = image_file.read() return base64.b64encode(image)
[документация] @classmethod def base64_to_html_img(cls, image_base64, **kwargs): """Возвращает base64 изображение в `<img>` html теге Args: image_base64 (:obj:`str`): Base64 image **kwargs: HTML `<img>` tag attributes. Подробнее на `html.com <https://html.com/tags/img/#Attributes_of_img>`_ Returns: :obj:`str`: html image Examples: >>> base64_image = BaseUtils.image_to_base64(r"manual/en/cloud-devices-16.png") >>> html_image = BaseUtils.base64_to_html_img(base64_image, width=280, height=75) >>> html_image '<img src="data:image/png;base64,iVBORw0KGgoAA...Jggg==" width="280" height="75">' >>> host.message(html_image) .. image:: images/popup_sender.image.png """ html_img = cls._HTML_IMG_TEMPLATE.format( img=image_base64, attr=" ".join( '%s="%s"' % (key, value) for key, value in kwargs.iteritems() ), ) return html_img
[документация] @staticmethod def save_pkl(file_path, data): """Сохраняет данные в `.pkl` файл Args: file_path (:obj:`str`): Путь до файла data: Данные для сохранения Returns: :obj:`str`: Абсолютный путь до файла Examples: >>> data = {"key": "value"} >>> BaseUtils.save_pkl("saved_data.pkl", data) 'D:\\DSSL\\Trassir-4.1-Client\\saved_data.pkl' """ if not file_path.endswith(".pkl"): file_path = file_path + ".pkl" with open(file_path, "wb") as opened_file: pickle.dump(data, opened_file) return os.path.abspath(file_path)
[документация] @staticmethod def load_pkl(file_path, default_type=dict): """Загружает данные из `.pkl` файла Args: file_path (:obj:`str`): Путь до файла default_type (optional): Тип данных, возвращаемый при неудачной загрузке данных из файла. По умолчанию :obj:`dict` Returns: Данные из файла или :obj:`default_type()` Examples: >>> BaseUtils.load_pkl("fake_saved_data.pkl") {} >>> BaseUtils.load_pkl("fake_saved_data.pkl", default_type=list) [] >>> BaseUtils.load_pkl("fake_saved_data.pkl", default_type=int) 0 >>> BaseUtils.load_pkl("fake_saved_data.pkl", default_type=str) '' >>> BaseUtils.load_pkl("saved_data.pkl") {'key': 'value'} """ if not file_path.endswith(".pkl"): file_path = file_path + ".pkl" data = default_type() if os.path.isfile(file_path): try: with open(file_path, "rb") as opened_file: data = pickle.load(opened_file) except (EOFError, IndexError, ValueError, TypeError): """ dump file is empty or broken """ return data
@classmethod def get_object(cls, obj_id): """Возвращает объект Trassir, если он доступен, иначе ``None`` Args: obj_id (:obj:`str`): Guid объекта или его имя Returns: :obj:`ScriptHost.SE_Object`: Объект Trassir или ``None`` Examples: >>> obj = BaseUtils.get_object("EZJ4QnbC") >>> if obj is None: ... host.error("Object not found") ... else: ... host.message("Object name is {0.name}".format(obj)) """ if not isinstance(obj_id, (str, unicode)): raise TypeError( "Expected str or unicode, got '{}'".format(type(obj_id).__name__) ) obj = cls._host_api.object(obj_id) try: obj.name except EnvironmentError: """Object not found""" obj = None return obj @classmethod def get_object_name_by_guid(cls, guid): """Возвращает имя объекта Trassir по его guid Tip: Можно использовать: - guid объекта ``"CFsuNBzt"`` - guid объекта + guid сервера ``"CFsuNBzt_pV4ggECb"`` Args: guid (:obj:`str`): Guid объекта Trassir Returns: :obj:`str`: Имя объекта, если объект найден, иначе ``guid`` Examples: >>> BaseUtils.get_object_name_by_guid("EZJ4QnbC") 'AC-D2141IR3' >>> BaseUtils.get_object_name_by_guid("EZJ4QnbC-") 'EZJ4QnbC-' """ guid = guid.split("_", 1)[0] obj = cls.get_object(guid) if obj is None: name = guid else: name = obj.name return name @classmethod def get_full_guid(cls, obj_id): """Возвращает полный guid объекта Args: obj_id (:obj:`str`): Guid объекта или его имя Returns: :obj:`str`: Полный guid объекта """ tr_obj = cls.get_object(obj_id) if tr_obj is not None: for obj in cls._host_api.objects_list(""): if tr_obj.guid == obj[1]: return "{}_{}".format(obj[1], cls._FOLDERS.get(obj[3], obj[3]))
[документация] @classmethod def get_operator_gui(cls): """Возвращает объект интерфейса оператора Returns: :obj:`OperatorGUI`: Объект интерфейса оператора Raises: ScriptError: Если не удается загрузить интерфейс Examples: Открыть интерфейс Trassir а мониторе №1 >>> operator_gui = BaseUtils.get_operator_gui() >>> operator_gui.raise_monitor(1) """ obj = cls.get_object("operatorgui_{}".format(cls._host_api.settings("").guid)) if obj is None: raise ScriptError("Failed to load operator gui") return obj
[документация] @classmethod def get_server_guid(cls): """Возвращает guid текущего сервра Returns: :obj:`str`: Guid сервера Examples: >>> BaseUtils.get_server_guid() 'client' """ return cls._host_api.settings("").guid
[документация] @classmethod def get_script_name(cls): """Возвращает имя текущего скрипта Returns: :obj:`str`: Имя скрипта Examples: >>> BaseUtils.get_script_name() 'Новый скрипт' """ return cls._host_api.stats().parent()["name"] or __name__
[документация] @classmethod def get_screenshot_folder(cls): """Возвращает путь до папки скриншотов При этом производит проверку папки методом :meth:`BaseUtils.is_folder_exists` Returns: :obj:`str`: Полный путь к папке скриншотов Examples: >>> BaseUtils.get_screenshot_folder() '/home/trassir/shots' """ folder = cls._host_api.settings("system_wide_options")["screenshots_folder"] cls.is_folder_exists(folder) return folder
[документация] @classmethod def get_logger( cls, host_log="WARNING", popup_log="ERROR", file_log=None, file_name=None, file_max_bytes=5 * 1024 * 1024, file_backup_count=2, ): """Возвращает логгер с предустановленными хэндлерами Доступные хэндлеры: - *host_log*: Пишет сообщения в основной лог сервера _t1server.log - *popup_log*: Показывает всплывающие сообщения ``message/alert/error`` - *file_log*: Пишет сообщения в отдельный файл в папку скриншотов Для каждого хэндлера можно установить разный уровень логирования По умолчанию ``host_log="WARNING"`` и ``popup_log="ERROR"`` Note: Имя файла лога можно указать с расширение ".log" или без. See Also: `Logging levels на сайте docs.python.org <https://docs.python.org/2/library/logging.html#logging-levels>`_ Args: host_log (:obj:`str`, optional): Уровень логирования в основной лог. По умолчанию ``"WARNING"`` popup_log (:obj:`str`, optional): Уровень логирования во всплывающих сообщениях. По умолчанию ``"ERROR"`` file_log (:obj:`str`, optional): Уровень логирования в отдельный файл По умолчанию :obj:`None` file_name (:obj:`str`, optional): Имя файла для логирования. По умолчанию :obj:`None` и равно ``<имени скрипта>.log`` file_max_bytes (:obj:`int`, optional): Максимальный размер файла лога в байтах. По умолчанию :obj:`5 * 1024 * 1024` file_backup_count (:obj:`int`, optional): Макссимальное кол-во бэкапов лога. По умолчанию :obj:`2` Returns: :obj:`logging.logger`: Логгер Examples: >>> logger = BaseUtils.get_logger() >>> logger.warning("My warning message") >>> try: ... # noinspection PyUnresolvedReferences ... do_something() ... except NameError: ... logger.error("Function is not defined", exc_info=True) """ logger_ = logging.getLogger(__name__) logger_.setLevel("DEBUG") def _remove_handlers(): """Close and remove handlers on disable script""" logger_.info("Remove %s handlers..." % len(logger_.handlers)) for handler in logger_.handlers[:]: handler.close() logger_.removeHandler(handler) cls._host_api.register_finalizer(_remove_handlers) if host_log: host_handler = HostLogHandler() host_handler.setLevel(host_log) host_formatter = logging.Formatter( "[%(levelname)-8s] %(lineno)-4s <%(funcName)s> - %(message)s" ) host_handler.setFormatter(host_formatter) logger_.addHandler(host_handler) if popup_log: popup_handler = PopupHandler() popup_handler.setLevel(popup_log) popup_formatter = logging.Formatter( fmt="<b>[%(levelname)s]</b> Line: %(lineno)s<br><i>%(message).630s</i>" ) popup_handler.setFormatter(popup_formatter) logger_.addHandler(popup_handler) if file_log: if file_name is None: file_name = cls.get_script_name() if not file_name.endswith(".log"): file_name = "{}.log".format(file_name) file_path = os.path.join(cls.get_screenshot_folder(), file_name) file_path = cls.win_encode_path(file_path) file_handler = RotatingFileHandler( file_path, maxBytes=file_max_bytes, backupCount=file_backup_count ) file_handler.setLevel(file_log) file_formatter = logging.Formatter( fmt="%(duplicates)s%(asctime)s [%(levelname)-8s] %(lineno)-4s <%(funcName)s> - %(message)s", datefmt="%Y/%m/%d %H:%M:%S", ) file_handler.setFormatter(file_formatter) file_handler.addFilter(DuplicateFilter()) logger_.addHandler(file_handler) return logger_
[документация] @classmethod def set_script_name(cls, fmt=None, script_name=None): """Автоматически изменяет имя скрипта Новое имя скрипта создается на основе `параметров <https://www.dssl.ru/files/trassir/manual/ru/setup-script-parameters.html>`_ скрипта. По желанию можно изменить шаблон имени. По умолчанию :obj:`"{title} v{version}"` Note: Имя изменяется только если сейчас у скрипта стандартное имя, например :obj:`"Новый скрипт"` или :obj:`"Unnamed Script"` и др. Args: fmt (:obj:`str`, optional): Шаблон имени скрипта. По умолчанию :obj:`None` script_name (:obj:`str`, optional): Имя скрипта. Если не задано - парсит имя из параметров. По умолчанию :obj:`None` Examples: >>> BaseUtils.set_script_name() 'trassir_script_framework v0.4' >>> BaseUtils.set_script_name(fmt="{title}") 'trassir_script_framework' """ if cls._host_api.stats().parent()["name"] in cls._SCR_DEFAULT_NAMES: if script_name is None: try: root = ElementTree.fromstring(__doc__) except ElementTree.ParseError: root = None if root is None: company, title, version = None, None, None else: company = root.find("company") if root else None title = root.find("title") if root else None version = root.find("version") if root else None if fmt is None: fmt = "{title} v{version}" script_name = fmt.format( company="DSSL" if company is None else company.text, title="Script" if title is None else title.text, version="0.1" if version is None else version.text, ) cls._host_api.stats().parent()["name"] = script_name return script_name
if globals().get("DEBUG", False): logger = BaseUtils.get_logger(host_log="DEBUG", popup_log="WARNING", file_log="DEBUG") else: logger = BaseUtils.get_logger() class Worker(threading.Thread): """Thread executing tasks from a given tasks queue""" def __init__(self, tasks): super(Worker, self).__init__() self.tasks = tasks self.daemon = True self.start() self.task_working = False def run(self): while __name__ in sys.modules.keys(): if not self.tasks.empty(): self.task_working = True func, args, kwargs = self.tasks.get(timeout=1) # noinspection PyBroadException try: func(*args, **kwargs) except: logger.exception("ThreadPool Worker error") finally: self.tasks.task_done() else: self.task_working = False class ThreadPool: """Pool of threads consuming tasks from a queue""" def __init__(self, num_threads, host_api=host): self._host_api = host_api self.tasks = Queue() self.workers = [Worker(self.tasks) for _ in xrange(num_threads)] @property def working(self): for worker in self.workers: if worker.task_working: return True return False def add_task(self, func, *args, **kargs): """Add a task to the queue""" self.tasks.put((func, args, kargs)) def wait_completion(self): """Wait for completion of all the tasks in the queue""" self.tasks.join()
[документация]class HTTPRequester(py_object): """Framework for urllib2 See Also: https://docs.python.org/2/library/urllib2.html#urllib2.build_opener Args: opener (:obj:`urllib2.OpenerDirector`, optional): Обработчик запросов. По умолчанию :obj:`None` timeout (:obj:`int`, optional): Время ожидания запроса, в секундах. По умолчанию :obj:`timeout=10` Examples: Пример запроса к SDK Trassir >>> # Отключение проверки сертификата >>> context = ssl.create_default_context() >>> context.check_hostname = False >>> context.verify_mode = ssl.CERT_NONE >>> >>> handler = urllib2.HTTPSHandler(context=context) >>> opener = urllib2.build_opener(handler) >>> >>> requests = HTTPRequester(opener, timeout=20) >>> response = requests.get( ... "https://172.20.0.101:8080/login", ... params={"username": "Admin", "password": "12345"} ... ) >>> >>> response.code 200 >>> response.text '{\\n "sid" : "T6LAAcxg",\\n "success" : 1\\n}\\n' >>> response.json {u'success': 1, u'sid': u'T6LAAcxg'} """
[документация] class Response(py_object): """Класс ответа от сервера Attributes: code (:obj:`str` | :obj:`int`): Код ответа сервера text (:obj:`str`): Текст ответа json (:obj:`dict` | :obj:`list`): Создает объект из json ответа """ def __init__(self, *args): self.code, self.text = args @property def json(self): return json.loads(self.text)
def __init__(self, opener=None, timeout=10): if opener is None: handler = urllib2.BaseHandler() opener = urllib2.build_opener(handler) self._opener = opener self.timeout = timeout @BaseUtils.catch_request_exceptions def _get_response(self, request): """Returns response Args: request (:obj:`urllib2.Request`): This class is an abstraction of a URL request """ response = self._opener.open(request, timeout=self.timeout) return response.code, response.read() @staticmethod def _parse_params(**params): """Params get string params Args: **params (dict): Keyword arguments Returns: str: params string """ return "&".join( "{key}={value}".format(key=key, value=value) for key, value in params.iteritems() ) @staticmethod def _prepare_headers(headers): """Prepare headers for request""" if headers is None: headers = {} if "User-Agent" not in headers: headers["User-Agent"] = "TrassirScript" return headers
[документация] def get(self, url, params=None, headers=None): """Создает GET запрос по указанному :obj:`url` Args: url (:obj:`str`): Url для запроса params (:obj:`dict`, optional): Параметры GET запроса headers (:obj:`dict`, optional): Заголовки запроса Examples: >>> requests = HTTPRequester() >>> response = requests.get( ... "http://httpbin.org/get", ... params={"PARAMETER": "TEST"}, ... ) >>> response.code 200 >>> response.text '{\\n "args": {\\n "PARAMETER": "TEST"\\n }, \\n ...' >>> response.json {u'args': {u'PARAMETER': u'TEST'}, ...} Returns: :class:`HTTPRequester.Response`: Response instance """ if params is not None: url += "?{params}".format(params=self._parse_params(**params)) headers = self._prepare_headers(headers) request = urllib2.Request(url, headers=headers) response = self._get_response(request) return self.Response(*response)
[документация] def post(self, url, data=None, headers=None): """Создает POST запрос по указанному :obj:`url` Args: url (:obj:`str`): Url для запроса data (:obj:`dict`, optional): Данные POST запроса headers (:obj:`dict`, optional): Заголовки запроса Examples: >>> requests = HTTPRequester() >>> response = requests.post( ... "http://httpbin.org/post", ... data={"PARAMETER": "TEST"}, ... headers={"Content-Type": "application/json"}, ... ) >>> response.code 200 >>> response.text '{\\n "args": {\\n "PARAMETER": "TEST"\\n }, \\n ...' >>> response.json {u'args': {u'PARAMETER': u'TEST'}, ...} Returns: :class:`HTTPRequester.Response`: Response instance """ if data is None: data = {} if isinstance(data, dict): data = urllib.urlencode(data) headers = self._prepare_headers(headers) request = urllib2.Request(url, data=data, headers=headers) response = self._get_response(request) return self.Response(*response)
[документация]class ScriptObject(host.TrassirObject, py_object): """Создает объект для генерации событий Args: name (:obj:`str`, optional): Имя объекта. По умолчанию :obj:`None` guid (:obj:`str`, optional): Guid объекта. По умолчанию :obj:`None` parent (:obj:`str`, optional): Guid родительского объекта. По умолчанию :obj:`None` Note: - Имя объекта по умолчанию - :meth:`BaseUtils.get_script_name` - Guid объекта по умолчанию строится по шаблноу ``"{script_guid}_object"`` - Guid родительского объекта по умолчанию - :meth:`BaseUtils.get_server_guid` Examples: >>> # Создаем объект >>> scr_obj = ScriptObject() >>> # Проверяем текущее состояние объекта >>> scr_obj.health 'OK' >>> # Установить флаг возле объекта >>> scr_obj.check_me = True >>> # Сгенерировать событие с текстом >>> scr_obj.fire_event_v2("New event") """ def __init__(self, name=None, guid=None, parent=None, host_api=host): super(ScriptObject, self).__init__("Script") self._host_api = host_api scr_parent = host_api.stats().parent() self._name = name or BaseUtils.get_script_name() self.set_name(self._name) self._guid = guid or "{}-object".format(scr_parent.guid) self.set_guid(self._guid) self._parent = parent or BaseUtils.get_server_guid() self.set_parent(self._parent) self._folder = "" self._health = "OK" self._check_me = True self.set_initial_state([self._health, self._check_me]) host_api.object_add(self) self.context_menu = [] @property def health(self): """:obj:`"OK"` | :obj:`"Error"`: Состояние объекта""" return self._health @health.setter def health(self, value): if value in ["OK", "Error"]: self.set_state([value, self._check_me]) self._health = value else: raise ValueError("Expected 'OK' or 'Error', got '{}'".format(value)) @property def check_me(self): """:obj:`bool`: Флаг ``check_me`` объекта""" return bool(1 - self._check_me) @check_me.setter def check_me(self, value): if isinstance(value, bool) or value in [1, 0]: value = 1 - value self.set_state([self._health, value]) self._check_me = value else: raise ValueError("Expected bool or 1|0, got '{}'".format(value)) @property def name(self): """:obj:`str`: Имя объекта""" return self._name @name.setter def name(self, value): if isinstance(value, str): self.set_name(value) self._name = value else: raise ValueError("Expected str, got {}".format(type(value).__name__)) @property def folder(self): """:obj:`str`: Папка объекта""" return self._folder @folder.setter def folder(self, value): if not value: raise ValueError("Object guid can't be empty") if isinstance(value, str): if self._folder: self.change_folder(value) else: self.set_folder(value) self._folder = value else: raise ValueError("Expected str, got {}".format(type(value).__name__))
[документация] def context_menu_button(self, text, callback): """Добавляет кнопку в контекстное меню объекта Args: text (:obj:`str`): Текст кнопки callback (:obj:`function`): Функция, которая вызывается при нажатии на кнопку. В качестве единственного аргумента функция приимает текущий объект (:obj:`host.object(self.guid)`). Returns: :obj:`SE_ContextCatcher`: Хендлер контекстного меню Raises: ValueError: Если пустой текст кнопки. TypeError: Если callback нельзя вызвать в качестве функции. Examples: >>> scr = ScriptObject() >>> >>> def switch(obj): ... check_me = scr.check_me ... scr.check_me = not check_me ... btn.set_name("ON" if check_me else "OFF") >>> >>> btn = scr.context_menu_button("ON", switch) >>> btn <host.SE_ContextCatcher object at 0x17B01A98> >>> scr.context_menu [('ON', 'switch', <host.SE_ContextCatcher object at 0x17B01A98>)] """ if not text: raise ValueError("No text") if not callable(callback): raise TypeError("Callback function is not callable") btn = self._host_api.activate_on_context_menu(self._guid, text, callback) self.context_menu.append((text, callback.__name__, btn)) return btn
[документация] def fire_event_v2(self, message, channel="", data=""): """Создает событие в Trassir Args: message (:obj:`str`): Сообщение события (``p1``) channel (:obj:`str`, optional): Ассоциированный с событием канал (``p2``) data (:obj:`str`, optional): Дополнительные данные (``p3``) """ if not isinstance(data, str): data = BaseUtils.to_json(data, indent=None) self.fire_event("Script: %1", message, channel, data)
class ShotSaverError(ScriptError): """Base ShotSaver Exception""" pass
[документация]class ShotSaver(py_object): """Класс для сохранения скриншотов Args: shot_awaiting_time (:obj:`int`, optional): Время ожидания скриншота, с. По умолчанию :obj:`5`. tries_to_make_shot (:obj:`int`, optional): Кол-во попыток сохранить скриншот. Если в течении времени `shot_awaiting_time` скриншот не был сохранен - производится следующая попытка сохранить скриншот. По умолчанию :obj:`2` pool_size (:obj:`int`): Размер пула. По умолчанию :obj:`10` """ _SHOT_NAME_TEMPLATE = ( "{name} (%Y.%m.%d %H-%M-%S).jpg" ) # Template for shot file name def __init__( self, shot_awaiting_time=5, tries_to_make_shot=2, pool_size=10, host_api=host ): self._shot_awaiting_time = shot_awaiting_time self._tries_to_make_shot = tries_to_make_shot self._thread_pool = None self._pool_size = pool_size self._host_api = host_api self._screenshots_folder = BaseUtils.get_screenshot_folder() @property def pool_size(self): """:obj:`int`: Размер пула для метода :obj:`pool_shot` Устанавливает размер пула (кол-во одновременно созданных задач сохранения скриншотов). По умолчанию :obj:`10`. Warnings: Изменить данный параметр можно только до первого вызова метода :obj:`pool_shot`. После вызовет :obj:`RuntimeError` Raises: RuntimeError: Если пул уже создан. """ return self._pool_size @pool_size.setter def pool_size(self, value): if self._thread_pool is None: self._pool_size = value else: raise RuntimeError("You can't change pool size when workers created") @property def pool_queue_size(self): """:obj:`int`: Размер текущей очереди в пуле Возвращает текущий размер очереди в пуле. Note: Если пул еще не был созда (метод :obj:`pool_shot` не вызывался) данный метод вернет :obj:`-1` """ if self._thread_pool is None: return -1 else: return self._thread_pool.tasks.qsize() @property def pool_working(self): """:obj:`bool`: :obj:`True` если в пуле есть не законченные задачи Note: Если пул еще не был созда (метод :obj:`pool_shot` не вызывался) данный метод вернет :obj:`None` """ if self._thread_pool is None: return None else: return self._thread_pool.working @property def screenshots_folder(self): """:obj:`str`: Папка для сохранения скриншотов по умолчанию Устанавливает новый путь по умолчанию для сохранения скриншотов, если папка не существует - создает папку. Или возвращает текущий путь для сохранения скриншотов. Note: По молчанию :obj:`screenshots_folder` = :meth:`BaseUtils.get_screenshot_folder` Raises: OSError: Если возникает ошибка при создании папки """ return self._screenshots_folder @screenshots_folder.setter def screenshots_folder(self, folder): if not os.path.isdir(folder): try: os.makedirs(folder) except OSError as err: raise OSError("Can't make dir '{}': {}".format(folder, err)) self._screenshots_folder = folder
[документация] def shot(self, channel_full_guid, dt=None, file_name=None, file_path=None): """Делает скриншот с указанного канала Note: По умолчанию: - :obj:`dt=datetime.now()` - :obj:`file_name="{name} (%Y.%m.%d %H-%M-%S).jpg"`, где ``{name}`` - имя канала Args: channel_full_guid (:obj:`str`): Полный guid анала. Например: ``"CFsuNBzt_pV4ggECb"`` dt (:obj:`datetime.datetime`, optional): :obj:`datetime.datetime` для скриншота. По умолчанию :obj:`None` file_name (:obj:`str`, optional): Имя файла с расширением. По умолчанию :obj:`None` file_path (:obj:`str`, optional): Путь для сохранения скриншота. По умолчанию :obj:`None` Returns: :obj:`str`: Полный путь до скриншота Raises: ValueError: Если в guid канала отсутствует guid сервера TypeError: Если ``isinstance(dt, (datetime, date)) is False`` Examples: >>> ss = ShotSaver() >>> ss.shot("e80kgBLh_pV4ggECb") '/home/trassir/shots/AC-D2141IR3 Склад (2019.04.03 15-58-26).jpg' """ logger.debug( "ShotSaver.shot({channel_full_guid}, dt={dt}, file_name={file_name}, file_path={file_path})".format( channel_full_guid=repr(channel_full_guid), dt=repr(dt), file_name=repr(file_name), file_path=repr(file_path), ) ) if "_" not in channel_full_guid: raise ValueError( "Expected full channel guid, got {}".format(channel_full_guid) ) if dt is None: ts = "0" dt = datetime.now() else: if not isinstance(dt, (datetime, date)): raise TypeError("Expected datetime, got {}".format(type(dt).__name__)) ts = dt.strftime("%Y%m%d_%H%M%S") if file_name is None: file_name = dt.strftime( self._SHOT_NAME_TEMPLATE.format( name=BaseUtils.get_object_name_by_guid(channel_full_guid) ) ) if file_path is None: file_path = self.screenshots_folder self._host_api.screenshot_v2_figures( channel_full_guid, file_name, file_path, ts ) return os.path.join(file_path, file_name)
def _async_shot( self, channel_full_guid, dt=None, file_name=None, file_path=None, callback=None ): """Вызывает ``callback`` после сохнанения скриншота * Метод работает в отдельном потоке * Вызывает функцию :meth:`ShotSaver.shot` * Ждет выполнения функции :meth:`BaseUtils.check_file` ``tries=10`` * Вызвает ``callback`` функцию Args: channel_full_guid (:obj:`str`): Полный guid канала. Например: ``"CFsuNBzt_pV4ggECb"`` dt (:obj:`datetime.datetime`, optional): :obj:`datetime.datetime` для скриншота. По умолчанию :obj:`None` file_name (:obj:`str`, optional): Имя файла с расширением. По умолчанию :obj:`None` file_path (:obj:`str`, optional): Путь для сохранения скриншота. По умолчанию :obj:`None` callback (:obj:`function`): Callable function """ if callback is None: callback = BaseUtils.do_nothing shot_file = "" for _ in xrange(self._tries_to_make_shot): shot_file = self.shot( channel_full_guid, dt=dt, file_name=file_name, file_path=file_path ) if BaseUtils.is_file_exists( BaseUtils.win_encode_path(shot_file), self._shot_awaiting_time ): self._host_api.timeout(100, lambda: callback(True, shot_file)) break else: self._host_api.timeout(100, lambda: callback(False, shot_file))
[документация] @BaseUtils.run_as_thread def async_shot( self, channel_full_guid, dt=None, file_name=None, file_path=None, callback=None ): """async_shot(channel_full_guid, dt=None, file_name=None, file_path=None, callback=None) Вызывает ``callback`` после сохнанения скриншота * Метод работает в отдельном потоке * Вызывает функцию :meth:`ShotSaver.shot` * Ждет выполнения функции :meth:`BaseUtils.check_file` ``tries=10`` * Вызвает ``callback`` функцию Args: channel_full_guid (:obj:`str`): Полный guid канала. Например: ``"CFsuNBzt_pV4ggECb"`` dt (:obj:`datetime.datetime`, optional): :obj:`datetime.datetime` для скриншота. По умолчанию :obj:`None` file_name (:obj:`str`, optional): Имя файла с расширением. По умолчанию :obj:`None` file_path (:obj:`str`, optional): Путь для сохранения скриншота. По умолчанию :obj:`None` callback (:obj:`function`, optional): Функциюя, которая вызывается после сохранения скриншота. В качестве аргументов должна принимать `success`, `shot_path`. По умолчанию :obj:`None` Returns: :obj:`threading.Thread`: Thread object Examples: >>> # noinspection PyUnresolvedReferences >>> def callback(success, shot_path): ... # Пример callback функции ... # Args: ... # success (bool): True если скриншот успешно сохранен, иначе False ... # shot_path (str): Полный путь до скриншота ... if success: ... host.message("Скриншот успешно сохранен<br>%s" % shot_path) ... else: ... host.error("Ошибка сохранения скриншота <br>%s" % shot_path) >>> >>> ss = ShotSaver() >>> ss.async_shot("e80kgBLh_pV4ggECb", callback=callback) """ self._async_shot( channel_full_guid, dt=dt, file_name=file_name, file_path=file_path, callback=callback, )
@BaseUtils.run_as_thread def _pool_awaiting(self): self._thread_pool.wait_completion() # noinspection PyIncorrectDocstring
[документация] def pool_shot(self, *args, **kwargs): """pool_shot(channel_full_guid, dt=None, file_name=None, file_path=None, callback=None) Сохраняет скриншоты в пуле. Одновременно в работе не более :obj:`ShotSaver.pool_size` задач. Warnings: Данный метод создает :obj:`ShotSaver.pool_size` доп. потоков. Потоки удаляются при отключении скрипта. Args: channel_full_guid (:obj:`str`): Полный guid канала. Например: ``"CFsuNBzt_pV4ggECb"`` dt (:obj:`datetime.datetime`, optional): :obj:`datetime.datetime` для скриншота. По умолчанию :obj:`None` file_name (:obj:`str`, optional): Имя файла с расширением. По умолчанию :obj:`None` file_path (:obj:`str`, optional): Путь для сохранения скриншота. По умолчанию :obj:`None` callback (:obj:`function`, optional): Функциюя, которая вызывается после сохранения скриншота. В качестве аргументов должна принимать `success`, `shot_path`. По умолчанию :obj:`None` Examples: >>> ss = ShotSaver() >>> ss.pool_size = 2 >>> >>> ss.pool_shot("e80kgBLh_pV4ggECb") >>> ss.pool_shot("e80kgBLh_pV4ggECb") >>> ss.pool_shot("e80kgBLh_pV4ggECb") >>> ss.pool_shot("e80kgBLh_pV4ggECb") >>> >>> ss.pool_queue_size 4 """ if self._thread_pool is None: self._thread_pool = ThreadPool(self._pool_size) self._thread_pool.add_task(self._async_shot, *args, **kwargs)
class VideoExporterError(ScriptError): """Base ShotSaver Exception""" pass
[документация]class VideoExporter(py_object): """Класс для экспорта видео Examples: Смена папки экспорта видео по умолчанию >>> ss = VideoExporter() >>> ss.export_folder '/home/trassir/shots' >>> ss.export_folder += "/my_videos" >>> ss.export_folder '/home/trassir/shots/my_videos' | Экспорт видео с вызовом ``callback`` функции после выполнения. | Начало экспорта - 120 секунд назад, продолжительность 60 сек. >>> # noinspection PyUnresolvedReferences >>> def callback(success, file_path, channel_full_guid): ... # Пример callback функции ... # Args: ... # success (bool): True если видео экспортировано успешно, иначе False ... # file_path (str): Полный путь до видеофайла ... # channel_full_guid (str) : Полный guid канала ... if success: ... host.message("Экспорт успешно завершен<br>%s" % file_path) ... else: ... host.error("Ошибка экспорта<br>%s" % file_path) >>> ss = VideoExporter() >>> dt_start = datetime.now() - timedelta(seconds=120) >>> ss.export(callback, "e80kgBLh_pV4ggECb", dt_start) """ _EXPORTED_VIDEO_NAME_TEMPLATE = ( "{name} ({dt_start} - {dt_end}){sub}.avi" ) # Template for shot file name def __init__(self, host_api=host): self._host_api = host_api self._export_folder = BaseUtils.get_screenshot_folder() self._now_exporting = False self._queue = deque() self._default_prebuffer = host_api.settings("archive")["prebuffer"] + 2 @property def export_folder(self): """:obj:`str`: Папка для экспорта видео по умолчанию Устанавливает новый путь по умолчанию для экспорта видео, если папка не существует - создает папку. Или возвращает текущий путь для экспорта видео. Note: По молчанию ``export_folder`` = :meth:`BaseUtils.get_screenshot_folder` Raises: OSError: Если возникает ошибка при создании папки """ return self._export_folder @export_folder.setter def export_folder(self, folder): if not os.path.isdir(folder): try: os.makedirs(folder) except OSError as err: raise OSError("Can't make dir '{}': {}".format(folder, err)) self._export_folder = folder def _get_prebuffer(self, server_guid, dt_end): """Get prebuffer delay Args: server_guid (str): Full channel guid include server guid Returns: int: Prebuffer delay """ setting_path = "/{}/archive".format(server_guid) try: prebuffer = self._host_api.settings(setting_path)["prebuffer"] + 2 except KeyError: prebuffer = self._default_prebuffer wait_dt_end = (int(time.mktime(dt_end.timetuple())) + prebuffer) * 1000000 return "%.0f" % wait_dt_end def clear_complete_tasks(self): for task in self._host_api.archive_export_tasks_get(): if task["state"] != 1: self._host_api.archive_export_task_cancel( task["id"], # task id from archive_export_tasks_get -1, # -1 - do not wait for result, 0 - wait forever, > 0 - wait timeout_sec seconds BaseUtils.do_nothing, # callback_success BaseUtils.do_nothing, # callback_error ) def _check_queue(self): self._host_api.timeout(10, self.clear_complete_tasks) if self._queue: args, kwargs = self._queue.popleft() self._export(*args, **kwargs) def _export_checker(self, status, callback, file_path, channel_full_guid): if status == 1: return elif status in [0, 2]: """Export failed""" self._host_api.timeout( 100, lambda: callback(False, file_path, channel_full_guid) ) else: """Export success""" self._host_api.timeout( 100, lambda: callback(True, file_path, channel_full_guid) ) self._now_exporting = False self._check_queue() def _export( self, channel_full_guid, dt_start, dt_end=None, duration=60, prefer_substream=False, file_name=None, file_path=None, callback=None, ): """Exporting file Call callback(success: bool, file_path: str, channel_full_guid: str) when export finished, and clear tasks in trassir main control panel Note: Export task adding only when previous task finished You can set dt_start, dt_end, or dt_start, duration for export if dt_end is None: dt_end = dt_start + timedelta(seconds=duration) Args: channel_full_guid (str): Full channel guid; example: "CFsuNBzt_pV4ggECb" dt_start (datetime): datetime instance for export start dt_end (datetime, optional): datetime instance for export end; default: None duration (int, optional): Export duration (dt_start + duration seconds) if dt_end is None; default: 10 prefer_substream (bool, optional): If True - export substream; default: False file_name (str, optional): File name with extension; default: _EXPORTED_VIDEO_NAME_TEMPLATE file_path (str, optional): Path to save shot; default: screenshots_folder callback (function, optional): Function that calling when export finished """ if "_" not in channel_full_guid: raise ValueError( "Expected full channel guid, got {}".format(channel_full_guid) ) if not isinstance(dt_start, (datetime, date)): raise TypeError("Expected datetime, got {}".format(type(dt_start).__name__)) if dt_end: if not isinstance(dt_end, (datetime, date)): raise TypeError( "Expected datetime, got {}".format(type(dt_end).__name__) ) else: dt_end = dt_start + timedelta(seconds=duration) ts_start = "%.0f" % (time.mktime(dt_start.timetuple()) * 1000000) ts_end = "%.0f" % (time.mktime(dt_end.timetuple()) * 1000000) channel_guid, server_guid = channel_full_guid.split("_") options = { "prefer_substream": prefer_substream, "postponed_until_ts": self._get_prebuffer(server_guid, dt_end), } if file_name is None: file_name = self._EXPORTED_VIDEO_NAME_TEMPLATE.format( name=BaseUtils.get_object_name_by_guid(channel_guid), dt_start=dt_start.strftime("%Y.%m.%d %H-%M-%S"), dt_end=dt_end.strftime("%Y.%m.%d %H-%M-%S"), sub="_sub" if prefer_substream else "", ) if file_path is None: file_path = self.export_folder exporting_path = os.path.join(file_path, file_name) if callback is None: callback = BaseUtils.do_nothing self._now_exporting = True def checker(status): self._export_checker(status, callback, exporting_path, channel_full_guid) self._host_api.archive_export( server_guid, channel_guid, exporting_path, ts_start, ts_end, options, checker, )
[документация] def export( self, channel_full_guid, dt_start, dt_end=None, duration=60, prefer_substream=False, file_name=None, file_path=None, callback=None, ): """Запускает экспорт или добавляет задачу экспорта в очередь. После завершения экспорта вызывает ``callback`` функцию а также очищает список задач экспорта в панеле управления Trassir. Note: Задача экспорта добавляется только после завершения предыдущей. Tip: - Вы можете задать время начала и окончания экспорта ``dt_start``, ``dt_end``. - Или можно задать время начала экспорта ``dt_start`` и продолжительность экспорта (в сек.) ``duration``. По умолчнию ``duration=60``. - Если ``dt_end=None`` фунция использует ``duration`` для вычисления времени окончания ``dt_end = dt_start + timedelta(seconds=duration)``. Args: channel_full_guid (:obj:`str`): Полный guid канала. Например: ``"CFsuNBzt_pV4ggECb"`` dt_start (:obj:`datetime.datetime`): :obj:`datetime.datetime` начала экспорта dt_end (:obj:`datetime.datetime`, optional): :obj:`datetime.datetime` окончания экспорта. По умолчанию :obj:`None` duration (:obj:`int`, optional): Продолжительность экспорта, в секундах. Используется если ``dt_end is None``. По умолчанию ``60`` prefer_substream (:obj:`bool`, optional): Если ``True`` - Экспортирует субпоток. По умолчанию ``False`` file_name (:obj:`str`, optional): Имя экспортируемого файла. По умолчанию :obj:`None` file_path (:obj:`str`, optional): Путь для экспорта. По умолчанию :obj:`None` callback (:obj:`function`, optional): Функция, которая вызывается после завершения экспорта. По умолчанию :obj:`None` """ args = (channel_full_guid, dt_start) kwargs = { "dt_end": dt_end, "duration": duration, "prefer_substream": prefer_substream, "file_name": file_name, "file_path": file_path, "callback": callback, } if self._now_exporting: self._queue.append((args, kwargs)) else: self._export(*args, **kwargs)
class TemplateError(ScriptError): """Raised by Template class""" pass
[документация]class GUITemplate(py_object): """Класс для работы с шаблонами Trassir При инициализации находит существующий шаблон по имени или создает новый. Note: Если вручную создать два или большее шаблона с одинаковыми именами данный класс выберет первый попавшийся шаблон с заданным именем. Warning: Работа с контентом шаблона может привести к падениям трассира. Используйте данный класс на свой страх и риск! Tip: Для понимания, как формируется контент отредактируйте любой шаблон вручную и посмотрите что получится в скрытых параметрах трассира (активируются нажатием клавиши F4 в настройках трассира) `Настройки/Шабоны/<Имя шаблона>/content` Ниже предсталвены некоторые примеры шаблонов - Вывод одного канала ``S0tE8nfg_Or3QZu4D`` :obj:`gui7(DEWARP_SETTINGS,zwVj07w0,dewarp(),1,S0tE8nfg_Or3QZu4D)` - Вывод шаблона 4х4 с каналами двумя ``Kpid6EC0_Or3QZu4D``, ``ZRtXLrgu_Or3QZu4D`` :obj:`gui7(DEWARP_SETTINGS,zwVj07w0,dewarp(),4,Kpid6EC0_Or3QZu4D,ZRtXLrgu_Or3QZu4D,,)` - Вывод шаблон с минибраузером и ссылкой на https://www.google.com/ :obj:`minibrowser(0,htmltab(,https://www.google.com/))` Args: template_name (:obj:`str`): Имя шаблон Examples: >>> # Создаем шаблон с именем "New template" и получаем его guid >>> template = GUITemplate("New template") >>> template.guid 'Y2YFAkeZ' >>> # Устанавливаем на шаблон минибраузер с ссылкой на google >>> template.content = "minibrowser(0,htmltab(,https://www.google.com/))" >>> # Изменяем имя шаблона на "Google search" >>> template.name = "Google search" >>> # Открываем шаблон на первом мониторе >>> template.show(1) """ _DEFAULT_TEMPLATE = "" def __init__(self, template_name, host_api=host): self._name = template_name self._host_api = host_api self._operator_gui = BaseUtils.get_operator_gui() try: self._guid, self._template_settings = self._find_template_guid( template_name ) except KeyError: self._guid, self._template_settings = self._init_template(template_name) def _find_template_guid(self, name): """Find template guid by name Args: name (str) : Template name Raises: KeyError if can't find template """ templates = self._host_api.settings("templates") for template_ in templates.ls(): if name == template_.name: return ( template_.guid, self._host_api.settings("templates/{}".format(template_.guid)), ) raise KeyError def _init_template(self, name): """Create new template Args: name (str) : Template name """ self._host_api.object(self._host_api.settings("").guid + "T").create_template( name, self._DEFAULT_TEMPLATE ) try: return self._find_template_guid(name) except KeyError: raise TemplateError("Failed to create template {}".format(self._name)) @property def guid(self): """:obj:`str`: Guid шаблона""" return self._guid @guid.setter def guid(self, value): raise RuntimeError("You can't change object guid") @property def name(self): """:obj:`str`: Имя шаблона""" return self._name @name.setter def name(self, value): if isinstance(value, str): self._name = value self._template_settings["name"] = value else: raise TypeError("Expected str, got {}".format(type(value).__name__)) @property def content(self): """:obj:`str`: Контент шаблона""" return self._template_settings["content"] @content.setter def content(self, value): if isinstance(value, str): self._template_settings["content"] = value else: raise TypeError("Expected str, got {}".format(type(value).__name__))
[документация] def delete(self): """Удаляет шаблон""" obj = BaseUtils.get_object(self.guid) if obj is None: raise TemplateError("Template object not found!") obj.delete_template()
[документация] def show(self, monitor=1): """Открывает шаблон на указаном мониторе Args: monitor (:obj:`int`, optional): Номер монитора. По умолчанию ``monitor=1`` """ self._operator_gui.show(self.guid, monitor)
[документация]class TrObject(py_object): """Вспомогательный класс для работы с объектами Trassir Attributes: obj (:obj:`SE_Object`): Объект trassir :obj:`object('{guid}')` или :obj:`None` obj_methods (List[:obj:`str`]): Список методов объекта :attr:`TrObject.obj` name (:obj:`str`): Имя объекта или его guid guid (:obj:`str`): Guid объекта full_guid (:obj:`str`): Полный guid :obj:`{guid объекта}_{guid сервера}` или :obj:`None` type (:obj:`str`): Тип объекта, например :obj:`"RemoteServer"`, :obj:`"Channel"`, :obj:`"Grabber"`, :obj:`"User"`, и др. path (:obj:`str`): Путь в настройках или :obj:`None` parent (:obj:`str`): Guid родительского объекта или :obj:`None` server (:obj:`str`): Guid сервера или :obj:`None` settings (:obj:`SE_Settings`): Объект настроек ``settings('{path}')`` или :obj:`None` Raises: TypeError: Если неправильные параметры объекта ValueError: Если в имени объекта есть запятые """ obj, name, guid, full_guid, type = None, None, None, None, None path, parent, server, settings = None, None, None, None def __init__(self, obj, host_api=host): self._host_api = host_api if isinstance(obj, host_api.ScriptHost.SE_Settings): self._load_from_settings(obj) elif isinstance(obj, tuple): if len(obj) == 4: self._load_from_tuple(obj) else: raise TypeError( "Expected tuple(name, guid, type, parent), got tuple'{}'".format( obj ) ) else: raise TypeError("Unexpected object type '{}'".format(type(obj).__name__)) @staticmethod def _check_object_name(object_name): """Check if object name hasn't got commas Args: object_name (str): Returns: str: object_name.strip() Raises: ValueError: If "," found in object name """ if "," in object_name: raise ValueError( "Please, rename object '{}' without commas".format(object_name) ) return object_name.strip() @staticmethod def _parse_server_from_path(path): """Parse server guid from full path Args: path (str): Full Trassir settings path; example: '/pV4ggECb/_persons/n68LOBhG' returns 'pV4ggECb' """ try: server = path.split("/", 2)[1] except IndexError: server = None return server def _find_server_guid_for_object(self, object_guid): """Find server guid for object Args: object_guid (str): Object guid Returns: str: Server guid if server found None: If server not found """ all_objects = { obj[1]: {"name": obj[0], "guid": obj[1], "type": obj[2], "parent": obj[3]} for obj in self._host_api.objects_list("") } def get_parent(child_guid): child = all_objects.get(child_guid, None) if child: if child["type"] == "Server": return child["guid"] else: return get_parent(child["parent"]) else: return None return get_parent(object_guid) def _get_object_methods(self): """Get object methods""" if self.obj: return [method for method in dir(self.obj) if not method.startswith("__")] else: return [] def _load_from_settings(self, obj): """Preparing attributes from SE_Settings object""" self.obj = BaseUtils.get_object(obj.guid) self.obj_methods = self._get_object_methods() try: obj_name = obj.name except KeyError: obj_name = obj.guid self.name = self._check_object_name(obj_name) self.guid = obj.guid self.type = obj.type self.path = obj.path self.server = self._parse_server_from_path(obj.path) self.settings = obj if self.server and self.server != self.guid: self.full_guid = "{0.guid}_{0.server}".format(self) def _load_from_tuple(self, obj): """Preparing attributes from tuple object""" self.obj = BaseUtils.get_object(obj[1]) self.obj_methods = self._get_object_methods() self.name = self._check_object_name(obj[0]) self.guid = obj[1] self.type = obj[2] self.parent = obj[3] self.server = self._find_server_guid_for_object(obj[1]) if self.server and self.server != self.guid: self.full_guid = "{0.guid}_{0.server}".format(self) def __repr__(self): return "TrObject('{}')".format(self.name) def __str__(self): return "{self.type}: {self.name} ({self.guid})".format(self=self)
class ParameterError(ScriptError): """Ошибка в параметрах скрипта""" pass class BasicObject(py_object): """""" def __init__(self, host_api=host): self._host_api = host_api self.this_server_guid = BaseUtils.get_server_guid() class UniqueNameError(ScriptError): """Имя объекта не уникально""" pass class ObjectsNotFoundError(ScriptError): """Не найдены объекты с заданными именами""" pass def _check_unique_name(self, objects, object_names): """Check if all objects name are unique Args: objects (list): Objects list from _get_objects_from_settings Raises: UniqueNameError: If some object name is not uniques """ unique_names = [] for obj in objects: if obj.name in object_names: if obj.name not in unique_names: unique_names.append(obj.name) else: raise self.UniqueNameError( "Найдено несколько объектов {obj.type} с одинаковым именем '{obj.name}'! " "Задайте уникальные имена".format(obj=obj) ) @staticmethod def _objects_str_to_list(objects): """Split object names if objects is str and strip each name Args: objects (str|list): Trassir object names in comma spaced string or list Returns: list: Stripped Trassir object names Raises: ScriptError: If object name selected more than once """ if isinstance(objects, str): objects = objects.split(",") names = [] for name in objects: strip_name = name.strip() if strip_name in names: raise ParameterError("Объект '{}' выбран несколько раз".format(name)) names.append(strip_name) return names def _filter_objects_by_name(self, objects, object_names): """Filter object by names Args: objects (list): TrObject objects list object_names (str|list): Trassir object names in comma spaced string or list Raises: ObjectsNotFoundError: If len(object_name) != len(filtered_object) """ object_names = self._objects_str_to_list(object_names) self._check_unique_name(objects, object_names) filtered_object = [obj for obj in objects if obj.name in object_names] if len(filtered_object) != len(object_names): channels_not_found = set(object_names) - set( obj.name for obj in filtered_object ) try: object_type = objects[0].type except IndexError: object_type = "Unknown" raise self.ObjectsNotFoundError( "Не найдены объекты {object_type}: {names}".format( object_type=object_type, names=", ".join(name for name in channels_not_found), ) ) return filtered_object class ObjectFromSetting(BasicObject): """""" def __init__(self): super(ObjectFromSetting, self).__init__() def _load_objects_from_settings(self, settings_path, obj_type, sub_condition=None): """Load objects from Trassir settings Args: settings_path (:obj:`str`): Trassir settings path. Example ``"scripts"``. Click F4 in the Trassir settings window to show hidden parameters. obj_type (:obj:`str` | :obj:`list`): Loading object type. Example ``"EmailAccount"`` sub_condition (function, optional): Function with SE_Settings as argument to filter objects Returns: list: TrObject objects list Example [TrObject(...), TrObject(...), ...] """ try: settings = self._host_api.settings(settings_path) except KeyError: settings = None objects = [] if settings is not None: if isinstance(obj_type, str): obj_type = [obj_type] if sub_condition is None: sub_condition = BaseUtils.do_nothing for obj in settings.ls(): if obj.type in obj_type: if sub_condition(obj): objects.append(TrObject(obj)) return objects def _get_objects_from_settings( self, settings_path, object_type, object_names=None, server_guid=None, ban_empty_result=False, sub_condition=None, ): """Check if objects exists and returns list from _load_objects_from_settings Note: If object_names is not None - checking if all object names are unique Args: settings_path (:obj:`str`): Trassir settings path. Example ``"scripts"``. Click F4 in the Trassir settings window to show hidden parameters. object_type (:obj:`str` | :obj:`list`): Loading object type. Example ``"EmailAccount"`` object_names (:obj:`str` | :obj:`list`, optional): Comma spaced string or list of object names. Default :obj:`None` server_guid (:obj:`str` | :obj:`list`, optional): Server guid. Default :obj:`None` ban_empty_result (:obj:`bool`, optional): If True - raise error if no one object found sub_condition (:obj:`func`, optional) : Function with SE_Settings as argument to filter objects Returns: list: Trassir list from _load_objects_from_settings Raises: ObjectsNotFoundError: If can't find channel """ if object_names == "": raise ParameterError("'{}' не выбраны".format(object_type)) if server_guid is None: server_guid = self.this_server_guid if isinstance(server_guid, str): server_guid = [server_guid] objects = [] for guid in server_guid: objects += self._load_objects_from_settings( settings_path.format(server_guid=guid), object_type, sub_condition ) if ban_empty_result and not objects: raise self.ObjectsNotFoundError( "Не найдено ниодного объекта '{}'".format(object_type) ) if object_names is None: return objects else: return self._filter_objects_by_name(objects, object_names)
[документация]class Servers(ObjectFromSetting): """Класс для работы с серверами Examples: >>> srvs = Servers() >>> local_srv = srvs.get_local() [TrObject('Клиент')] >>> # Првоерим "Здоровье" локального сервера >>> local_srv[0].obj.state("server_health") 'Health Problem' """ def __init__(self): super(Servers, self).__init__()
[документация] def get_local(self): """Возвращает локальный сервер (на котором запущен скрипт) Returns: List[:class:`TrObject`]: Список объектов """ return self._load_objects_from_settings("/", ["Client", "LocalServer"])
[документация] def get_remote(self): """Возвращает список удаленных серверов Returns: List[:class:`TrObject`]: Список объектов """ return self._load_objects_from_settings("/", "RemoteServer")
[документация] def get_all(self): """Возвращает список всех доступных серверов Returns: List[:class:`TrObject`]: Список объектов """ return self._load_objects_from_settings( "/", ["Client", "LocalServer", "RemoteServer"] )
[документация]class Channels(ObjectFromSetting): """Класс для работы с каналами See Also: `Каналы - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-channels-folder.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> channels = Channels() >>> selected_channels = channels.get_enabled("AC-D2121IR3W 2,AC-D9141IR2 1") >>> selected_channels [TrObject('AC-D2121IR3W 2'), TrObject('AC-D9141IR2 1')] >>> >>> # Включим ручную запись на выбранных каналах >>> for channel in selected_channels: ... channel.obj.manual_record_start() >>> >>> # Или добавим к имени канала его guid >>> for channel in selected_channels: ... channel.settings["name"] += " ({})".format(channel.guid) """ def __init__(self, server_guid=None): super(Channels, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных каналов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): not_zombie = 1 - sett["archive_zombie_flag"] if not_zombie: try: return self._host_api.settings(sett.cd("info")["grabber_path"])[ "grabber_enabled" ] except KeyError: return 0 return 0 return self._get_objects_from_settings( "/{server_guid}/channels", "Channel", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных каналов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): zombie = sett["archive_zombie_flag"] if not zombie: try: return ( 1 - self._host_api.settings(sett.cd("info")["grabber_path"])[ "grabber_enabled" ] ) except KeyError: return 1 return 1 return self._get_objects_from_settings( "/{server_guid}/channels", "Channel", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех каналов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/channels", "Channel", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Devices(ObjectFromSetting): """Класс для работы с ip устройствами See Also: `IP-устройства - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-ip-cameras-folder.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> devices = Devices() >>> enabled_devices = devices.get_enabled() >>> enabled_devices [TrObject('AC-D2121IR3W'), TrObject('AC-D5123IR32'), ...] >>> >>> # Перезагрузим все устройства >>> for dev in enabled_devices: ... dev.settings["reboot"] = 1 """ def __init__(self, server_guid=None): super(Devices, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных устройств Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["grabber_enabled"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/ip_cameras", "Grabber", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных устройств Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["grabber_enabled"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/ip_cameras", "Grabber", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех устройств Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/ip_cameras", "Grabber", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Scripts(ObjectFromSetting): """Класс для работы со скриптами See Also: `Скрипты - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-script-feature.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> scripts = Scripts() >>> all_scripts = scripts.get_all() >>> all_scripts [TrObject('Новый скрипт'), TrObject('HDD Health Monitor'), TrObject('Password Reminder')] >>> >>> # Отключим все скрипты >>> for script in all_scripts: ... script.settings["enable"] = 0 """ def __init__(self, server_guid=None): super(Scripts, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных скриптов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["enable"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/scripts", "Script", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных скриптов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["enable"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/scripts", "Script", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех скриптов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/scripts", "Script", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class StockScripts(ObjectFromSetting): """Класс для работы со встроенными скриптами Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> stock_scripts = StockScripts() >>> all_scripts = stock_scripts.get_all() >>> all_scripts [TrObject('MegaRAID Monitor')] >>> >>> # Отключим все скрипты >>> for script in all_scripts: ... script.settings["enable"] = 0 """ def __init__(self, server_guid=None): super(StockScripts, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных скриптов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["enable"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/scripts", "StockScript", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных скриптов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["enable"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/scripts", "StockScript", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех скриптов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/scripts", "StockScript", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Rules(ObjectFromSetting): """Класс для работы с правилами See Also: `Правила - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-rule.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> rules = Rules() >>> all_rules = rules.get_all() >>> all_rules [TrObject('!Rule'), TrObject('NEW RULE'), TrObject('Новое правило')] >>> >>> # Отключим все правила >>> for rule in all_rules: ... rule.settings["enable"] = 0 """ def __init__(self, server_guid=None): super(Rules, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных правил Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["enable"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/scripts", "Rule", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных правил Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["enable"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/scripts", "Rule", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех правил Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/scripts", "Rule", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Schedules(ObjectFromSetting): """Класс для работы с расписаниями See Also: `Расписания - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-schedule.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> schedules = Schedules() >>> my_schedule = schedules.get_enabled("!Schedule")[0] >>> my_schedule.obj.state("color") 'Red' """ def __init__(self, server_guid=None): super(Schedules, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] @BaseUtils.run_as_thread def on_load(self, schedule_name, callback, tries=5): """on_load(schedule_name, callback, tries=5) Вызывает `callback` после загрузки расписания Note: При загрузке сервера, объект расписания становится не сразу доступен. Данный метод помогает предотвратить данную ошибку. Args: schedule_name (:obj:`str`): Имя расписания callback (:obj:`function`): Функция, которая вызывается после загрузки расписания. tries (:obj:`int`, optional): Кол-во попыток загрузки расписания. Каждая попытка производится с интервалом 1 с. По умолчанию :obj:`5` Examples >>> schedule = None >>> # noinspection PyGlobalUndefined,PyUnresolvedReferences >>> def on_schedule_loaded(schedule_obj): ... global schedule ... schedule = schedule_obj ... ... message("Schedule '{obj.name}' ({obj.guid}) loaded".format(obj=schedule)) ... schedule.activate_on_state_changes(lambda: alert(schedule.state("color"))) >>> >>> Schedules().on_load("Unnamed Schedule", on_schedule_loaded) """ if not schedule_name: raise ParameterError("Empty schedule name") tmp_server_guid = self.server_guid self.server_guid = BaseUtils.get_server_guid() while tries: obj = self.get_enabled(schedule_name)[0].obj if obj is None: tries -= 1 time.sleep(1) else: self.server_guid = tmp_server_guid self._host_api.timeout(1, lambda: callback(obj)) break else: self.server_guid = tmp_server_guid raise ScriptError( "Ошибка получения объекта расписания '{}'".format(schedule_name) )
[документация] def get_enabled(self, names=None): """Возвращает список активных расписаний Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["enable"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/scripts", "Schedule", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных расписаний Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["enable"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/scripts", "Schedule", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех расписаний Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/scripts", "Schedule", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class TemplateLoops(ObjectFromSetting): """Класс для работы с циклическими просмотрами шаблонов Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> tmplate_loops = TemplateLoops() >>> tmplate_loops.get_all() [TrObject('Новый циклический просмотр')] """ def __init__(self, server_guid=None): super(TemplateLoops, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных циклических просмотров шаблонов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["enable"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/scripts", "TemplateLoop", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных циклических просмотров шаблонов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["enable"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/scripts", "TemplateLoop", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех циклических просмотров шаблонов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/scripts", "TemplateLoop", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class EmailAccounts(ObjectFromSetting): """Класс для работы с E-Mail аккаунтами See Also: `Добавление учетной записи e-mail - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-email-account.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> email_accounts = EmailAccounts() >>> email_accounts.get_all() [TrObject('Новая учетная запись e-mail'), TrObject('MyAccount')] """ def __init__(self, server_guid=None): super(EmailAccounts, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_all(self, names=None): """Возвращает список всех E-Mail аккаунтов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/scripts", "EmailAccount", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class NetworkNodes(ObjectFromSetting): """Класс для работы с сетевыми подключениями See Also: `Сеть - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-network-folder.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> network_nodes = NetworkNodes("client") >>> network_nodes.get_enabled() [TrObject('QuattroStationPro (172.20.0.101)'), TrObject('NSK-HD-01 (127.0.0.1)')] """ def __init__(self, server_guid=None): super(NetworkNodes, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных сетевых подключений Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["should_be_connected"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/network", "NetworkNode", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных сетевых подключений Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["should_be_connected"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/network", "NetworkNode", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех сетевых подключений Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/network", "NetworkNode", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class PosTerminals(ObjectFromSetting): """Класс для работы с POS Терминалами See Also: `Настройка POS-терминалов - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-pos-terminals-folder.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> pos_terminals = PosTerminals() >>> pos_terminals.get_disabled() [TrObject('Касса (1)')] """ def __init__(self, server_guid=None): super(PosTerminals, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_enabled(self, names=None): """Возвращает список активных POS Терминалов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return sett["pos_enable"] except KeyError: return 0 return self._get_objects_from_settings( "/{server_guid}/pos_folder2/terminals", "PosTerminal", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_disabled(self, names=None): """Возвращает список неактивных POS Терминалов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ def sub_condition(sett): try: return 1 - sett["pos_enable"] except KeyError: return 1 return self._get_objects_from_settings( "/{server_guid}/pos_folder2/terminals", "PosTerminal", object_names=names, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация] def get_all(self, names=None): """Возвращает список всех POS Терминалов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/pos_folder2/terminals", "PosTerminal", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Users(ObjectFromSetting): """Класс для работы с пользователями и их группами. See Also: `Пользователи - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-users-folder.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> users = Users() >>> users.get_groups() [TrObject('TEST')] """ def __init__(self, server_guid=None): super(Users, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_groups(self, names=None): """Возвращает список групп пользователей Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/users", "Group", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_users(self, names=None): """Возвращает список пользователей Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/users", "User", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_users_by_groups(self, group_names): """Возвращает список пользователей из указанных групп Args: group_names (:obj:`str` | :obj:`list`): :obj:`str` - имена групп, разделенные запятыми или :obj:`list` - список имен. Returns: List[:class:`TrObject`]: Список объектов """ if group_names is None: groups = [""] else: groups = [group.guid for group in self.get_groups(names=group_names)] def sub_condition(sett): return sett["group"] in groups return self._get_objects_from_settings( "/{server_guid}/users", "User", object_names=None, server_guid=self.server_guid, sub_condition=sub_condition, )
[документация]class Templates(ObjectFromSetting): """Класс для работы с существующими шаблонами. Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> templates = Templates(BaseUtils.get_server_guid()) >>> templates.get_all() [TrObject('Parking'), TrObject('FR'), TrObject('AT'), TrObject('AD+')] """ def __init__(self, server_guid=None): super(Templates, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_all(self, names=None): """Возвращает список шаблонов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_settings( "/{server_guid}/templates", "Template", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Persons(ObjectFromSetting): """Класс для работы с персонами и их папками. See Also: `Персоны - Руководство пользователя Trassir <https://www.dssl.ru/files/trassir/manual/ru/setup-persons-folder.html>`_ Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> persons = Persons() >>> persons.get_folders() [TrObject('Мошенники'), TrObject('DSSL'), TrObject('persons')] >>> persons.get_persons() [ { 'name': 'Leonardo', 'guid': 'cJuJYAha', 'gender': 0, 'birth_date': '1980-01-01', 'comment': 'Comment', 'contact_info': 'Contact info', 'folder_guid': 'n68LOBhG', 'image': <image, str>, 'image_guid': 'gBHZ2vpz', 'effective_rights': 0, }, ... ] >>> persons.get_person_by_guid("cJuJYAha") { 'name': 'Leonardo', 'guid': 'cJuJYAha', 'gender': 0, 'birth_date': '1980-01-01', 'comment': 'Comment', 'contact_info': 'Contact info', 'folder_guid': 'n68LOBhG', 'image': <image, str>, 'image_guid': 'gBHZ2vpz', 'effective_rights': 0, } """ _PERSONS_UPDATE_TIMEOUT = 10 * 60 # Time in sec between update _persons dict def __init__(self, server_guid=None): super(Persons, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] if isinstance(server_guid, str): server_guid = [server_guid] self.server_guid = server_guid self._persons = None def _update_persons_dict(self, timeout=10): """Updating self._persons dict""" persons = self.get_persons(timeout=timeout) by_guid, by_name = {}, {} for person in persons: by_guid[person["guid"]] = person by_name[person["name"]] = person self._persons = { "update_ts": int(time.time()), "by_guid": by_guid, "by_name": by_name, } def _check_loaded_persons(self, timeout=10): """This method check if self._persons dict is need to be updated""" ts_now = int(time.time()) if ( self._persons is None or (ts_now - self._persons["update_ts"]) > self._PERSONS_UPDATE_TIMEOUT ): self._update_persons_dict(timeout=timeout)
[документация] def get_folders(self, names=None): """Возвращает список папок персон Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ try: folders = self._get_objects_from_settings( "/{server_guid}/persons", "PersonsSubFolder", object_names=names, server_guid=self.server_guid, ) if names is None or "persons" in names: for guid in self.server_guid: try: settings = self._host_api.settings("/{}/persons".format(guid)) except KeyError: continue folders.append(TrObject(settings)) except self.ObjectsNotFoundError as err: folders = [] names = self._objects_str_to_list(names) if names is None or "persons" in names: for guid in self.server_guid: try: settings = self._host_api.settings("/{}/persons".format(guid)) except KeyError: continue folders.append(TrObject(settings)) if not folders: raise err return folders
[документация] def get_persons(self, folder_names=None, timeout=10): """Возвращает список персон Note: Данный метод работает только с локальной БД. Args: folder_names (:obj:`str` | List[:obj:`str`], optional): :obj:`str` - названия папок персон, разделенные запятыми или :obj:`list` - список папок персон. По умолчанию :obj:`None` timeout (:obj:`int`, optional): Макс. время запроса к БД. По умолчанию ``timeout=10`` Returns: List[:obj:`dict`]: Список персон - если персоны найдены Raises: EnvironmentError: Если произошла ошибка при запросе в БД. TrassirError: Если в данной сборке Trassir нет метода :obj:`host.service_persons_get` """ tmp_server_guid = self.server_guid[:] self.server_guid = [self.this_server_guid] persons_folders = self.get_folders(names=folder_names) self.server_guid = tmp_server_guid[:] try: persons = self._host_api.service_persons_get( [folder.guid for folder in persons_folders], True, 0, 0, timeout ) except AttributeError: raise TrassirError( "Данный функционал не поддерживается вашей сборкой Trassir. " "Попробуйте обновить ПО." ) if isinstance(persons, str): raise EnvironmentError(persons) return persons
[документация] def get_person_by_guid(self, person_guid, timeout=10): """Возвращает информацию о персоне по его guid Note: Для уменьшения кол-ва запросов к БД - метод создает локальную копию всех персон при первом запросе и обновляет ее вместе с последующими запросами не чаще чем 1 раз в 10 минут. Args: person_guid (:obj:`str`): Guid персоны timeout (:obj:`int`, optional): Макс. время запроса к БД. По умолчанию ``timeout=10`` Returns: :obj:`dict`: Даные о персоне или :obj:`None` если персона не найдена """ self._check_loaded_persons(timeout=timeout) return self._persons["by_guid"].get(person_guid)
[документация] def get_person_by_name(self, person_name, timeout=10): """Возвращает информацию о персоне по его имени Note: Для уменьшения кол-ва запросов к БД - метод создает локальную копию всех персон при первом запросе и обновляет ее вместе с последующими запросами не чаще чем 1 раз в 10 минут. Args: person_name (:obj:`str`): Имя персоны timeout (:obj:`int`, optional): Макс. время запроса к БД. По умолчанию ``timeout=10`` Returns: :obj:`dict`: Даные о персоне или :obj:`None` если персона не найдена """ self._check_loaded_persons(timeout=timeout) return self._persons["by_name"].get(person_name)
class ObjectFromList(BasicObject): """""" def __init__(self): super(ObjectFromList, self).__init__() def _load_objects_from_list(self, obj_type, sub_condition=None): """Load objects from Trassir objects_list method Args: obj_type (str | list): Loading object type; example: "EmailAccount" sub_condition (function, optional): Function with SE_Settings as argument to filter objects Returns: list: TrObject objects list Example [TrObject(...), TrObject(...), ...] """ if sub_condition is None: sub_condition = BaseUtils.do_nothing objects = [] for obj in self._host_api.objects_list(obj_type): if sub_condition(obj): objects.append(TrObject(obj)) return objects def _get_objects_from_list( self, object_type, object_names=None, server_guid=None, ban_empty_result=False, sub_condition=None, ): """Check if objects exists and returns list from _load_objects_from_settings Note: If object_names is not None - checking if all object names are unique Args: object_type (str|list): Loading object type; example: "EmailAccount" object_names (str|list, optional): Comma spaced string or list of object names; default: None server_guid (str|list, optional): Server guids; default: None ban_empty_result (bool, optional): If True - raise ObjectsNotFoundError if no one object found sub_condition (func, optional) : Function with SE_Settings as argument to filter objects Returns: list: Trassir list from _load_objects_from_settings Raises: ObjectsNotFoundError: If can't find channel """ if object_names == "": raise ParameterError("'{}' не выбраны".format(object_type)) if server_guid is None: server_guid = self.this_server_guid else: if isinstance(server_guid, str): server_guid = [server_guid] objects = self._load_objects_from_list(object_type, sub_condition) objects = [obj for obj in objects if obj.server in server_guid] if ban_empty_result and not objects: raise self.ObjectsNotFoundError( "Не найдено ниодного объекта '{}'".format(object_type) ) if object_names is None: return objects else: return self._filter_objects_by_name(objects, object_names) def _zone_type(self, zone_obj): """Возвращает тип зоны для объекта Args: zone_obj (:obj:`SE_Object`): Объект trassir ``object('{guid}')`` Returns: :obj:`str`: Тип объекта :obj:`None`: Если тип зоны неизвестен """ if not isinstance(zone_obj, self._host_api.ScriptHost.SE_Object): raise TypeError( "Expected SE_Object, got '{}'".format(type(zone_obj).__name__) ) try: guid = zone_obj.guid channel, server = zone_obj.associated_channel.split("_") except (AttributeError, ValueError): return None try: zones_dir = self._host_api.settings( "/{}/channels/{}/people_zones".format(server, channel) ) for i in xrange(16): if zones_dir["zone%02d_guid" % i] == guid: func_type = zones_dir["zone%02d_func_type" % i] if isinstance(func_type, int): return ( ["Queue", "Workplace"][func_type] if func_type in range(2) else "Queue" ) else: return func_type except KeyError: "not a queue or workplace" try: zones_dir = self._host_api.settings( "/{}/channels/{}/workplace_zones".format(server, channel) ) for i in xrange(16): if zones_dir["zone%02d_guid" % i] == guid: return "Workplace" except KeyError: "not a workplace" try: zones_dir = self._host_api.settings( "/%s/channels/%s/deep_people" % (server, channel) ) for i in xrange(16): if zones_dir["zone%02d_guid" % i] == guid: if zones_dir["zone%02d_type" % i] in ["border", "border_swapped"]: return "Border" else: return "Queue" except KeyError: "not a deep people queue"
[документация]class GPIO(ObjectFromList): """Класс для работы с тревожными входами/выходами Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> gpio = GPIO() >>> gpio_door = gpio.get_inputs("Door")[0] >>> gpio_door.obj.state("gpio_input_level") 'Input Low (Normal High)' >>> gpio_light = gpio.get_outputs("Light")[0] >>> gpio_light.obj.set_output_high() """ def __init__(self, server_guid=None): super(GPIO, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_inputs(self, names=None): """Возвращает список тревожных входов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "GPIO Input", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_outputs(self, names=None): """Возвращает список тревожных выходов Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "GPIO Output", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Zones(ObjectFromList): """Класс для работы с зонами Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> zones = Zones() >>> zones.get_queues("Касса 1")[0].obj.state("zone_queue") '5+' """ def __init__(self, server_guid=None): super(Zones, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_people(self, names=None): """Возвращает список PeopleZones Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "PeopleZone", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_simt(self, names=None): """Возвращает список зон SIMT Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "SIMT Zone", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_workplaces(self, names=None): """Возвращает список рабочих зон Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ people_zones = self.get_people(names=names) return [ zone for zone in people_zones if self._zone_type(zone.obj) in ["Workplace", "Рабочее место"] ]
[документация] def get_queues(self, names=None): """Возвращает список зон очередей Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ people_zones = self.get_people(names=names) return [ zone for zone in people_zones if self._zone_type(zone.obj) in ["", "Queue", "Очередь"] ]
[документация] def get_shelves(self, names=None): """Возвращает список зон полок Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "Shelf", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация]class Borders(ObjectFromList): """Класс для работы с линиями пересечения Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. Examples: >>> borders = Borders() >>> borders.get_simt() [TrObject('DBOP')] >>> borders.get_all() [TrObject('Вход в офис'), TrObject('DBOP')] """ def __init__(self, server_guid=None): super(Borders, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid
[документация] def get_head(self, names=None): """Возвращает список HeadBorders Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "HeadBorder", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_people(self, names=None): """Возвращает список PeopleBorders Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "PeopleBorder", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_simt(self, names=None): """Возвращает список SIMT Borders Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "SIMT Border", object_names=names, server_guid=self.server_guid, sub_condition=None, )
[документация] def get_deep_people(self, names=None): """Возвращает список DeepPeopleBorders Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ people_zones = self._get_objects_from_list( "PeopleZone", object_names=names, server_guid=self.server_guid, sub_condition=None, ) return [zone for zone in people_zones if self._zone_type(zone.obj) == "Border"]
[документация] def get_all(self, names=None): """Возвращает список всех линий пересечения Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ all_borders = ( self.get_head() + self.get_people() + self.get_simt() + self.get_deep_people() ) if names is None: return all_borders else: return self._filter_objects_by_name(all_borders, names)
class Sigur(ObjectFromList): """Класс для работы со СКУД Sigur Args: server_guid (:obj:`str` | List[:obj:`str`], optional): Guid сервера или список guid. По умолчанию :obj:`None`, что соотвествует всем доступным серверам. """ def __init__(self, server_guid=None): super(Sigur, self).__init__() if server_guid is None: server_guid = [srv.guid for srv in Servers().get_all()] self.server_guid = server_guid def get_access_points(self, names=None): """Возвращает список точек доступа Args: names (:obj:`str` | :obj:`list`, optional): :obj:`str` - имена, разделенные запятыми или :obj:`list` - список имен. По умолчанию :obj:`None` Returns: List[:class:`TrObject`]: Список объектов """ return self._get_objects_from_list( "Access Point", object_names=names, server_guid=self.server_guid, sub_condition=None, ) class TrassirError(ScriptError): """Exception if bad trassir version""" pass
[документация]class PokaYoke(py_object): """Класс для защиты от дурака Позволяет блокировать запуск скрипта на ПО, где это не предусмотрено (например, на клиенте или TOS). А также производить некоторые другие проверки. """ _EMAIL_REGEXP = re.compile( r"[^@]+@[^@]+\.[^@]+" ) # Default regex to check emails list _PHONE_REGEXP = re.compile(r"[^\d,;]") # Default regex to check phone list _host_api = host def __init__(self): pass
[документация] @staticmethod def ban_tos(): """Блокирует запуск скрипта на `Trassir OS` Raises: OSError: Если скрипт запускается на `Trassir OS` Examples: >>> PokaYoke.ban_tos() OSError: Скрипт недоступен для TrassirOS """ if os.name != "nt": raise OSError("Скрипт недоступен для TrassirOS")
[документация] @staticmethod def ban_win(): """Блокирует запуск скрипта на `Windows OS` Raises: OSError: Если скрипт запускается на `Windows OS` Examples: >>> PokaYoke.ban_win() OSError: Скрипт недоступен для WindowsOS """ if os.name == "nt": raise OSError("Скрипт недоступен для WindowsOS")
[документация] @staticmethod def ban_client(): """Блокирует запуск скрипта на `Trassir Client` Raises: TrassirError: Если скрипт запускается на `Trassir Client` Examples: >>> PokaYoke.ban_client() TrassirError: Скрипт недоступен для клиентской версии Trassir """ if BaseUtils.get_server_guid() == "client": raise TrassirError("Скрипт недоступен для клиентской версии Trassir")
[документация] @classmethod def ban_daemon(cls): """Блокирует запуск скрипта на сервре Trassir, который запущен как служба Raises: TrassirError: Если скрипт запускается на сервре Trassir, который запущен как служба Examples: >>> PokaYoke.ban_daemon() TrassirError: Скрипт недоступен для Trassir запущенным как служба """ if cls._host_api.settings("system_wide_options")["daemon"]: raise TrassirError("Скрипт недоступен для Trassir запущенным как служба")
[документация] @staticmethod def check_email_account(account_name): """Проверяет существование E-Mail аккаунта Args: account_name (:obj:`str`): Имя E-Mail аккаунта Returns: List[:class:`TrObject`]: Список объектов Raises: ParameterError: Если аккаунт не выбран ObjectsNotFoundError: Если аккаунт не найден Examples: >>> PokaYoke.check_email_account("") ParameterError: 'EmailAccount' не выбраны >>> PokaYoke.check_email_account("YourAccount") ObjectsNotFoundError: Не найдены объекты EmailAccount: YourAccount >>> PokaYoke.check_email_account("MyAccount") [TrObject('MyAccount')] """ e_accounts = EmailAccounts(BaseUtils.get_server_guid()) return e_accounts.get_all(account_name)
[документация] @classmethod def parse_emails(cls, mailing_list, regex=None): """Парсит email дреса из строки Каждый email проверяется с помощью regex ``r"[^@]+@[^@]+\.[^@]+"``. Args: mailing_list (:obj:`str`): Список email адресов, разделенный запятыми regex (:obj:`SRE_Pattern`, optional): Новый regex шаблон для проверки. По умолчанию :obj:`None` Returns: List[:obj:`str`]: Список адресов Raises: ParameterError: Если найден невалидный email Examples: >>> PokaYoke.parse_emails("a.trubilil!dssl.ru,support@dssl.ru") ParameterError: Email 'a.trubilil!dssl.ru' is not valid! >>> >>> PokaYoke.parse_emails("a.trubilil@dssl.ru,support@dssl.ru") ['a.trubilil@dssl.ru', 'support@dssl.ru'] """ mailing_list = mailing_list.replace(" ", "") if not mailing_list: raise ParameterError("No emails to send!") if regex is None: regex = cls._EMAIL_REGEXP else: if not isinstance(regex, cls._EMAIL_REGEXP.__class__): raise TypeError( "Expected re.compile, got '{}'".format(type(regex).__name__) ) if isinstance(mailing_list, str): mailing_list = mailing_list.split(",") mailing_list = [mail.strip() for mail in mailing_list] for mail in mailing_list: if not regex.match(mail): raise ParameterError("Email '{}' is not valid!".format(mail)) return mailing_list
[документация] @classmethod def check_phones(cls, phones, regex=None): """Проверяет строку на валидность телефонных номеров Строка проверяется с помощью regex ``r"[^\d,;]"``. Args: phones (:obj:`str`): Список телефонов, разделенный запятыми или точкой с запятой regex (:obj:`SRE_Pattern`, optional): Новый regex шаблон для проверки. По умолчанию :obj:`None` Returns: :obj:`str`: Список номеров телефона Raises: ParameterError: Если найден невалидный номер телефона Examples: >>> PokaYoke.check_phones("79999999999,78888888888A") ParameterError: Bad chars in phone list: `A` >>> >>> PokaYoke.check_phones("a.trubilil@dssl.ru,support@dssl.ru") '79999999999,78888888888' """ phones = phones.replace(" ", "") if not phones: raise ParameterError("No phones!") if regex is None: regex = cls._PHONE_REGEXP else: if not isinstance(regex, cls._PHONE_REGEXP.__class__): raise TypeError( "Expected re.compile, got '{}'".format(type(regex).__name__) ) bad_chars = regex.findall(phones) if bad_chars: raise ParameterError( "Bad chars in phone list: `{}`".format(", ".join(bad_chars)) ) return phones
[документация] @classmethod def fire_recognizer_events(cls, enable=True, server_guid=None): """Проверяет "Режим для СКУД" настроек распознавания лиц. По умолчанию проверяет активирован ли "Режим для СКУД" на сервере, где запущен скрипт. По желанию можно указать удаленный сервер дял проверки. Args: enable (:obj:`bool`, optional): Состояние параметра. По умолчанию :obj:`True`. server_guid (:obj:`str`, optional): Guid сервера. По умолчанию :obj:`None`. Raises: RuntimeError: Если указанный сервер недоступен. EnvironmentError: Если моудль распознавания или режим для СКУД не доступны. TrassirError: Если текущее состояние не соотвествует необходимомому. Examples: >>> PokaYoke.fire_recognizer_events() TrassirError: Пожалуйста, активируйте 'Режим для СКУД' в настройках распознавания лиц """ if server_guid is None: server_guid = BaseUtils.get_server_guid() try: srv_sett = cls._host_api.settings("/%s" % server_guid) except KeyError: raise RuntimeError("Сервер '%s' не доступен" % server_guid) fr_sett = srv_sett.cd("face_recognizer") if fr_sett is None: raise EnvironmentError( "Модуль распознавания лиц не доступен на '%s'" % (srv_sett.name or srv_sett.guid) ) try: if fr_sett["fire_recognizer_events"] != enable: raise TrassirError( "Пожалуйста, {} 'Режим для СКУД' в настройках распознавания лиц".format( "активируйте" if enable else "отключите" ) ) except KeyError: raise EnvironmentError( "'Режим для СКУД' не доступен. Пожалуйста, обновите сервер trassir." )
[документация]class SoundPlayer(py_object): """Класс для проигрывания выбранной мелодии. Можно указать один из стандартных зуков или указать полный путь до своего файла. Note: Список стандартных файлов .. raw:: html <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/SNES-startup.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"SNES-startup.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/alarm.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"alarm.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/bell.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"bell.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/boxing-bell-1.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"boxing-bell-1.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/boxing-bell-3.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"boxing-bell-3.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/cardlock-open.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"cardlock-open.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/chime.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"chime.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/chip001.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"chip001.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/chip019.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"chip019.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/chip069.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"chip069.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/cordless-phone-ring.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"cordless-phone-ring.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/countdown.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"countdown.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/dialtone.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"dialtone.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/ding.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"ding.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/horn-beep.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"horn-beep.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/phone-beep.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"phone-beep.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/police2.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"police2.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/ship-on-fog.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"ship-on-fog.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/ships-bell.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"ships-bell.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/spin-up.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"spin-up.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/tada1.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"tada1.wav"</span> </code> <br> <audio controls="controls" style="height: 20px; margin-bottom: -5px;"> <source src="https://github.com/aatrubilin/trassir_script_framework/raw/master/docs/source/sounds/tape-slow9.wav" type="audio/wav"> Your browser does not support the <code>audio</code> element. </audio> <code class="xref py py-obj docutils literal notranslate"> <span class="pre">"tape-slow9.wav"</span> </code> <br> Args: sound_file (:obj:`str`): Имя файла с расширением """ _DEFAULT_SOUNDS = { "SNES-startup.wav", "alarm.wav", "bell.wav", "boxing-bell-1.wav", "boxing-bell-3.wav", "cardlock-open.wav", "chime.wav", "chip001.wav", "chip019.wav", "chip069.wav", "cordless-phone-ring.wav", "countdown.wav", "dialtone.wav", "ding.wav", "horn-beep.wav", "phone-beep.wav", "police2.wav", "ship-on-fog.wav", "ships-bell.wav", "spin-up.wav", "tada1.wav", "tape-slow9.wav", } def __init__(self, sound_file, host_api=host): self._host_api = host_api self._play = self._get_player(sound_file) def _check_file(self, sound_file): _, ext = os.path.splitext(sound_file) if ext.lower() != ".wav": raise RuntimeError("Expected *.wav file, got {!r}".format(ext)) if sound_file in self._DEFAULT_SOUNDS: if os.name == "nt": base_path = "sounds" else: base_path = "/opt/trassir/tech1/sounds" sound_file = os.path.join(base_path, sound_file) if not os.path.isfile(sound_file): raise IOError("File {} not found".format(sound_file)) return sound_file def _get_player(self, sound_file): sound_file = self._check_file(sound_file) if os.name == "nt": def player(): winsound.PlaySound( sound_file, winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_NOWAIT, ) else: def player(): os.system('aplay -D "sysdefault:CARD=PCH" %s &' % sound_file) return player
[документация] def play(self): """Проигрывает выбранный файл Examples: >>> player = SoundPlayer("alarm.wav") >>> player.play() """ self._play()
class SenderError(Exception): """Base Sender Exception""" pass class Sender(py_object): _HTML_IMG_TEMPLATE = """<img src="data:image/png;base64,{img}" {attr}>""" def __init__(self, host_api=host): self._host_api = host_api @staticmethod def _get_base64(image_path): """Returns base64 image Args: image_path (str): Image full path """ image_path = BaseUtils.win_encode_path(image_path) if os.path.isfile(image_path): with open(image_path, "rb") as image_file: return base64.b64encode(image_file.read()) @staticmethod def _get_html_img(image_base64, **kwargs): """Returns html img Args: image_base64 (str): Base64 image """ return BaseUtils.base64_to_html_img(image_base64, **kwargs) def text(self, text, **kwargs): """Send text Args: text (str): Text message """ pass def image(self, image_path, text="", **kwargs): """Send image and optional text Args: image_path (str | List[str]): Image path or paths text (str, optional): Text message; default: "" """ pass def files(self, file_paths, text="", **kwargs): """Send file or list of files Args: file_paths (str | List[str]): File path or list of paths text (str, optional): Text message; default: "" """ pass
[документация]class PopupSender(Sender): """Класс для показа всплывающих окон в правом нижнем углу экрана Args: width (:obj:`int`, optional): Ширина изображения, px. По умолчанию :obj:`width=400` Examples: >>> sender = PopupSender(300) >>> sender.text("Hello World!") .. image:: images/popup_sender.text.png >>> sender.image(r"manual/en/cloud-devices-16.png") .. image:: images/popup_sender.image.png """ def __init__(self, width=400): super(PopupSender, self).__init__() self._attr = {"width": width}
[документация] def text(self, text, popup_type="message", **kwargs): """Показывает текст во всплывающем окне Вызывает один из методов Trassir :obj:`host.alert`, :obj:`host.message` или :obj:`host.error` с текстом Args: text (:obj:`str`): Текст сообщения popup_type (:obj:`"message"` | :obj:`"alert"` | :obj:`"error"`, optional) Тип сообщения. По умолчанию :obj:`"message"` """ if popup_type == "alert": self._host_api.alert(text) elif popup_type == "error": self._host_api.error(text) else: self._host_api.message(text)
[документация] def image(self, image_path, text="", popup_type=None, **kwargs): """Показывает изображение во всплывающем окне Args: image_path (:obj:`str` | :obj:`List[str]`): Полный путь до изображения или список путей. text (:obj:`str`, optional): Текст сообщения. По умолчанию :obj:`""` popup_type (:obj:`"message"` | :obj:`"alert"` | :obj:`"error"`, optional) Тип сообщения. По умолчанию :obj:`"message"` """ if not isinstance(image_path, list): image_path = [image_path] images_base64 = [self._get_base64(img_path) for img_path in image_path] for image_base64, img_path in zip(images_base64, image_path): if not image_base64: self.text("<b>File not found</b><br>{}".format(img_path), popup_type) return html_image = BaseUtils.base64_to_html_img(image_base64, **self._attr) html = "{image}" if text: html = "<b>{text}</b><br>{image}" self.text(html.format(text=text, image=html_image), popup_type)
[документация]class PopupWithBtnSender(Sender): """Класс для показа всплывающих окон с кнопкой `Оk` Note: | Для закрытия окна необходимо нажать кнопку `Ok` в течении 60 сек. | После 60 сек окно закрывается автоматически. Args: width (:obj:`int`, optional): Ширина изображения, px. По умолчанию :obj:`width=800` Examples: >>> sender = PopupWithBtnSender() >>> sender.text("Hello World!") .. image:: images/popup_with_btn_sender.text.png >>> sender.image(r"manual/en/cloud-devices-16.png") .. image:: images/popup_with_btn_sender.image.png """ def __init__(self, width=800): super(PopupWithBtnSender, self).__init__() self._attr = {"width": width}
[документация] def text(self, text, **kwargs): """Показывает текст во всплывающем окне Вызывает метод Trassir :obj:`host.question` с текстом Args: text (:obj:`str`): Текст сообщения """ self._host_api.question( "<pre>{}</pre>".format(text), "Ok", BaseUtils.do_nothing )
[документация] def image(self, image_path, text="", **kwargs): """Показывает изображение во всплывающем окне Args: image_path (:obj:`str` | :obj:`List[str]`): Полный путь до изображения или список путей. text (:obj:`str`, optional): Текст сообщения. По умолчанию :obj:`""` """ if not isinstance(image_path, list): image_path = [image_path] images_base64 = [self._get_base64(img_path) for img_path in image_path] for image_base64, img_path in zip(images_base64, image_path): if not image_base64: self.text("<b>File not found</b><br>{}".format(image_path)) return html_image = BaseUtils.base64_to_html_img(image_base64, **self._attr) html = "{image}" if text: html = "<b>{text}</b><br>{image}" self.text(html.format(text=text, image=html_image))
[документация]class EmailSender(Sender): """Класс для отправки уведомлений, изображений и файлов на почту Note: По умолчанию тема сообщений соответствует шаблону ``{server_name} -> {script_name}`` Tip: При отправке изображения с текстом предпочтительней использовать метод :meth:`EmailSender.image` с необязательным аргументом :obj:`text` чем :meth:`EmailSender.text` с необазательным аргументом :obj:`attachments` Args: account (:obj:`str`): E-Mail аккаунт trassir. Проверяется методом :meth:`PokaYoke.check_email_account` mailing_list (:obj:`str`): Список email адресов для отправки писем разделенный запятыми. Проверяется и парсится в список методом :meth:`PokaYoke.parse_emails` subject (:obj:`str`, optional): Общая тема для сообщений. По умолчанию :obj:`None` max_size (:obj:`int`, optional): Максимальный размер вложения, байт. По умолчанию 25 * 1024 * 1024 Examples: >>> sender = EmailSender("MyAccount", "my_mail@google.com") >>> sender.text("Hello World!") .. image:: images/email_sender.text.png >>> sender.image(r"manual/en/cloud-devices-16.png") .. image:: images/email_sender.image.png >>> sender.files([r"manual/en/cloud.html", r"manual/en/cloud.png"]) .. image:: images/email_sender.files.png """ def __init__(self, account, mailing_list, subject=None, max_size=None): super(EmailSender, self).__init__() PokaYoke.check_email_account(account) self.max_size = max_size or 25 * 1024 * 1024 self._account = account self._mailing_list = PokaYoke.parse_emails(mailing_list) self._subject_default = subject or self._generate_subject() logger.info( "EmailSender({}, {}, subject={}, max_size={})".format( repr(account), repr(mailing_list), repr(self._subject_default), repr(self.max_size), ) ) def _generate_subject(self): """Returns `server name` -> `script name`""" subject = "{server_name} -> {script_name}".format( server_name=self._host_api.settings("").name or "Client", script_name=self._host_api.stats().parent()["name"], ) return subject def _group_files_by_max_size(self, file_paths, max_size): """Split files to groups. Size of each group is less then max_size Args: file_paths (list): List of files max_size (int): Max group size, bytes """ group = [] cur_size = 0 for idx, file_path in enumerate(file_paths): file_size = os.stat(BaseUtils.win_encode_path(file_path)).st_size if not cur_size or (cur_size + file_size) < max_size: cur_size += file_size group.append(file_path) else: break else: return [group] return [group] + self._group_files_by_max_size(file_paths[idx:], max_size)
[документация] def text(self, text, subject=None, attachments=None, **kwargs): """Отправка текстового сообщения Args: text (:obj:`str`): Текст сообщения subject (:obj:`str`, optional): Новая тема сообщения. По умолчанию :obj:`None` attachments (:obj:`list`, optional): Список вложений. По умолчанию :obj:`None` """ if attachments is None: attachments = [] self._host_api.send_mail_from_account( self._account, self._mailing_list, subject or self._subject_default, text, attachments, )
[документация] def image(self, image_path, text="", subject=None, **kwargs): """Отправка изображения Args: image_path (:obj:`str` | :obj:`List[str]`): Полный путь до изображения или список путей. text (:obj:`str`, optional): Текст сообщения. По умолчанию :obj:`""` subject (:obj:`str`, optional): Новая тема сообщения. По умолчанию :obj:`None` """ if not isinstance(image_path, list): image_path = [image_path] self.files(image_path, text=text, subject=subject)
[документация] def files(self, file_paths, text="", subject=None, callback=None, **kwargs): """Отправка файлов Note: Если отправляется несколько файлов они могут быть разделены на несколько сообщений, основываясь на максимальном размере вложений. Args: file_paths (:obj:`str` | :obj:`list`): Путь до файла или список файлов для отправки text (:obj:`str`, optional): Текст сообщения. По умолчанию :obj:`""` subject (:obj:`str`, optional): Новая тема сообщения. По умолчанию :obj:`None` callback (:obj:`function`, optional): Функция, которая вызывается после отправки частей """ logger.debug( "EmailSender.files({}, text={})".format(repr(file_paths), repr(text)) ) if isinstance(file_paths, str): file_paths = [file_paths] if callback is None: callback = BaseUtils.do_nothing files_to_send = [] for path in file_paths: if BaseUtils.is_file_exists(path): files_to_send.append(path) else: text += "\nFile not found: {}".format(path) file_groups = self._group_files_by_max_size(files_to_send, self.max_size) for grouped_files in file_groups: logger.debug( "EmailSender.files: grouped_files: {}".format(repr(grouped_files)) ) self.text(text, subject=subject, attachments=grouped_files) callback(grouped_files)
[документация]class TelegramSender(Sender): """Работа с телеграм ботом `@trassirbot <https://t.me/trassirbot>`_ Warnings: | Cкрипт должен быть запущен на **сервере** Trassir. | На Клиенте скрипт вызовет ошибку ``ServerKeyError`` Args: telegram_ids (:obj:`str`): Id пользователей, через запятую. Examples: >>> # Можно указать id для рассылки при инициализации >>> # класса, для всех уведомлений >>> sender = TelegramSender("123456789") >>> sender.text("Hello World!") .. image:: images/telegram_sender.text.png >>> sender.image(r"manual/en/cloud-devices-16.png") .. image:: images/telegram_sender.image.png >>> sender.files([r"manual/en/cloud.html", r"manual/en/cloud.png"]) .. image:: images/telegram_sender.files.png >>> # Или можно опередовать telegram id при вызове методов >>> sender = TelegramSender() >>> sender.text("Hello World!", tg_users=[123456789]) .. image:: images/telegram_sender.text.png >>> sender.image(r"manual/en/cloud-devices-16.png", tg_users=[123456789]) .. image:: images/telegram_sender.image.png >>> sender.files([r"manual/en/cloud.html", r"manual/en/cloud.png"], tg_users=[123456789]) .. image:: images/telegram_sender.files.png """ def __init__(self, telegram_ids=None): super(TelegramSender, self).__init__() self._host_api.exec_encoded(tbot_service) self._tbot_api = TBotAPI() if telegram_ids is not None: self.telegram_ids = TBotAPI.prepare_users(telegram_ids) else: self.telegram_ids = None
[документация] def text(self, text, tg_users=None, **kwargs): """Отправка текстового сообщения Args: text (:obj:`str`): Текст сообщения. tg_users (List[:obj:`int`], optional): Список id пользователей telegram для отправки отдельных сообщений. По умолчанию :obj:`None` """ if tg_users is None: tg_users = self.telegram_ids self._tbot_api.send_message(tg_users, text)
[документация] def image(self, image_path, text="", tg_users=None, remove=False, **kwargs): """Отправка изображения Args: image_path (:obj:`str` | List[:obj:`str`]): Полный путь до изображения или список путей text (:obj:`str`, optional): Текст сообщения. По умолчанию :obj:`""` tg_users (List[:obj:`int`], optional): Список id пользователей telegram для отправки отдельных сообщений. По умолчанию :obj:`None` remove (bool, optional): Удалить файл после отправки или нет. По умолчанию :obj:`False` """ if tg_users is None: tg_users = self.telegram_ids if len(text) > 150: self.text(text, tg_users=tg_users) text = None if isinstance(image_path, list): self._tbot_api.send_image_album( tg_users, image_path, captions=[text] or None, remove=remove ) else: if not os.path.isfile(image_path): self.text("Image not found: {}".format(image_path)) return self._tbot_api.send_image(tg_users, image_path, caption=text, remove=remove)
[документация] def files(self, file_paths, text="", tg_users=None, remove=False, **kwargs): """Отправка файлов Args: file_paths (:obj:`str` | :obj:`list`): Путь до файла или список файлов для отправки text (:obj:`str`, optional): Текст сообщения. По умолчанию :obj:`""` tg_users (List[:obj:`int`], optional): Список id пользователей telegram для отправки отдельных сообщений. По умолчанию :obj:`None` remove (bool, optional): Удалить файл после отправки или нет. По умолчанию :obj:`False` """ if tg_users is None: tg_users = self.telegram_ids if isinstance(file_paths, str): file_paths = [file_paths] if text and len(file_paths) == 1: self.text(text, tg_users=tg_users) text = "" files_not_found_text = "" for path in file_paths: if os.path.isfile(BaseUtils.win_encode_path(path)): self._tbot_api.send_document( tg_users, path, caption=text, remove=remove ) else: files_not_found_text += "\nFile not found: {}".format(path) if files_not_found_text: self.text(files_not_found_text, tg_users=tg_users)
class SMSCSenderError(SenderError): """Raises with SMSCSender errors""" pass
[документация]class SMSCSender(Sender): """Класс для отправки сообщений с помощью сервиса smsc.ru See Also: `https://smsc.ru/api/http/ <https://smsc.ru/api/http/>`_ Note: | Номера проверяются методом :meth:`PokaYoke.check_phones` | Также при первом запуске скрипт проверяет данные авторизации Warnings: | По умолчанию сервис smsc.ru отправляет сообщения от своего имени *SMSC.RU.* При этом отправка на номера Мегафон/Йота **недоступна** т.к. имя *SMSC.RU* заблокировано оператором. | | Мы настоятельно **НЕ** рекомендуем использовать стандартное имя *SMSC.RU.* | | Для отправки смс от вашего буквенного имени необходимо его создать в разделе - https://smsc.ru/senders/ и зарегистрировать для операторов в колонке Действия по кнопке Изменить (после заключения договора согласно инструкции - https://smsc.ru/contract/info/ ) а также приложить гарантийное письмо на МТС в личный кабинет http://smsc.ru/documents/ и отправить на почту inna@smsc.ru Args: login (:obj:`str`): SMSC Логин password (:obj:`str`): SMSC Пароль phones (:obj:`str`): Список номеров для отправки смс резделенный запятыми или точкой с запятой translit(:obj:`bool`, optional): Переводить сообщение в транслит. По умолчанию :obj:`True` Raises: SMSCSenderError: При любых ошибках с отправкой сообщения Examples: >>> sender = SMSCSender("login", "password", "79999999999") >>> sender.text("Hello World!") .. image:: images/smsc_sender.text.png """ _BASE_URL = "https://smsc.ru/sys/send.php?{params}" _ERROR_CODES = { 1: "URL Params error", 2: "Invalid login or password", 3: "Not enough money", 4: "Your IP is temporary blocked. More info: https://smsc.ru/faq/99", 5: "Bad date format", 6: "Message is denied (by text or sender name)", 7: "Bad phone format", 8: "Can't send message to this number", 9: "Too many requests", } def __init__(self, login, password, phones, translit=True): super(SMSCSender, self).__init__() if not login: raise SMSCSenderError("Empty login") if not password: raise SMSCSenderError("Empty password") self._params = { "login": urllib.quote(login), # Login "psw": urllib.quote(password), # Password or MD5 hash "phones": urllib.quote( PokaYoke.check_phones(phones) ), # Comma or semicolon spaced phone list "fmt": 3, # Response format: 0 - string; 1 - integers; 2 - xml; 3 - json "translit": 1 if translit else 0, # If 1 - transliting message "charset": "utf-8", # Message charset: "windows-1251"|"utf-8"|"koi8-r" "cost": 3, # Message cost in response: 0 - msg; 1 - cost; 2 - msg+cost, 3 - msg+cost+balance } self._check_account() def _get_link(self, **kwargs): """Returns get link""" params = self._params.copy() params.update(kwargs) url = self._BASE_URL.format(params=urllib.urlencode(params)) return url def _request_callback(self, code, result, error): """Callback for async_get""" if code != 200: raise SMSCSenderError("RequestError [{}]: {}".format(code, error)) else: try: data = json.loads(result) except ValueError: data = {"error_code": 0, "error": "JSON loads error: {}".format(result)} error_code = data.get("error_code") if error_code is not None: error = self._ERROR_CODES.get(error_code) if not error: error = data.get("error", "Unknown error") raise SMSCSenderError( "ResponseError [{}]: {}".format(error_code, error) ) def _check_account(self): """Send test request to smsc server""" url = self._get_link(cost=1, mes=urllib.quote("Hello world!")) self._host_api.async_get(url, self._request_callback)
[документация] def text(self, text, **kwargs): """Отправка текстового сообщения Args: text (:obj:`str`): Текст сообщения. """ url = self._get_link(mes=text) self._host_api.async_get(url, self._request_callback)
class FtpUploadTracker: """Upload progress class""" size_written = 0.0 last_shown_percent = 0 def __init__(self, file_path, callback, host_api=host): self._host_api = host_api self.total_size = os.path.getsize(BaseUtils.win_encode_path(file_path)) self.file_path = file_path self.callback = callback # noinspection PyUnusedLocal def handle(self, block): """Handler for storbinary See Also: https://docs.python.org/2/library/ftplib.html#ftplib.FTP.storbinary """ self.size_written += 1024.0 percent_complete = round((self.size_written / self.total_size) * 100) if self.last_shown_percent != percent_complete: self.last_shown_percent = percent_complete self._host_api.timeout( 100, lambda: self.callback(self.file_path, int(percent_complete), "") ) class FTPSenderError(SenderError): """Raises with FTPSender errors""" pass
[документация]class FTPSender(Sender): """Класс для отправки файлов на ftp сервер При инициализации проверят подключение к ftp серверу. Файлы отправляет по очереди. Максимальный размер очереди можно изменить. Во время выполнения передает текущий прогресс отправки файла в callback функцию. Note: Помимо прогресса в функцию callback может вернуться код ошибки. - -1 Файл не существует. - -2 Ошибка отправки на ftp, файл будет повторно отправлен. - -3 Неизвестная ошибка. Args: host (:obj:`str`): Адрес ftp сервера. port (:obj:`int`, optional): Порт ftp сервера. По умолчанию :obj:`port=21` user (:obj:`str`, optional): Имя пользователя. По умолчанию :obj:`"anonymous"` passwd (:obj:`str`, optional): Пароль пользователя. По умолчанию :obj:`passwd=""` work_dir (:obj:`str`, optional): Директория на сервре для сохранения файлов. По умолчанию :obj:`None` callback (:obj:`function`, optional): Callable function. По умолчанию :obj:`None` queue_maxlen (:obj:`int`, optional): Максимальная длина очереди на отправку. По умолчанию :obj:`queue_maxlen=1000` Examples: >>> # noinspection PyUnresolvedReferences >>> def callback(file_path, progress, error): ... # Пример callback функции, которая отображает ... # текущий прогресс в счетчике запуска скрипта ... # Args: ... # file_path (str): Путь до файла ... # progress (int): Текущий прогресс передачи файла, % ... # error (str | Exception): Ошибка при отправке файла, если есть ... host.stats()["run_count"] = progress ... if error: ... host.error(error) ... ... if progress == 100: ... host.timeout(3000, lambda: os.remove(BaseUtils.win_encode_path(file_path))) >>> >>> sender = FTPSender("172.20.0.10", 21, "trassir", "12345", work_dir="/test_dir/", callback=callback) >>> sender.files(r"D:/Shots/export_video.avi") """ # noinspection SpellCheckingInspection,PyShadowingNames def __init__( self, host, port=21, user="anonymous", passwd="", work_dir=None, callback=None, queue_maxlen=1000, ): super(FTPSender, self).__init__() self._host = host self._port = port self._user = user self._passwd = passwd self._work_dir = work_dir self.queue = deque(maxlen=queue_maxlen) self._ftp = None if callback is None: callback = BaseUtils.do_nothing self.callback = callback self._work_now = False self._check_connection() def _check_connection(self): """Check if it possible to connect""" try: ftp = ftplib.FTP() ftp.connect(self._host, self._port, timeout=10) ftp.login(self._user, self._passwd) except ftplib.all_errors as err: raise FTPSenderError(err) if self._work_dir: try: ftp.cwd(self._work_dir) except ftplib.error_perm: ftp.mkd(self._work_dir) ftp.cwd(self._work_dir) ftp.quit() def _get_connection(self): """Connecting to ftp Returns: ftplib.FTP: Ftp object """ try: ftp = ftplib.FTP() ftp.connect(self._host, self._port, timeout=10) ftp.login(self._user, self._passwd) if self._work_dir: try: ftp.cwd(self._work_dir) except ftplib.error_perm: ftp.mkd(self._work_dir) ftp.cwd(self._work_dir) ftp.encoding = "utf-8" return ftp except ftplib.all_errors: return def _close_connection(self): """Close ftp connection""" try: if self._ftp is not None: self._ftp.close() finally: self._ftp = None def _send_file(self, file_path, work_dir=None): """Storbinary file with self.ftp Args: file_path (str): Full file path work_dir (str): Work dir on ftp """ if work_dir is not None: if self._work_dir: work_dir = os.path.normpath("{}/{}".format(self._work_dir, work_dir)) try: self._ftp.cwd(work_dir) except ftplib.error_perm: self._ftp.mkd(work_dir) self._ftp.cwd(work_dir) file_name = os.path.basename(file_path) upload_tracker = FtpUploadTracker(file_path, self.callback) with open(BaseUtils.win_encode_path(file_path), "rb") as opened_file: self._ftp.storbinary( "STOR " + file_name, opened_file, 1024, upload_tracker.handle ) @BaseUtils.run_as_thread def _sender(self): """Send files in queue""" if self.queue: if self._ftp is None: self._ftp = self._get_connection() if self._ftp: work_dir = None file_path = self.queue.popleft() if isinstance(file_path, tuple): file_path, work_dir = file_path if BaseUtils.is_file_exists(BaseUtils.win_encode_path(file_path)): try: self._send_file(file_path, work_dir) except ftplib.all_errors as err: self._host_api.timeout( 100, lambda: self.callback(file_path, -2, error=err) ) self.queue.append(file_path) self._close_connection() except Exception as err: self._host_api.timeout( 100, lambda: self.callback(file_path, -3, error=err) ) else: self._host_api.timeout( 100, lambda: self.callback(file_path, -1, error="File not found"), ) self._host_api.timeout(500, self._sender) else: self._work_now = False self._close_connection()
[документация] def files(self, file_paths, *args, **kwargs): """Отправка файлов Note: Можно указать отдельный путь на ftp сервере для каждого файла. Для этого список файлов на отправку должен быть приведен к виду ``[(shot_path, ftp_path), ...]`` При этом так же будет учитываться глобальная папка :obj:`work_dir` заданная при инициализации класса. Args: file_paths (:obj:`str` | :obj:`list`): Путь до файла или список файлов для отправки """ if not isinstance(file_paths, list): file_paths = [file_paths] self.queue.extend(file_paths) if not self._work_now: self._work_now = True self._sender()