#!/usr/bin/env python3 # == Rapid Develop Macros in LibreOffice == # ~ https://git.cuates.net/elmau/easymacro # ~ easymacro is free software: you can redistribute it and/or modify # ~ it under the terms of the GNU General Public License as published by # ~ the Free Software Foundation, either version 3 of the License, or # ~ (at your option) any later version. # ~ easymacro is distributed in the hope that it will be useful, # ~ but WITHOUT ANY WARRANTY; without even the implied warranty of # ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # ~ GNU General Public License for more details. # ~ You should have received a copy of the GNU General Public License # ~ along with easymacro. If not, see . import datetime import getpass import json import logging import os import platform import socket import subprocess import sys import threading import time import traceback from functools import wraps from pprint import pprint from typing import Any import uno from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.beans import PropertyValue, NamedValue LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_DATE = '%d/%m/%Y %H:%M:%S' logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) # Global variables OS = platform.system() DESKTOP = os.environ.get('DESKTOP_SESSION', '') PC = platform.node() USER = getpass.getuser() IS_WIN = OS == 'Windows' IS_MAC = OS == 'Darwin' _info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' CTX = uno.getComponentContext() SM = CTX.getServiceManager() # UNO Enum class MessageBoxType(): """Class for import enum `See Api MessageBoxType `_ """ from com.sun.star.awt.MessageBoxType \ import MESSAGEBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX 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 :param name: Name of service :type name: str :param with_context: If used context :type with_context: bool :param argument: If needed some argument :type argument: Any :return: PyUno instance :rtype: PyUno Object """ if with_context: instance = SM.createInstanceWithContext(name, CTX) elif argument: instance = SM.createInstanceWithArguments(name, (argument,)) else: instance = SM.createInstance(name) return instance def get_app_config(node_name: str, key: str='') -> Any: """Get any key from any node from LibreOffice configuration. :param node_name: Name of node :type name: str :param key: Name of key :type key: str :return: Any value :rtype: Any `See Api ConfigurationProvider `_ """ name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) value = '' try: value = cp.createInstanceWithArguments(service, (node,)) if value and value.hasByName(key): value = value.getPropertyValue(key) except Exception as e: error(e) value = '' return value def set_app_config(node_name: str, key: str, new_value: Any) -> Any: """Update value for key in node name. :param node_name: Name of node :type name: str :param key: Name of key :type key: str :return: True if update sucesfully :rtype: bool `See Api ConfigurationUpdateAccess `_ """ result = True current_value = '' name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationUpdateAccess' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) update = cp.createInstanceWithArguments(service, (node,)) try: current_value = update.getPropertyValue(key) update.setPropertyValue(key, new_value) update.commitChanges() except Exception as e: error(e) if update.hasByName(key) and current_value: update.setPropertyValue(key, current_value) update.commitChanges() result = False return result def _set_app_command(command: str, disable: bool): NEW_NODE_NAME = f'zaz_disable_command_{command.lower()}' name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationUpdateAccess' node_name = '/org.openoffice.Office.Commands/Execute/Disabled' cp = create_instance(name, True) node = PropertyValue(Name='nodepath', Value=node_name) update = cp.createInstanceWithArguments(service, (node,)) result = True try: if disable: new_node = update.createInstanceWithArguments(()) new_node.setPropertyValue('Command', command) update.insertByName(NEW_NODE_NAME, new_node) else: update.removeByName(NEW_NODE_NAME) update.commitChanges() except Exception as e: result = False return result class commands(): """Class for disable and enable commands :param command: UNO Command for disable or enable :type command: str `See DispatchCommands `_ """ def __init__(self, command): self._command = command def disable(self): """Disable command :return: True if correctly disable, False if not. :rtype: bool """ return _set_app_command(self._command, True) def enabled(self): """Enable command :return: True if correctly enable, False if not. :rtype: bool """ 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 :param obj: Any pyUno object :type obj: Any `See MRI `_ """ mri = create_instance('mytools.Mri') if m is None: msg = 'Extension MRI not found' error(msg) return if hasattr(obj, 'obj'): obj = obj.obj mri.inspect(obj) return def catch_exception(f): """Catch exception for any function :param f: Any Python function :type f: Function instance """ @wraps(f) def func(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: name = f.__name__ if IS_WIN: msgbox(traceback.format_exc()) log.error(name, exc_info=True) return func def msgbox(message: Any, title: str=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, \ type_message_box=MessageBoxType.INFOBOX) -> int: """Create message box :param message: Any type message, all is converted to string. :type message: Any :param title: The title for message box :type title: str :param buttons: A combination of `com::sun::star::awt::MessageBoxButtons `_ :type buttons: long :param type_message_box: The `message box type `_ :type type_message_box: enum :return: `MessageBoxResult `_ :rtype: int `See Api XMessageBoxFactory `_ """ toolkit = create_instance('com.sun.star.awt.Toolkit') parent = toolkit.getDesktopWindow() box = toolkit.createMessageBox(parent, type_message_box, buttons, title, str(message)) return box.execute() def question(message: str, title: str=TITLE) -> bool: """Create message box question, show buttons YES and NO :param message: Message question :type message: str :param title: The title for message box :type title: str :return: True if user click YES and False if click NO :rtype: bool """ result = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, MessageBoxType.QUERYBOX) return result == YES def warning(message: Any, title: str=TITLE) -> int: """Create message box with icon warning :param message: Any type message, all is converted to string. :type message: Any :param title: The title for message box :type title: str :return: MessageBoxResult :rtype: int """ return msgbox(message, title, type_message_box=MessageBoxType.WARNINGBOX) def errorbox(message: Any, title: str=TITLE) -> int: """Create message box with icon error :param message: Any type message, all is converted to string. :type message: Any :param title: The title for message box :type title: str :return: MessageBoxResult :rtype: int """ return msgbox(message, title, type_message_box=MessageBoxType.ERRORBOX) def sleep(seconds: int): """Sleep """ time.sleep(seconds) return def run_in_thread(fn): """Run any function in thread :param fn: Any Python function (macro) :type fn: Function instance """ def run(*k, **kw): t = threading.Thread(target=fn, args=k, kwargs=kw) t.start() return t return run def dict_to_property(values: dict, uno_any: bool=False): """Convert dictionary to array of PropertyValue :param values: Dictionary of values :type values: dict :param uno_any: If return like array uno.Any :type uno_any: bool :return: Tuple of PropertyValue or array uno.Any :rtype: tuples or uno.Any """ ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) return ps def _property_to_dict(values): d = {v.Name: v.Value for v in values} return d def data_to_dict(data) -> dict: """Convert tuples, list, PropertyValue, NamedValue to dictionary :param data: Dictionary of values :type data: array of tuples, list, PropertyValue or NamedValue :return: Dictionary :rtype: dict """ d = {} if not isinstance(data, (tuple, list)): return d if isinstance(data[0], (tuple, list)): d = {r[0]: r[1] for r in data} elif isinstance(data[0], (PropertyValue, NamedValue)): d = _property_to_dict(data) return d class _classproperty: def __init__(self, method=None): self.fget = method def __get__(self, instance, cls=None): return self.fget(cls) def getter(self, method): self.fget = method return self class Dates(object): """Class for datetimes """ _start = None @_classproperty def now(cls): """Current local date and time :return: Return the current local date and time :rtype: datetime """ return datetime.datetime.now() @_classproperty def today(cls): """Current local date :return: Return the current local date :rtype: date """ return datetime.date.today() @_classproperty def epoch(cls): """Get unix time :return: Return unix time :rtype: int `See Unix Time `_ """ n = cls.now e = int(time.mktime(n.timetuple())) return e @classmethod def date(cls, year: int, month: int, day: int): """Get date from year, month, day :param year: Year of date :type year: int :param month: Month of date :type month: int :param day: Day of day :type day: int :return: Return the date :rtype: date `See Python date `_ """ d = datetime.date(year, month, day) return d @classmethod def str_to_date(cls, str_date: str, template: str, to_calc: bool=False): """Get date from string :param str_date: Date in string :type str_date: str :param template: Formato of date string :type template: str :param to_calc: If date is for used in Calc cell :type to_calc: bool :return: Return date or int if used in Calc :rtype: date or int `See Python strptime `_ """ d = datetime.datetime.strptime(str_date, template).date() if to_calc: d = d.toordinal() - DATE_OFFSET return d @classmethod def calc_to_date(cls, value: float): """Get date from calc value :param value: Float value from cell :type value: float :return: Return the current local date :rtype: date `See Python fromordinal `_ """ d = datetime.date.fromordinal(int(value) + DATE_OFFSET) return d @classmethod def start(cls): """Start counter """ cls._start = cls.now info('Start: ', cls._start) return @classmethod def end(cls, get_seconds: bool=True): """End counter :param get_seconds: If return value in total seconds :type get_seconds: bool :return: Return the timedelta or total seconds :rtype: timedelta or int """ e = cls.now td = e - cls._start result = str(td) if get_seconds: result = td.total_seconds() info('End: ', e) return result class Json(object): """Class for json data """ @classmethod def dumps(cls, data: Any) -> str: """Dumps :param data: Any data :type data: Any :return: Return string json :rtype: str """ return json.dumps(data, indent=4, sort_keys=True) @classmethod def loads(cls, data: str) -> Any: """Loads :param data: String data :type data: str :return: Return any object :rtype: Any """ return json.loads(data) class Macro(object): """Class for call macro `See Scripting Framework `_ """ @classmethod def call(cls, args: dict, in_thread: bool=False): """Call any macro :param args: Dictionary with macro location :type args: dict :param in_thread: If execute in thread :type in_thread: bool :return: Return None or result of call macro :rtype: Any """ result = None if in_thread: t = threading.Thread(target=cls._call, args=(args,)) t.start() else: result = cls._call(args) return result @classmethod def _get_url_script(cls, args: dict): library = args['library'] name = args['name'] language = args.get('language', 'Python') location = args.get('location', 'user') module = args.get('module', '.') if language == 'Python': module = '.py$' elif language == 'Basic': module = f".{module}." if location == 'user': location = 'application' url = 'vnd.sun.star.script' url = f'{url}:{library}{module}{name}?language={language}&location={location}' return url @classmethod def _call(cls, args: dict): url = cls._get_url_script(args) args = args.get('args', ()) service = 'com.sun.star.script.provider.MasterScriptProviderFactory' factory = create_instance(service) script = factory.createScriptProvider('').getScript(url) result = script.invoke(args, None, None)[0] return result def __getattr__(name): classes = { 'dates': Dates, 'json': Json, 'macro': Macro, } if name in classes: return classes[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") class LOServer(object): """Started LibeOffice like server """ HOST = 'localhost' PORT = '8100' ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext' CMD = ['soffice', '-env:SingleAppInstance=false', '-env:UserInstallation=file:///tmp/LO_Process8100', '--headless', '--norestore', '--invisible', f'--accept={ARG}'] def __init__(self): self._server = None self._ctx = None self._sm = None self._start_server() self._init_values() def _init_values(self): global CTX global SM if not self.is_running: return ctx = uno.getComponentContext() service = 'com.sun.star.bridge.UnoUrlResolver' resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) self._sm = self._ctx.getServiceManager() CTX = self._ctx SM = self._sm return @property def is_running(self): try: s = socket.create_connection((self.HOST, self.PORT), 5.0) s.close() debug('LibreOffice is running...') return True except ConnectionRefusedError: return False def _start_server(self): if self.is_running: return for i in range(3): self._server = subprocess.Popen(self.CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(3) if self.is_running: break return def stop(self): """Stop server """ if self._server is None: print('Search pgrep soffice') else: self._server.terminate() debug('LibreOffice is stop...') return def _create_instance(self, name, with_context=True): if with_context: instance = self._sm.createInstanceWithContext(name, self._ctx) else: instance = self._sm.createInstance(name) return instance