diff --git a/docs/source/_static/images/path_01.png b/docs/source/_static/images/path_01.png new file mode 100644 index 0000000..bfdb76f Binary files /dev/null and b/docs/source/_static/images/path_01.png differ diff --git a/docs/source/_static/images/path_02.png b/docs/source/_static/images/path_02.png new file mode 100644 index 0000000..4e54ea0 Binary files /dev/null and b/docs/source/_static/images/path_02.png differ diff --git a/docs/source/_static/images/path_03.png b/docs/source/_static/images/path_03.png new file mode 100644 index 0000000..8667d23 Binary files /dev/null and b/docs/source/_static/images/path_03.png differ diff --git a/docs/source/generated/easymacro.rst b/docs/source/generated/easymacro.rst index 2c87f10..2550bc0 100644 --- a/docs/source/generated/easymacro.rst +++ b/docs/source/generated/easymacro.rst @@ -40,11 +40,15 @@ .. autosummary:: Dates + Hash Json LOServer MBT Macro MessageBoxType + Paths + Shell + Timer commands diff --git a/docs/source/index.rst b/docs/source/index.rst index 1320d50..e9d7d5e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,7 @@ You can used **easymacro** with any extension or directly in your macros. install tools_debug tools + paths api diff --git a/docs/source/paths.rst b/docs/source/paths.rst new file mode 100644 index 0000000..d4e315b --- /dev/null +++ b/docs/source/paths.rst @@ -0,0 +1,101 @@ +Paths and files +=============== + +Remember, always import library first. + +.. code-block:: python + + import easymacro as app + + +Get info path +------------- + +.. code-block:: python + + path = '/home/mau/myfile.ods' + p = app.path(path) + + app.debug(p.path) + app.debug(p.file_name) + app.debug(p.name) + app.debug(p.ext) + app.debug(p.size) + app.debug(p.url) + +.. image:: _static/images/path_01.png + +| + +Get info like tuple + +.. code-block:: python + + app.debug(p.info) + +.. image:: _static/images/path_02.png + +| + +Or like dict + +.. code-block:: python + + app.debug(p.dict) + +.. image:: _static/images/path_03.png + + +Get home path +------------- + +.. code-block:: python + + p = app.path + app.debug(p.home) + + +Get documento path +------------------ + +.. code-block:: python + + p = app.path + app.debug(p.documents) + + +Get temporary directory +----------------------- + +.. code-block:: python + + p = app.path + app.debug(p.temp_dir) + + +Get path user profile +--------------------- + +.. code-block:: python + + p = app.path + app.debug(p.user_profile) + + +Get path user config +-------------------- + +.. code-block:: python + + p = app.path + app.debug(p.user_config) + + +Get python executable path +-------------------------- + +.. code-block:: python + + p = app.path + app.debug(p.python) + diff --git a/docs/source/tools.rst b/docs/source/tools.rst index 83191d8..487067d 100644 --- a/docs/source/tools.rst +++ b/docs/source/tools.rst @@ -453,20 +453,20 @@ Execute macro in other thread Call external program -^^^^^^^^^^^^^^^^^^^^^ +--------------------- .. code-block:: python app_name = 'gnome-calculator' - app.run(app_name) - app.msgbox('ok') + app.shell.run(app_name) + app.debug(app_name) Call command line and capture output .. code-block:: python args = 'ls -lh ~' - result = app.run(args, True) + result = app.shell.run(args, True) app.debug(result) .. code-block:: bash @@ -502,7 +502,68 @@ Call command line and capture output line by line. Timer -^^^^^ +----- + +Only once +^^^^^^^^^ + +Execute any macro only once in N seconds. + +.. code-block:: python + + TIMER_NAME = 'clock' + + def show_time(): + app.debug(app.dates.time) + return + + def start_clock(): + seconds = 5 + macro = { + 'library': 'test', + 'name': 'show_time', + } + app.timer.once(TIMER_NAME, seconds, macro) + return + + + def main(args=None): + start_clock() + return + + +Cancel execution, before start. + +.. code-block:: python + + TIMER_NAME = 'clock' + + def show_time(): + app.debug(app.dates.time) + return + + def start_clock(): + seconds = 60 + macro = { + 'library': 'test', + 'name': 'show_time', + } + app.timer.once(TIMER_NAME, seconds, macro) + return + + def stop_clock(): + app.timer.cancel(TIMER_NAME) + return + + +.. code-block:: bash + + 26/02/2022 12:23:09 - INFO - Event: "clock", started... execute in 60 seconds + 26/02/2022 12:23:16 - INFO - Cancel event: "clock", ok... + + +Every seconds +^^^^^^^^^^^^^ Execute any macro every seconds. @@ -511,7 +572,7 @@ Execute any macro every seconds. TIMER_NAME = 'clock' def show_time(): - app.debug(app.now(True)) + app.debug(app.dates.time) return def start_clock(): @@ -520,46 +581,74 @@ Execute any macro every seconds. 'library': 'test', 'name': 'show_time', } - app.start_timer(TIMER_NAME, seconds, macro) + app.timer.start(TIMER_NAME, seconds, macro) return def stop_clock(): - app.stop_timer(TIMER_NAME) + app.timer.stop(TIMER_NAME) return def main(args=None): start_clock() return -Execute `stop_clock` for stop timer. +Execute **stop_clock** for stop timer. .. code-block:: bash - 21/06/2021 22:43:17 - INFO - Timer started... show_time - 21/06/2021 22:43:18 - DEBUG - 22:43:18.080315 - 21/06/2021 22:43:19 - DEBUG - 22:43:19.082211 + 26/02/2022 11:28:01 - INFO - Timer 'clock' started, execute macro: 'show_time' + 26/02/2022 11:28:02 - DEBUG - 11:28:02 + 26/02/2022 11:28:03 - DEBUG - 11:28:03 ... - 21/06/2021 22:43:46 - DEBUG - 22:43:46.126446 - 21/06/2021 22:43:47 - DEBUG - 22:43:47.128487 - 21/06/2021 22:43:47 - INFO - Timer stopped... show_time + 26/02/2022 11:28:08 - DEBUG - 11:28:08 + 26/02/2022 11:28:09 - DEBUG - 11:28:09 + 26/02/2022 11:28:10 - INFO - Timer stopped... + +.. note:: + + Be sure to use a unique name for each timer. + +.. warning:: + + Be sure to macro for execute not block UI LibO Get digest -^^^^^^^^^^ +---------- + +For default get digest in hex .. code-block:: python data = 'LibreOffice with Python' - digest = app.sha256(data) - app.msgbox(digest) + digest = app.hash.digest('md5', data) + app.debug('MD5 = ', digest) - digest = app.sha512(data) - app.msgbox(digest) + digest = app.hash.digest('sha1', data) + app.debug('SHA1 = ', digest) + + digest = app.hash.digest('sha256', data) + app.debug('SHA256 = ', digest) + + digest = app.hash.digest('sha512', data) + app.debug('SHA512 = ', digest) + + # Get bytes + digest = app.hash.digest('md5', data, False) + app.debug('MD5 = ', digest) + +.. code-block:: bash + + 26/02/2022 15:57:53 - DEBUG - MD5 = e0cb96d2c04b26db79dbd30c4d56b555 + 26/02/2022 15:57:53 - DEBUG - SHA1 = 7006fb17b7a235245cfc986710a11f10543ae10d + 26/02/2022 15:57:53 - DEBUG - SHA256 = 3fe4586d51fa3e352ec28c05b7e71eaee2e41d5ee78f372c44eeb2f433f7e002 + 26/02/2022 15:57:53 - DEBUG - SHA512 = b6eaea6bc11956eae7f990034ff950eba4b0fe51a577d301272cc8b4c1c603abd36ce852311766e5af2f603d1d96741797b62d4b405459348bacae7ec54e2982 + 26/02/2022 15:57:53 - DEBUG - MD5 = b'\xe0\xcb\x96\xd2\xc0K&\xdby\xdb\xd3\x0cMV\xb5U' Save and get configurations -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +--------------------------- You can save any data. diff --git a/source/easymacro.py b/source/easymacro.py index 548be88..d3496b3 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -20,18 +20,23 @@ import datetime import getpass +import hashlib import json import logging import os import platform +import shlex +import shutil import socket import subprocess import sys +import tempfile import threading import time import traceback from functools import wraps +from pathlib import Path from pprint import pprint from typing import Any @@ -64,6 +69,11 @@ _info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join( SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' +_EVENTS = {} +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' + CTX = uno.getComponentContext() SM = CTX.getServiceManager() @@ -80,56 +90,6 @@ class MessageBoxType(): MBT = MessageBoxType -def debug(*messages) -> None: - """Show messages debug - - :param messages: List of messages to debug - :type messages: list[Any] - """ - - data = [str(m) for m in messages] - log.debug('\t'.join(data)) - return - - -def error(message: Any) -> None: - """Show message error - - :param message: The message error - :type message: Any - """ - - log.error(message) - return - - -def info(*messages) -> None: - """Show messages info - - :param messages: List of messages to debug - :type messages: list[Any] - """ - - data = [str(m) for m in messages] - log.info('\t'.join(data)) - return - - -def save_log(path: str, data: Any) -> None: - """Save data in file, data append to end and automatic add current time. - - :param path: Path to save log - :type path: str - :param data: Data to save in file log - :type data: Any - """ - - with open(path, 'a') as f: - f.write(f'{str(now())[:19]} - ') - pprint(data, stream=f) - return - - def create_instance(name: str, with_context: bool=False, argument: Any=None) -> Any: """Create a service instance @@ -183,6 +143,22 @@ def get_app_config(node_name: str, key: str='') -> Any: return value +# Get info LibO +NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') +VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') +LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') +LANG = LANGUAGE.split('-')[0] + +INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{_info_debug}" + +# Get start date from Calc configuration +node = '/org.openoffice.Office.Calc/Calculate/Other/Date' +year = get_app_config(node, 'YY') +month = get_app_config(node, 'MM') +day = get_app_config(node, 'DD') +DATE_OFFSET = datetime.date(year, month, day).toordinal() + + def set_app_config(node_name: str, key: str, new_value: Any) -> Any: """Update value for key in node name. @@ -217,6 +193,56 @@ def set_app_config(node_name: str, key: str, new_value: Any) -> Any: return result +def debug(*messages) -> None: + """Show messages debug + + :param messages: List of messages to debug + :type messages: list[Any] + """ + + data = [str(m) for m in messages] + log.debug('\t'.join(data)) + return + + +def error(message: Any) -> None: + """Show message error + + :param message: The message error + :type message: Any + """ + + log.error(message) + return + + +def info(*messages) -> None: + """Show messages info + + :param messages: List of messages to debug + :type messages: list[Any] + """ + + data = [str(m) for m in messages] + log.info('\t'.join(data)) + return + + +def save_log(path: str, data: Any) -> None: + """Save data in file, data append to end and automatic add current time. + + :param path: Path to save log + :type path: str + :param data: Data to save in file log + :type data: Any + """ + + with open(path, 'a') as f: + f.write(f'{str(now())[:19]} - ') + pprint(data, stream=f) + return + + def _set_app_command(command: str, disable: bool): NEW_NODE_NAME = f'zaz_disable_command_{command.lower()}' name = 'com.sun.star.configuration.ConfigurationProvider' @@ -270,22 +296,6 @@ class commands(): return _set_app_command(self._command, False) -# Get info LibO -NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName') -VERSION = get_app_config('org.openoffice.Setup/Product','ooSetupVersion') -LANGUAGE = get_app_config('org.openoffice.Setup/L10N/', 'ooLocale') -LANG = LANGUAGE.split('-')[0] - -INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{_info_debug}" - -# Get start date from Calc configuration -node = '/org.openoffice.Office.Calc/Calculate/Other/Date' -year = get_app_config(node, 'YY') -month = get_app_config(node, 'MM') -day = get_app_config(node, 'DD') -DATE_OFFSET = datetime.date(year, month, day).toordinal() - - def mri(obj: Any) -> None: """Inspect object with MRI Extension @@ -487,6 +497,16 @@ class Dates(object): """ return datetime.date.today() + @_classproperty + def time(cls): + """Current local time + + :return: Return the current local time + :rtype: datetime.time + """ + t = cls.now.time().replace(microsecond=0) + return t + @_classproperty def epoch(cls): """Get unix time @@ -662,11 +682,372 @@ class Macro(object): return result +class Shell(object): + """Class for subprocess + + `See Subprocess `_ + """ + @classmethod + def run(cls, command, capture=False, split=False): + """Execute commands + + :param command: Command to run + :type command: str + :param capture: If capture result of command + :type capture: bool + :param split: Some commands need split. + :type split: bool + :return: Result of command + :rtype: Any + """ + if split: + cmd = shlex.split(command) + result = subprocess.run(cmd, capture_output=capture, text=True, shell=IS_WIN) + if capture: + result = result.stdout + else: + result = result.returncode + else: + if capture: + result = subprocess.check_output(command, shell=True).decode() + else: + result = subprocess.Popen(command) + return result + + @classmethod + def popen(cls, command): + """Execute commands and return line by line + + :param command: Command to run + :type command: str + :return: Result of command + :rtype: Any + """ + try: + proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + for line in proc.stdout: + yield line.decode().rstrip() + except Exception as e: + error(e) + yield (e.errno, e.strerror) + + +class Timer(object): + """Class for timer thread""" + + class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self._event = event + self._seconds = seconds + self._macro = macro + + def run(self): + while not self._event.wait(self._seconds): + Macro.call(self._macro) + info('\tTimer stopped... ') + return + + @classmethod + def exists(cls, name): + """Validate in timer **name** exists + + :param name: Timer name, it must be unique + :type name: str + :return: True if exists timer name + :rtype: bool + """ + global _EVENTS + return name in _EVENTS + + @classmethod + def start(cls, name: str, seconds: float, macro: dict): + """Start timer **name** every **seconds** and execute **macro** + + :param name: Timer name, it must be unique + :type name: str + :param seconds: Seconds for wait + :type seconds: float + :param macro: Macro for execute + :type macro: dict + """ + global _EVENTS + + _EVENTS[name] = threading.Event() + info(f"Timer '{name}' started, execute macro: '{macro['name']}'") + thread = cls.TimerThread(_EVENTS[name], seconds, macro) + thread.start() + return + + @classmethod + def stop(cls, name: str): + """Stop timer **name** + + :param name: Timer name + :type name: str + """ + global _EVENTS + _EVENTS[name].set() + del _EVENTS[name] + return + + @classmethod + def once(cls, name: str, seconds: float, macro: dict): + """Start timer **name** only once in **seconds** and execute **macro** + + :param name: Timer name, it must be unique + :type name: str + :param seconds: Seconds for wait before execute macro + :type seconds: float + :param macro: Macro for execute + :type macro: dict + """ + global _EVENTS + + _EVENTS[name] = threading.Timer(seconds, Macro.call, (macro,)) + _EVENTS[name].start() + info(f'Event: "{name}", started... execute in {seconds} seconds') + + return + + @classmethod + def cancel(cls, name: str): + """Cancel timer **name** only once events. + + :param name: Timer name, it must be unique + :type name: str + """ + global _EVENTS + + if name in _EVENTS: + try: + _EVENTS[name].cancel() + del _EVENTS[name] + info(f'Cancel event: "{name}", ok...') + except Exception as e: + error(e) + else: + debug(f'Cancel event: "{name}", not exists...') + return + + +class Hash(object): + """Class for hash + """ + @classmethod + def digest(cls, method: str, data: str, in_hex: bool=True): + """Get digest from data with method + + :param method: Digest method: md5, sha1, sha256, sha512, etc... + :type method: str + :param data: Data for get digest + :type data: str + :param in_hex: If True, get digest in hexadecimal, if False, get bytes + :type in_hex: bool + :return: bytes or hex digest + :rtype: bytes or str + """ + + result = '' + obj = getattr(hashlib, method)(data.encode()) + if in_hex: + result = obj.hexdigest() + else: + result = obj.digest() + return result + + +class Paths(object): + """Class for paths + """ + FILE_PICKER = 'com.sun.star.ui.dialogs.FilePicker' + FOLDER_PICKER = 'com.sun.star.ui.dialogs.FolderPicker' + REMOTE_FILE_PICKER = 'com.sun.star.ui.dialogs.RemoteFilePicker' + OFFICE_FILE_PICKER = 'com.sun.star.ui.dialogs.OfficeFilePicker' + + def __init__(self, path=''): + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + self._path = Path(path) + + @property + def path(self): + """Get base path""" + return str(self._path.parent) + + @property + def file_name(self): + """Get file name""" + return self._path.name + + @property + def name(self): + """Get name""" + return self._path.stem + + @property + def ext(self): + """Get extension""" + return self._path.suffix[1:] + + @property + def size(self): + """Get size""" + return self._path.stat().st_size + + @property + def url(self): + """Get like URL""" + return self._path.as_uri() + + @property + def info(self): + """Get all info like tuple""" + i = (self.path, self.file_name, self.name, self.ext, self.size, self.url) + return i + + @property + def dict(self): + """Get all info like dict""" + data = { + 'path': self.path, + 'file_name': self.file_name, + 'name': self.name, + 'ext': self.ext, + 'size': self.size, + 'url': self.url, + } + return data + + @_classproperty + def home(self): + """Get user home""" + return str(Path.home()) + + @_classproperty + def documents(self): + """Get user save documents""" + return self.config() + + @_classproperty + def temp_dir(self): + """Get temporary directory in system""" + return tempfile.gettempdir() + + @_classproperty + def python(self): + """Get path executable python""" + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) + else: + path = sys.executable + return path + + @_classproperty + def user_profile(self): + """Get path user profile""" + path = self.config('UserConfig') + path = str(Path(path).parent) + return path + + @_classproperty + def user_config(self): + """Get path config in user profile""" + path = self.config('UserConfig') + return path + + @classmethod + def to_system(cls, path:str) -> str: + """Convert paths in URL to system + + :param path: Path to convert + :type path: str + :return: Path system format + :rtype: str + """ + if path.startswith('file://'): + path = str(Path(uno.fileUrlToSystemPath(path)).resolve()) + return path + + @classmethod + def to_url(cls, path: str) -> str: + """Convert paths in format system to URL + + :param path: Path to convert + :type path: str + :return: Path in URL + :rtype: str + """ + if not path.startswith('file://'): + path = Path(path).as_uri() + return path + + @classmethod + def config(cls, name: str='Work') -> str: + """Return path from config + + :param name: Name in service PathSettings, default get path documents + :type name: str + :return: Path in config, if exists. + :rtype: str + + `See Api XPathSettings `_ + """ + path = create_instance('com.sun.star.util.PathSettings') + path = cls.to_system(getattr(path, name)) + return path + + @classmethod + def join(cls, *paths: str) -> str: + """Join paths + + :param paths: Paths to join + :type paths: list + :return: New path with joins + :rtype: str + """ + path = str(Path(paths[0]).joinpath(*paths[1:])) + return path + + @classmethod + def exists(cls, path: str) -> bool: + """If exists path + + :param path: Path for validate + :type path: str + :return: True if path exists, False if not. + :rtype: bool + """ + path = cls.to_system(path) + result = Path(path).exists() + return result + + @classmethod + def exists_app(cls, name_app: str) -> bool: + """If exists app in system + + :param name_app: Name of application + :type name_app: str + :return: True if app exists, False if not. + :rtype: bool + """ + result = bool(shutil.which(name_app)) + return result + + def __getattr__(name): classes = { 'dates': Dates, 'json': Json, 'macro': Macro, + 'shell': Shell, + 'timer': Timer, + 'hash': Hash, + 'path': Paths, } if name in classes: return classes[name]