easymacro/source/easymacro.py

750 lines
20 KiB
Python
Raw Normal View History

2022-02-21 23:43:58 -06:00
#!/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 <https://www.gnu.org/licenses/>.
2022-02-23 22:49:43 -06:00
import datetime
2022-02-21 23:43:58 -06:00
import getpass
2022-02-26 00:11:41 -06:00
import json
2022-02-21 23:43:58 -06:00
import logging
import os
import platform
import socket
import subprocess
import sys
2022-02-26 00:11:41 -06:00
import threading
2022-02-21 23:43:58 -06:00
import time
2022-02-23 22:49:43 -06:00
import traceback
2022-02-21 23:43:58 -06:00
2022-02-23 22:49:43 -06:00
from functools import wraps
from pprint import pprint
2022-02-21 23:43:58 -06:00
from typing import Any
import uno
2022-02-23 22:49:43 -06:00
from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS
from com.sun.star.awt.MessageBoxResults import YES
2022-02-26 00:11:41 -06:00
from com.sun.star.beans import PropertyValue, NamedValue
2022-02-21 23:43:58 -06:00
2022-02-24 23:59:04 -06:00
2022-02-21 23:43:58 -06:00
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'
2022-02-23 22:49:43 -06:00
2022-02-21 23:43:58 -06:00
_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()
2022-02-24 23:59:04 -06:00
# UNO Enum
class MessageBoxType():
2022-02-25 22:43:37 -06:00
"""Class for import enum
`See Api MessageBoxType <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt.html#ad249d76933bdf54c35f4eaf51a5b7965>`_
"""
2022-02-24 23:59:04 -06:00
from com.sun.star.awt.MessageBoxType \
import MESSAGEBOX, INFOBOX, WARNINGBOX, ERRORBOX, QUERYBOX
MBT = MessageBoxType
2022-02-23 22:49:43 -06:00
2022-02-24 23:59:04 -06:00
def debug(*messages) -> None:
"""Show messages debug
:param messages: List of messages to debug
:type messages: list[Any]
2022-02-23 22:49:43 -06:00
"""
2022-02-24 23:59:04 -06:00
data = [str(m) for m in messages]
2022-02-21 23:43:58 -06:00
log.debug('\t'.join(data))
return
2022-02-23 22:49:43 -06:00
def error(message: Any) -> None:
2022-02-24 23:59:04 -06:00
"""Show message error
2022-02-23 22:49:43 -06:00
2022-02-24 23:59:04 -06:00
:param message: The message error
:type message: Any
2022-02-23 22:49:43 -06:00
"""
log.error(message)
return
2022-02-24 23:59:04 -06:00
def info(*messages) -> None:
"""Show messages info
2022-02-23 22:49:43 -06:00
2022-02-24 23:59:04 -06:00
:param messages: List of messages to debug
:type messages: list[Any]
2022-02-23 22:49:43 -06:00
"""
2022-02-24 23:59:04 -06:00
data = [str(m) for m in messages]
2022-02-23 22:49:43 -06:00
log.info('\t'.join(data))
return
def save_log(path: str, data: Any) -> None:
2022-02-24 23:59:04 -06:00
"""Save data in file, data append to end and automatic add current time.
2022-02-23 22:49:43 -06:00
2022-02-24 23:59:04 -06:00
:param path: Path to save log
:type path: str
:param data: Data to save in file log
:type data: Any
2022-02-23 22:49:43 -06:00
"""
with open(path, 'a') as f:
f.write(f'{str(now())[:19]} - ')
pprint(data, stream=f)
2022-02-21 23:43:58 -06:00
return
2022-02-24 23:59:04 -06:00
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
"""
2022-02-21 23:43:58 -06:00
if with_context:
instance = SM.createInstanceWithContext(name, CTX)
2022-02-24 23:59:04 -06:00
elif argument:
instance = SM.createInstanceWithArguments(name, (argument,))
2022-02-21 23:43:58 -06:00
else:
instance = SM.createInstance(name)
2022-02-24 23:59:04 -06:00
2022-02-21 23:43:58 -06:00
return instance
2022-02-24 23:59:04 -06:00
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 <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1configuration_1_1ConfigurationProvider.html>`_
"""
2022-02-21 23:43:58 -06:00
name = 'com.sun.star.configuration.ConfigurationProvider'
service = 'com.sun.star.configuration.ConfigurationAccess'
cp = create_instance(name, True)
node = PropertyValue(Name='nodepath', Value=node_name)
2022-02-24 23:59:04 -06:00
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:
2022-02-25 22:43:37 -06:00
"""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 <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1configuration_1_1ConfigurationUpdateAccess.html>`_
"""
2022-02-24 23:59:04 -06:00
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,))
2022-02-21 23:43:58 -06:00
try:
2022-02-24 23:59:04 -06:00
current_value = update.getPropertyValue(key)
update.setPropertyValue(key, new_value)
update.commitChanges()
2022-02-21 23:43:58 -06:00
except Exception as e:
error(e)
2022-02-24 23:59:04 -06:00
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,))
2022-02-25 22:43:37 -06:00
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
2022-02-24 23:59:04 -06:00
2022-02-25 22:43:37 -06:00
return result
2022-02-24 23:59:04 -06:00
class commands():
2022-02-25 22:43:37 -06:00
"""Class for disable and enable commands
:param command: UNO Command for disable or enable
:type command: str
`See DispatchCommands <https://wiki.documentfoundation.org/Development/DispatchCommands>`_
"""
2022-02-24 23:59:04 -06:00
def __init__(self, command):
self._command = command
2022-02-25 22:43:37 -06:00
2022-02-24 23:59:04 -06:00
def disable(self):
2022-02-25 22:43:37 -06:00
"""Disable command
:return: True if correctly disable, False if not.
:rtype: bool
"""
2022-02-24 23:59:04 -06:00
return _set_app_command(self._command, True)
2022-02-25 22:43:37 -06:00
2022-02-24 23:59:04 -06:00
def enabled(self):
2022-02-25 22:43:37 -06:00
"""Enable command
:return: True if correctly enable, False if not.
:rtype: bool
"""
2022-02-24 23:59:04 -06:00
return _set_app_command(self._command, False)
2022-02-21 23:43:58 -06:00
# Get info LibO
2022-02-23 22:49:43 -06:00
NAME = TITLE = get_app_config('org.openoffice.Setup/Product', 'ooName')
2022-02-21 23:43:58 -06:00
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}"
2022-02-25 22:43:37 -06:00
# Get start date from Calc configuration
2022-02-23 22:49:43 -06:00
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()
2022-02-24 23:59:04 -06:00
def mri(obj: Any) -> None:
"""Inspect object with MRI Extension
2022-02-23 22:49:43 -06:00
2022-02-24 23:59:04 -06:00
:param obj: Any pyUno object
:type obj: Any
2022-02-23 22:49:43 -06:00
2022-02-24 23:59:04 -06:00
`See MRI <https://github.com/hanya/MRI/releases>`_
2022-02-23 22:49:43 -06:00
"""
2022-02-24 23:59:04 -06:00
mri = create_instance('mytools.Mri')
2022-02-23 22:49:43 -06:00
if m is None:
msg = 'Extension MRI not found'
error(msg)
return
if hasattr(obj, 'obj'):
obj = obj.obj
2022-02-24 23:59:04 -06:00
mri.inspect(obj)
2022-02-23 22:49:43 -06:00
return
def catch_exception(f):
2022-02-24 23:59:04 -06:00
"""Catch exception for any function
:param f: Any Python function
:type f: Function instance
"""
2022-02-23 22:49:43 -06:00
@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
2022-02-21 23:43:58 -06:00
2022-02-24 23:59:04 -06:00
def msgbox(message: Any, title: str=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, \
2022-02-25 22:43:37 -06:00
type_message_box=MessageBoxType.INFOBOX) -> int:
2022-02-24 23:59:04 -06:00
"""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 <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1MessageBoxButtons.html>`_
:type buttons: long
:param type_message_box: The `message box type <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt.html#ad249d76933bdf54c35f4eaf51a5b7965>`_
:type type_message_box: enum
2022-02-25 22:43:37 -06:00
:return: `MessageBoxResult <https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1MessageBoxResults.html>`_
:rtype: int
2022-02-24 23:59:04 -06:00
`See Api XMessageBoxFactory <http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html>`_
"""
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()
2022-02-25 22:43:37 -06:00
def question(message: str, title: str=TITLE) -> bool:
2022-02-24 23:59:04 -06:00
"""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
2022-02-25 22:43:37 -06:00
def warning(message: Any, title: str=TITLE) -> int:
2022-02-24 23:59:04 -06:00
"""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
2022-02-25 22:43:37 -06:00
:return: MessageBoxResult
:rtype: int
2022-02-24 23:59:04 -06:00
"""
return msgbox(message, title, type_message_box=MessageBoxType.WARNINGBOX)
2022-02-25 22:43:37 -06:00
def errorbox(message: Any, title: str=TITLE) -> int:
2022-02-24 23:59:04 -06:00
"""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
2022-02-25 22:43:37 -06:00
:return: MessageBoxResult
:rtype: int
2022-02-24 23:59:04 -06:00
"""
return msgbox(message, title, type_message_box=MessageBoxType.ERRORBOX)
2022-02-26 00:11:41 -06:00
def sleep(seconds: int):
2022-02-25 22:43:37 -06:00
"""Sleep
2022-02-24 23:59:04 -06:00
"""
2022-02-25 22:43:37 -06:00
time.sleep(seconds)
return
2022-02-24 23:59:04 -06:00
2022-02-26 00:11:41 -06:00
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
2022-02-25 22:43:37 -06:00
class _classproperty:
def __init__(self, method=None):
self.fget = method
2022-02-24 23:59:04 -06:00
2022-02-25 22:43:37 -06:00
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
2022-02-24 23:59:04 -06:00
"""
2022-02-25 22:43:37 -06:00
_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 <https://en.wikipedia.org/wiki/Unix_time>`_
"""
n = cls.now
e = int(time.mktime(n.timetuple()))
return e
@classmethod
2022-02-26 00:11:41 -06:00
def date(cls, year: int, month: int, day: int):
2022-02-25 22:43:37 -06:00
"""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 <https://docs.python.org/3/library/datetime.html#date-objects>`_
"""
d = datetime.date(year, month, day)
return d
@classmethod
2022-02-26 00:11:41 -06:00
def str_to_date(cls, str_date: str, template: str, to_calc: bool=False):
2022-02-25 22:43:37 -06:00
"""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 <https://docs.python.org/3/library/datetime.html#datetime.datetime.strptime>`_
"""
d = datetime.datetime.strptime(str_date, template).date()
if to_calc:
d = d.toordinal() - DATE_OFFSET
return d
@classmethod
2022-02-26 00:11:41 -06:00
def calc_to_date(cls, value: float):
2022-02-25 22:43:37 -06:00
"""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 <https://docs.python.org/3/library/datetime.html#datetime.datetime.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
2022-02-26 00:11:41 -06:00
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 <https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification>`_
"""
@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
2022-02-25 22:43:37 -06:00
def __getattr__(name):
classes = {
2022-02-26 00:11:41 -06:00
'dates': Dates,
'json': Json,
'macro': Macro,
2022-02-25 22:43:37 -06:00
}
if name in classes:
return classes[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
2022-02-24 23:59:04 -06:00
2022-02-21 23:43:58 -06:00
class LOServer(object):
2022-02-25 22:43:37 -06:00
"""Started LibeOffice like server
"""
2022-02-21 23:43:58 -06:00
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):
2022-02-25 22:43:37 -06:00
"""Stop server
"""
2022-02-21 23:43:58 -06:00
if self._server is None:
print('Search pgrep soffice')
else:
self._server.terminate()
debug('LibreOffice is stop...')
return
2022-02-25 22:43:37 -06:00
def _create_instance(self, name, with_context=True):
2022-02-21 23:43:58 -06:00
if with_context:
instance = self._sm.createInstanceWithContext(name, self._ctx)
else:
instance = self._sm.createInstance(name)
return instance