diff --git a/CHANGELOG b/CHANGELOG index b7b582f..3b183ba 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +v 0.2.0 [27-sep-2019] +--------------------- + - Update easymacro.py + + v 0.1.0 [19-sep-2019] --------------------- - Initial version diff --git a/README.md b/README.md index 7082f2e..16d67fa 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ BCH: `1RPLWHJW34p7pMQV1ft4x7eWhAYw69Dsb` BTC: `3Fe4JuADrAK8Qs7GDAxbSXR8E54avwZJLW` +PayPal :( donate ATT elmau DOT net -* [See the wiki](https://gitlab.com/mauriciobaeza/zaz-favorite/wikis/home) + + +* [Look the wiki](https://gitlab.com/mauriciobaeza/zaz-favorite/wikis/home) * [Mira la wiki](https://gitlab.com/mauriciobaeza/zaz-favorite/wikis/home_es) diff --git a/VERSION b/VERSION index 49b49e4..5faa42c 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -0.1.0 +0.2.0 diff --git a/conf.py b/conf.py index 4820939..0543de4 100644 --- a/conf.py +++ b/conf.py @@ -26,7 +26,7 @@ import logging TYPE_EXTENSION = 1 # ~ https://semver.org/ -VERSION = '0.1.0' +VERSION = '0.2.0' # ~ Your great extension name, not used spaces NAME = 'ZAZFavorites' diff --git a/easymacro.py b/easymacro.py index dd65e9e..3bf53b9 100644 --- a/easymacro.py +++ b/easymacro.py @@ -17,11 +17,12 @@ # ~ You should have received a copy of the GNU General Public License # ~ along with ZAZ. If not, see . - +import base64 import ctypes import datetime import errno import getpass +import hashlib import json import logging import os @@ -34,19 +35,30 @@ import sys import tempfile import threading import time +import traceback import zipfile from collections import OrderedDict from collections.abc import MutableMapping -from datetime import datetime from functools import wraps from operator import itemgetter from pathlib import Path, PurePath from pprint import pprint +from string import Template from subprocess import PIPE +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import formatdate +from email import encoders +import mailbox + import uno import unohelper +from com.sun.star.util import Time, Date, DateTime from com.sun.star.beans import PropertyValue from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES @@ -61,12 +73,20 @@ from com.sun.star.lang import XEventListener from com.sun.star.awt import XActionListener from com.sun.star.awt import XMouseListener +try: + from fernet import Fernet, InvalidToken + CRYPTO = True +except ImportError: + CRYPTO = False + MSG_LANG = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', 'Select file': 'Seleccionar archivo', + 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', + 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', } } @@ -95,7 +115,8 @@ TYPE_DOC = { 'writer': 'com.sun.star.text.TextDocument', 'impress': 'com.sun.star.presentation.PresentationDocument', 'draw': 'com.sun.star.drawing.DrawingDocument', - 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'base': 'com.sun.star.sdb.DocumentDataSource', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', } @@ -134,7 +155,12 @@ MENUS_APP = { } -FILE_NAME_DEBUG = 'zaz-debug.log' +EXT = { + 'pdf': 'pdf', +} + + +FILE_NAME_DEBUG = 'debug.odt' FILE_NAME_CONFIG = 'zaz-{}.json' LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_DATE = '%d/%m/%Y %H:%M:%S' @@ -145,10 +171,16 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) +_start = 0 +_stop_thread = {} +TIMEOUT = 10 + + CTX = uno.getComponentContext() SM = CTX.getServiceManager() +# ~ Export ok def create_instance(name, with_context=False): if with_context: instance = SM.createInstanceWithContext(name, CTX) @@ -178,6 +210,7 @@ NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') +# ~ Export ok def mri(obj): m = create_instance('mytools.Mri') if m is None: @@ -195,7 +228,10 @@ def catch_exception(f): try: return f(*args, **kwargs) except Exception as e: - log.error(f.__name__, exc_info=True) + name = f.__name__ + if IS_WIN: + debug(traceback.format_exc()) + log.error(name, exc_info=True) return func @@ -203,30 +239,30 @@ class LogWin(object): def __init__(self, doc): self.doc = doc + self.doc.Title = FILE_NAME_DEBUG def write(self, info): text = self.doc.Text cursor = text.createTextCursor() cursor.gotoEnd(False) - text.insertString(cursor, str(info), 0) + text.insertString(cursor, str(info) + '\n\n', 0) return +# ~ Export ok def info(data): log.info(data) return +# ~ Export ok def debug(info): if IS_WIN: - # ~ app = LOApp(self.ctx, self.sm, self.desktop, self.toolkit) - # ~ doc = app.getDoc(FILE_NAME_DEBUG) - # ~ if not doc: - # ~ doc = app.newDoc(WRITER) - # ~ out = OutputDoc(doc) - # ~ sys.stdout = out - # ~ pprint(info) - doc = LogWin(new_doc('writer').obj) + doc = get_document(FILE_NAME_DEBUG) + if doc is None: + # ~ doc = new_doc('writer') + return + doc = LogWin(doc.obj) doc.write(info) return @@ -234,14 +270,16 @@ def debug(info): return +# ~ Export ok def error(info): log.error(info) return +# ~ Export ok def save_log(path, data): with open(path, 'a') as out: - out.write('{} -{}- '.format(str(datetime.now())[:19], LOG_NAME)) + out.write('{} -{}- '.format(str(now())[:19], LOG_NAME)) pprint(data, stream=out) return @@ -254,34 +292,40 @@ def run_in_thread(fn): return run -def get_config(key='', prefix='config'): +def now(): + return datetime.datetime.now() + + +# ~ Export ok +def get_config(key='', default=None, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) - values = {} + values = None path = join(get_config_path('UserConfig'), path_json) if not exists_path(path): - return values + return default with open(path, 'r', encoding='utf-8') as fh: data = fh.read() - if data: - values = json.loads(data) + values = json.loads(data) if key: - return values.get(key, None) + return values.get(key, default) return values +# ~ Export ok def set_config(key, value, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) path = join(get_config_path('UserConfig'), path_json) - values = get_config(prefix=prefix) + values = get_config(default={}, prefix=prefix) values[key] = value with open(path, 'w', encoding='utf-8') as fh: json.dump(values, fh, ensure_ascii=False, sort_keys=True, indent=4) - return True + return +# ~ Export ok def sleep(seconds): time.sleep(seconds) return @@ -298,6 +342,7 @@ def _(msg): return MSG_LANG[L][msg] +# ~ Export ok def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): """ Create message box type_msg: infobox, warningbox, errorbox, querybox, messbox @@ -309,15 +354,18 @@ def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infob return mb.execute() +# ~ Export ok def question(message, title=TITLE): res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return res == YES +# ~ Export ok def warning(message, title=TITLE): return msgbox(message, title, type_msg='warningbox') +# ~ Export ok def errorbox(message, title=TITLE): return msgbox(message, title, type_msg='errorbox') @@ -326,10 +374,20 @@ def get_desktop(): return create_instance('com.sun.star.frame.Desktop', True) +# ~ Export ok def get_dispatch(): return create_instance('com.sun.star.frame.DispatchHelper') +# ~ Export ok +def call_dispatch(url, args=()): + frame = get_document().frame + dispatch = get_dispatch() + dispatch.executeDispatch(frame, url, '', 0, args) + return + + +# ~ Export ok def get_temp_file(): delete = True if IS_WIN: @@ -349,6 +407,7 @@ def _path_system(path): return path +# ~ Export ok def exists_app(name): try: dn = subprocess.DEVNULL @@ -358,33 +417,20 @@ def exists_app(name): return False return True -# ~ Delete -# ~ def exists(path): - # ~ return Path(path).exists() + +# ~ Export ok def exists_path(path): return Path(path).exists() +# ~ Export ok def get_type_doc(obj): - # ~ services = { - # ~ 'calc': 'com.sun.star.sheet.SpreadsheetDocument', - # ~ 'writer': 'com.sun.star.text.TextDocument', - # ~ 'impress': 'com.sun.star.presentation.PresentationDocument', - # ~ 'draw': 'com.sun.star.drawing.DrawingDocument', - # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', - # ~ 'math': 'com.sun.star.formula.FormulaProperties', - # ~ 'basic': 'com.sun.star.script.BasicIDE', - # ~ } for k, v in TYPE_DOC.items(): if obj.supportsService(v): return k return '' -# ~ def _properties(values): - # ~ p = [PropertyValue(Name=n, Value=v) for n, v in values.items()] - # ~ return tuple(p) - def dict_to_property(values, uno_any=False): ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: @@ -397,82 +443,12 @@ def property_to_dict(values): return d -# ~ Third classes - - -# ~ https://github.com/psf/requests/blob/v2.22.0/requests/structures.py -class CaseInsensitiveDict(MutableMapping): - """A case-insensitive ``dict``-like object. - Implements all methods and operations of - ``MutableMapping`` as well as dict's ``copy``. Also - provides ``lower_items``. - All keys are expected to be strings. The structure remembers the - case of the last key to be set, and ``iter(instance)``, - ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` - will contain case-sensitive keys. However, querying and contains - testing is case insensitive:: - cid = CaseInsensitiveDict() - cid['Accept'] = 'application/json' - cid['aCCEPT'] == 'application/json' # True - list(cid) == ['Accept'] # True - For example, ``headers['content-encoding']`` will return the - value of a ``'Content-Encoding'`` response header, regardless - of how the header name was originally stored. - If the constructor, ``.update``, or equality comparison - operations are given keys that have equal ``.lower()``s, the - behavior is undefined. - """ - - def __init__(self, data=None, **kwargs): - self._store = OrderedDict() - if data is None: - data = {} - self.update(data, **kwargs) - - def __setitem__(self, key, value): - # Use the lowercased key for lookups, but store the actual - # key alongside the value. - self._store[key.lower()] = (key, value) - - def __getitem__(self, key): - return self._store[key.lower()][1] - - def __delitem__(self, key): - del self._store[key.lower()] - - def __iter__(self): - return (casedkey for casedkey, mappedvalue in self._store.values()) - - def __len__(self): - return len(self._store) - - def lower_items(self): - """Like iteritems(), but with all lowercase keys.""" - return ( - (lowerkey, keyval[1]) - for (lowerkey, keyval) - in self._store.items() - ) - - def __eq__(self, other): - if isinstance(other, Mapping): - other = CaseInsensitiveDict(other) - else: - return NotImplemented - # Compare insensitively - return dict(self.lower_items()) == dict(other.lower_items()) - - # Copy is required - def copy(self): - return CaseInsensitiveDict(self._store.values()) - - def __repr__(self): - return str(dict(self.items())) +def array_to_dict(values): + d = {r[0]: r[1] for r in values} + return d # ~ Custom classes - - class LODocument(object): def __init__(self, obj): @@ -481,7 +457,10 @@ class LODocument(object): def _init_values(self): self._type_doc = get_type_doc(self.obj) - self._cc = self.obj.getCurrentController() + if self._type_doc == 'base': + self._cc = self.obj.DatabaseDocument.getCurrentController() + else: + self._cc = self.obj.getCurrentController() return @property @@ -516,6 +495,10 @@ class LODocument(object): def path(self): return _path_system(self.obj.getURL()) + @property + def statusbar(self): + return self._cc.getStatusIndicator() + @property def visible(self): w = self._cc.getFrame().getContainerWindow() @@ -560,6 +543,30 @@ class LODocument(object): self._cc.insertTransferable(transferable) return self.obj.getCurrentSelection() + def to_pdf(self, path, **kwargs): + path_pdf = path + if path: + if is_dir(path): + _, _, n, _ = get_info_path(self.path) + path_pdf = join(path, '{}.{}'.format(n, EXT['pdf'])) + else: + path_pdf = replace_ext(self.path, EXT['pdf']) + + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(kwargs, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + args = dict_to_property(args) + try: + self.obj.storeToURL(_path_url(path_pdf), args) + except Exception as e: + error(e) + path_pdf = '' + + return path_pdf + class LOCalc(LODocument): @@ -710,7 +717,6 @@ class LODrawImpress(LODocument): def draw_page(self): return self._cc.getCurrentPage() - @catch_exception def insert_image(self, path, **kwargs): w = kwargs.get('width', 3000) h = kwargs.get('Height', 1000) @@ -1121,6 +1127,10 @@ class UnoListBox(UnoBaseObject): super().__init__(obj) self._data = [] + @property + def type(self): + return 'listbox' + @property def value(self): return self.obj.SelectedItem @@ -1381,7 +1391,6 @@ class LODialog(object): return _path_url(path) return '' - @catch_exception def add_control(self, properties): tipo = properties.pop('Type').lower() @@ -1419,14 +1428,35 @@ def _get_class_doc(obj): return classes[type_doc](obj) -def get_document(): +# ~ Export ok +def get_document(title=''): doc = None desktop = get_desktop() - try: + if not title: doc = _get_class_doc(desktop.getCurrentComponent()) - except Exception as e: - log.error(e) - return doc + return doc + + for d in desktop.getComponents(): + if d.Title == title: + doc = d + break + + if doc is None: + return + + return _get_class_doc(doc) + + +# ~ Export ok +def get_documents(custom=True): + docs = [] + desktop = get_desktop() + for doc in desktop.getComponents(): + if custom: + docs.append(_get_class_doc(doc)) + else: + docs.append(doc) + return docs def get_selection(): @@ -1463,6 +1493,7 @@ def set_properties(model, properties): return +# ~ Export ok def get_config_path(name='Work'): """ Return de path name in config @@ -1472,6 +1503,7 @@ def get_config_path(name='Work'): return _path_system(getattr(path, name)) +# ~ Export ok def get_file(init_dir='', multiple=False, filters=()): """ init_folder: folder default open @@ -1490,32 +1522,109 @@ def get_file(init_dir='', multiple=False, filters=()): file_picker.setDisplayDirectory(init_dir) file_picker.setMultiSelectionMode(multiple) + path = '' if filters: file_picker.setCurrentFilter(filters[0][0]) for f in filters: file_picker.appendFilter(f[0], f[1]) if file_picker.execute(): + path = _path_system(file_picker.getSelectedFiles()[0]) if multiple: - return [_path_system(f) for f in file_picker.getSelectedFiles()] - return _path_system(file_picker.getSelectedFiles()[0]) + path = [_path_system(f) for f in file_picker.getSelectedFiles()] - return '' + return path +# ~ Export ok +def get_path(init_dir='', filters=()): + """ + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: Example + ( + ('XML', '*.xml'), + ('TXT', '*.txt'), + ) + """ + if not init_dir: + init_dir = get_config_path() + init_dir = _path_url(init_dir) + file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.initialize((2,)) + if filters: + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + path = _path_system(file_picker.getSelectedFiles()[0]) + return path + + +# ~ Export ok +def get_dir(init_dir=''): + folder_picker = create_instance('com.sun.star.ui.dialogs.FolderPicker') + if not init_dir: + init_dir = get_config_path() + init_dir = _path_url(init_dir) + folder_picker.setDisplayDirectory(init_dir) + + path = '' + if folder_picker.execute(): + path = _path_system(folder_picker.getDirectory()) + return path + + +# ~ Export ok def get_info_path(path): path, filename = os.path.split(path) name, extension = os.path.splitext(filename) return (path, filename, name, extension) +# ~ Export ok +def read_file(path, mode='r', array=False): + data = '' + with open(path, mode) as f: + if array: + data = tuple(f.read().splitlines()) + else: + data = f.read() + return data + + +# ~ Export ok +def save_file(path, mode='w', data=None): + with open(path, mode) as f: + f.write(data) + return + + +# ~ Export ok +def to_json(path, data): + with open(path, 'w') as f: + f.write(json.dumps(data, indent=4, sort_keys=True)) + return + + +# ~ Export ok +def from_json(path): + with open(path) as f: + data = json.loads(f.read()) + return data + + def get_path_extension(id): pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') path = _path_system(pip.getPackageLocation(id)) return path -def inputbox(message, default='', title=TITLE): +# ~ Export ok +def inputbox(message, default='', title=TITLE, echochar=''): class ControllersInput(object): @@ -1554,6 +1663,8 @@ def inputbox(message, default='', title=TITLE): 'Width': 190, 'Height': 15, } + if echochar: + args['EchoChar'] = ord(echochar[0]) dlg.add_control(args) dlg.txt_value.move(dlg.lbl_msg) @@ -1586,12 +1697,24 @@ def inputbox(message, default='', title=TITLE): return '' -def new_doc(type_doc=CALC): +# ~ Export ok +def new_doc(type_doc=CALC, **kwargs): path = 'private:factory/s{}'.format(type_doc) - doc = get_desktop().loadComponentFromURL(path, '_default', 0, ()) + opt = dict_to_property(kwargs) + doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt) return _get_class_doc(doc) +# ~ Export ok +def new_db(path): + dbc = create_instance('com.sun.star.sdb.DatabaseContext') + db = dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' # hsqldb + db.DatabaseDocument.storeAsURL(_path_url(path), ()) + return _get_class_doc(db) + + +# ~ Export ok def open_doc(path, **kwargs): """ Open document in path Usually options: @@ -1606,7 +1729,6 @@ def open_doc(path, **kwargs): http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html """ path = _path_url(path) - # ~ opt = _properties(kwargs) opt = dict_to_property(kwargs) doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) if doc is None: @@ -1615,6 +1737,7 @@ def open_doc(path, **kwargs): return _get_class_doc(doc) +# ~ Export ok def open_file(path): if IS_WIN: os.startfile(path) @@ -1623,37 +1746,45 @@ def open_file(path): return +# ~ Export ok def join(*paths): return os.path.join(*paths) +# ~ Export ok def is_dir(path): return Path(path).is_dir() +# ~ Export ok def is_file(path): return Path(path).is_file() +# ~ Export ok def get_file_size(path): return Path(path).stat().st_size +# ~ Export ok def is_created(path): return is_file(path) and bool(get_file_size(path)) +# ~ Export ok def replace_ext(path, extension): path, _, name, _ = get_info_path(path) return '{}/{}.{}'.format(path, name, extension) -def zip_names(path): +# ~ Export ok +def zip_content(path): with zipfile.ZipFile(path) as z: names = z.namelist() return names +# ~ Export ok def run(command, wait=False): # ~ debug(command) # ~ debug(shlex.split(command)) @@ -1708,6 +1839,7 @@ def _zippwd(source, target, pwd): return is_created(target) +# ~ Export ok def zip(source, target='', mode='w', pwd=''): if pwd: return _zippwd(source, target, pwd) @@ -1749,6 +1881,7 @@ def zip(source, target='', mode='w', pwd=''): return is_created(target) +# ~ Export ok def unzip(source, path='', members=None, pwd=None): if not path: path, _, _, _ = get_info_path(source) @@ -1761,6 +1894,7 @@ def unzip(source, path='', members=None, pwd=None): return True +# ~ Export ok def merge_zip(target, zips): try: with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: @@ -1775,18 +1909,20 @@ def merge_zip(target, zips): return True +# ~ Export ok def kill(path): p = Path(path) - if p.is_file(): - try: + try: + if p.is_file(): p.unlink() - except: - pass - elif p.is_dir(): - p.rmdir() + elif p.is_dir(): + shutil.rmtree(path) + except OSError as e: + log.error(e) return +# ~ Export ok def get_size_screen(): if IS_WIN: user32 = ctypes.windll.user32 @@ -1797,6 +1933,7 @@ def get_size_screen(): return res.strip() +# ~ Export ok def get_clipboard(): df = None text = '' @@ -1842,6 +1979,7 @@ class TextTransferable(unohelper.Base, XTransferable): return False +# ~ Export ok def set_clipboard(value): ts = TextTransferable(value) sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') @@ -1849,40 +1987,38 @@ def set_clipboard(value): return -def copy(doc=None): - if doc is None: - doc = get_document() - if hasattr(doc, 'frame'): - frame = doc.frame - else: - frame = doc.getCurrentController().getFrame() - dispatch = get_dispatch() - dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) +# ~ Todo +def copy(): + call_dispatch('.uno:Copy') return +# ~ Export ok def get_epoch(): - now = datetime.datetime.now() - return int(time.mktime(now.timetuple())) + n = now() + return int(time.mktime(n.timetuple())) +# ~ Export ok def file_copy(source, target='', name=''): p, f, n, e = get_info_path(source) if target: p = target if name: + e = '' n = name path_new = join(p, '{}{}'.format(n, e)) shutil.copy(source, path_new) - return + return path_new -def get_files(path, ext='*'): - docs = [] +# ~ Export ok +def get_path_content(path, filters='*'): + paths = [] for folder, _, files in os.walk(path): - pattern = re.compile(r'\.{}'.format(ext), re.IGNORECASE) - docs += [join(folder, f) for f in files if pattern.search(f)] - return docs + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [join(folder, f) for f in files if pattern.search(f)] + return paths def _get_menu(type_doc, name_menu): @@ -2022,6 +2158,460 @@ def get_app_menus(name_app, index=-1): return menus +# ~ Export ok +def start(): + global _start + _start = now() + log.info(_start) + return + + +# ~ Export ok +def end(): + global _start + e = now() + return str(e - _start).split('.')[0] + + +# ~ Export ok +# ~ https://en.wikipedia.org/wiki/Web_colors +def get_color(value): + COLORS = { + 'aliceblue': 15792383, + 'antiquewhite': 16444375, + 'aqua': 65535, + 'aquamarine': 8388564, + 'azure': 15794175, + 'beige': 16119260, + 'bisque': 16770244, + 'black': 0, + 'blanchedalmond': 16772045, + 'blue': 255, + 'blueviolet': 9055202, + 'brown': 10824234, + 'burlywood': 14596231, + 'cadetblue': 6266528, + 'chartreuse': 8388352, + 'chocolate': 13789470, + 'coral': 16744272, + 'cornflowerblue': 6591981, + 'cornsilk': 16775388, + 'crimson': 14423100, + 'cyan': 65535, + 'darkblue': 139, + 'darkcyan': 35723, + 'darkgoldenrod': 12092939, + 'darkgray': 11119017, + 'darkgreen': 25600, + 'darkgrey': 11119017, + 'darkkhaki': 12433259, + 'darkmagenta': 9109643, + 'darkolivegreen': 5597999, + 'darkorange': 16747520, + 'darkorchid': 10040012, + 'darkred': 9109504, + 'darksalmon': 15308410, + 'darkseagreen': 9419919, + 'darkslateblue': 4734347, + 'darkslategray': 3100495, + 'darkslategrey': 3100495, + 'darkturquoise': 52945, + 'darkviolet': 9699539, + 'deeppink': 16716947, + 'deepskyblue': 49151, + 'dimgray': 6908265, + 'dimgrey': 6908265, + 'dodgerblue': 2003199, + 'firebrick': 11674146, + 'floralwhite': 16775920, + 'forestgreen': 2263842, + 'fuchsia': 16711935, + 'gainsboro': 14474460, + 'ghostwhite': 16316671, + 'gold': 16766720, + 'goldenrod': 14329120, + 'gray': 8421504, + 'grey': 8421504, + 'green': 32768, + 'greenyellow': 11403055, + 'honeydew': 15794160, + 'hotpink': 16738740, + 'indianred': 13458524, + 'indigo': 4915330, + 'ivory': 16777200, + 'khaki': 15787660, + 'lavender': 15132410, + 'lavenderblush': 16773365, + 'lawngreen': 8190976, + 'lemonchiffon': 16775885, + 'lightblue': 11393254, + 'lightcoral': 15761536, + 'lightcyan': 14745599, + 'lightgoldenrodyellow': 16448210, + 'lightgray': 13882323, + 'lightgreen': 9498256, + 'lightgrey': 13882323, + 'lightpink': 16758465, + 'lightsalmon': 16752762, + 'lightseagreen': 2142890, + 'lightskyblue': 8900346, + 'lightslategray': 7833753, + 'lightslategrey': 7833753, + 'lightsteelblue': 11584734, + 'lightyellow': 16777184, + 'lime': 65280, + 'limegreen': 3329330, + 'linen': 16445670, + 'magenta': 16711935, + 'maroon': 8388608, + 'mediumaquamarine': 6737322, + 'mediumblue': 205, + 'mediumorchid': 12211667, + 'mediumpurple': 9662683, + 'mediumseagreen': 3978097, + 'mediumslateblue': 8087790, + 'mediumspringgreen': 64154, + 'mediumturquoise': 4772300, + 'mediumvioletred': 13047173, + 'midnightblue': 1644912, + 'mintcream': 16121850, + 'mistyrose': 16770273, + 'moccasin': 16770229, + 'navajowhite': 16768685, + 'navy': 128, + 'oldlace': 16643558, + 'olive': 8421376, + 'olivedrab': 7048739, + 'orange': 16753920, + 'orangered': 16729344, + 'orchid': 14315734, + 'palegoldenrod': 15657130, + 'palegreen': 10025880, + 'paleturquoise': 11529966, + 'palevioletred': 14381203, + 'papayawhip': 16773077, + 'peachpuff': 16767673, + 'peru': 13468991, + 'pink': 16761035, + 'plum': 14524637, + 'powderblue': 11591910, + 'purple': 8388736, + 'red': 16711680, + 'rosybrown': 12357519, + 'royalblue': 4286945, + 'saddlebrown': 9127187, + 'salmon': 16416882, + 'sandybrown': 16032864, + 'seagreen': 3050327, + 'seashell': 16774638, + 'sienna': 10506797, + 'silver': 12632256, + 'skyblue': 8900331, + 'slateblue': 6970061, + 'slategray': 7372944, + 'slategrey': 7372944, + 'snow': 16775930, + 'springgreen': 65407, + 'steelblue': 4620980, + 'tan': 13808780, + 'teal': 32896, + 'thistle': 14204888, + 'tomato': 16737095, + 'turquoise': 4251856, + 'violet': 15631086, + 'wheat': 16113331, + 'white': 16777215, + 'whitesmoke': 16119285, + 'yellow': 16776960, + 'yellowgreen': 10145074, + } + + if isinstance(value, tuple): + return (value[0] << 16) + (value[1] << 8) + value[2] + + if isinstance(value, str) and value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + return (r << 16) + (g << 8) + b + + return COLORS.get(value.lower(), -1) + + +# ~ Export ok +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +def _to_date(value): + new_value = value + if isinstance(value, Time): + new_value = datetime.time(value.Hours, value.Minutes, value.Seconds) + elif isinstance(value, Date): + new_value = datetime.date(value.Year, value.Month, value.Day) + elif isinstance(value, DateTime): + new_value = datetime.datetime( + value.Year, value.Month, value.Day, + value.Hours, value.Minutes, value.Seconds) + return new_value + + +# ~ Export ok +def format(template, data): + """ + https://pyformat.info/ + """ + if isinstance(data, (str, int, float)): + # ~ print(template.format(data)) + return template.format(data) + + if isinstance(data, (Time, Date, DateTime)): + return template.format(_to_date(data)) + + if isinstance(data, tuple) and isinstance(data[0], tuple): + data = {r[0]: _to_date(r[1]) for r in data} + return template.format(**data) + + data = [_to_date(v) for v in data] + result = template.format(*data) + return result + + +def _call_macro(macro): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + name = 'com.sun.star.script.provider.MasterScriptProviderFactory' + factory = create_instance(name, False) + + data = macro.copy() + if macro['language'] == 'Python': + data['module'] = '.py$' + elif macro['language'] == 'Basic': + data['module'] = '.{}.'.format(macro['module']) + if macro['location'] == 'user': + data['location'] = 'application' + else: + data['module'] = '.' + + args = macro.get('args', ()) + url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}' + path = url.format(**data) + script = factory.createScriptProvider('').getScript(path) + return script.invoke(args, None, None)[0] + + +# ~ Export ok +def call_macro(macro): + in_thread = macro.pop('thread') + if in_thread: + t = threading.Thread(target=_call_macro, args=(macro,)) + t.start() + return + + return _call_macro(macro) + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +# ~ Export ok +def timer(name, seconds, macro): + global _stop_thread + _stop_thread[name] = threading.Event() + thread = TimerThread(_stop_thread[name], seconds, macro) + thread.start() + return + + +# ~ Export ok +def stop_timer(name): + global _stop_thread + _stop_thread[name].set() + del _stop_thread[name] + return + + +def _get_key(password): + digest = hashlib.sha256(password.encode()).digest() + key = base64.urlsafe_b64encode(digest) + return key + + +# ~ Export ok +def encrypt(data, password): + f = Fernet(_get_key(password)) + token = f.encrypt(data).decode() + return token + + +# ~ Export ok +def decrypt(token, password): + data = '' + f = Fernet(_get_key(password)) + try: + data = f.decrypt(token.encode()).decode() + except InvalidToken as e: + error('Invalid Token') + return data + + +class SmtpServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + self._sender = '' + self._is_connect = self._login(config) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self, config): + name = config['server'] + port = config['port'] + is_ssl = config['ssl'] + self._sender = config['user'] + hosts = ('gmail' in name or 'outlook' in name) + try: + if is_ssl and hosts: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + self._server.ehlo() + self._server.starttls() + self._server.ehlo() + elif is_ssl: + self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) + self._server.ehlo() + else: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + + self._server.login(self._sender, config['pass']) + msg = 'Connect to: {}'.format(name) + debug(msg) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = _('Incorrect user or password') + return False + if '534' in str(e) and 'gmail' in name: + self._error = _('Allow less secure apps in GMail') + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def _body(self, msg): + body = msg.replace('\\n', '
') + return body + + def send(self, message): + file_name = 'attachment; filename={}' + email = MIMEMultipart() + email['From'] = self._sender + email['To'] = message['to'] + email['Cc'] = message.get('cc', '') + email['Subject'] = message['subject'] + email['Date'] = formatdate(localtime=True) + if message.get('confirm', False): + email['Disposition-Notification-To'] = email['From'] + email.attach(MIMEText(self._body(message['body']), 'html')) + + for path in message.get('files', ()): + _, fn, _, _ = get_info_path(path) + part = MIMEBase('application', 'octet-stream') + part.set_payload(read_file(path, 'rb')) + encoders.encode_base64(part) + part.add_header('Content-Disposition', file_name.format(fn)) + email.attach(part) + + receivers = ( + email['To'].split(',') + + email['CC'].split(',') + + message.get('bcc', '').split(',')) + try: + self._server.sendmail(self._sender, receivers, email.as_string()) + msg = 'Email sent...' + debug(msg) + if message.get('path', ''): + self.save_message(email, message['path']) + return True + except Exception as e: + self._error = str(e) + return False + return False + + def save_message(self, email, path): + mbox = mailbox.mbox(path, create=True) + mbox.lock() + try: + msg = mailbox.mboxMessage(email) + mbox.add(msg) + mbox.flush() + finally: + mbox.unlock() + return + + def close(self): + try: + self._server.quit() + msg = 'Close connection...' + debug(msg) + except: + pass + return + + +def _send_email(server, messages): + with SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + error(server.error) + return server.error + + +def send_email(server, message): + messages = message + if isinstance(message, dict): + messages = (message,) + t = threading.Thread(target=_send_email, args=(server, messages)) + t.start() + return + + +def server_smtp_test(config): + with SmtpServer(config) as server: + if server.error: + error(server.error) + return server.error + + # ~ name = 'com.sun.star.configuration.ConfigurationProvider' # ~ cp = create_instance(name, True) # ~ node = PropertyValue(Name='nodepath', Value=NODE_SETTING) diff --git a/files/ZAZFavorites_v0.1.0.oxt b/files/ZAZFavorites_v0.1.0.oxt index e6a41af..09db96c 100644 Binary files a/files/ZAZFavorites_v0.1.0.oxt and b/files/ZAZFavorites_v0.1.0.oxt differ diff --git a/files/ZAZFavorites_v0.2.0.oxt b/files/ZAZFavorites_v0.2.0.oxt new file mode 100644 index 0000000..acaa466 Binary files /dev/null and b/files/ZAZFavorites_v0.2.0.oxt differ diff --git a/source/ZAZFavorites.py b/source/ZAZFavorites.py index 6d3d67a..b70d1c2 100644 --- a/source/ZAZFavorites.py +++ b/source/ZAZFavorites.py @@ -43,7 +43,6 @@ class Controllers(object): self.d.grid.sort(0) return - @app.catch_exception def button_save_action(self, event): msg = _('¿You want save your favorites?') if not app.question(msg, self.TITLE): @@ -167,7 +166,7 @@ class ZAZFavorites(unohelper.Base, XJobExecutor): } dlg.add_control(args) dlg.grid.set_column_image(1, app.join(self.IMAGES, 'delete.png')) - paths = app.get_config('paths') + paths = app.get_config('paths', []) for path in paths: p, filename, n, e = app.get_info_path(path) dlg.grid.add_row((filename, '', path)) diff --git a/source/description.xml b/source/description.xml index 06681f5..30abecd 100644 --- a/source/description.xml +++ b/source/description.xml @@ -1,7 +1,7 @@ - + Favorites files Archivos favoritos diff --git a/source/pythonpath/easymacro.py b/source/pythonpath/easymacro.py index dd65e9e..3bf53b9 100644 --- a/source/pythonpath/easymacro.py +++ b/source/pythonpath/easymacro.py @@ -17,11 +17,12 @@ # ~ You should have received a copy of the GNU General Public License # ~ along with ZAZ. If not, see . - +import base64 import ctypes import datetime import errno import getpass +import hashlib import json import logging import os @@ -34,19 +35,30 @@ import sys import tempfile import threading import time +import traceback import zipfile from collections import OrderedDict from collections.abc import MutableMapping -from datetime import datetime from functools import wraps from operator import itemgetter from pathlib import Path, PurePath from pprint import pprint +from string import Template from subprocess import PIPE +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import formatdate +from email import encoders +import mailbox + import uno import unohelper +from com.sun.star.util import Time, Date, DateTime from com.sun.star.beans import PropertyValue from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES @@ -61,12 +73,20 @@ from com.sun.star.lang import XEventListener from com.sun.star.awt import XActionListener from com.sun.star.awt import XMouseListener +try: + from fernet import Fernet, InvalidToken + CRYPTO = True +except ImportError: + CRYPTO = False + MSG_LANG = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', 'Select file': 'Seleccionar archivo', + 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', + 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', } } @@ -95,7 +115,8 @@ TYPE_DOC = { 'writer': 'com.sun.star.text.TextDocument', 'impress': 'com.sun.star.presentation.PresentationDocument', 'draw': 'com.sun.star.drawing.DrawingDocument', - 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'base': 'com.sun.star.sdb.DocumentDataSource', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', } @@ -134,7 +155,12 @@ MENUS_APP = { } -FILE_NAME_DEBUG = 'zaz-debug.log' +EXT = { + 'pdf': 'pdf', +} + + +FILE_NAME_DEBUG = 'debug.odt' FILE_NAME_CONFIG = 'zaz-{}.json' LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_DATE = '%d/%m/%Y %H:%M:%S' @@ -145,10 +171,16 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) +_start = 0 +_stop_thread = {} +TIMEOUT = 10 + + CTX = uno.getComponentContext() SM = CTX.getServiceManager() +# ~ Export ok def create_instance(name, with_context=False): if with_context: instance = SM.createInstanceWithContext(name, CTX) @@ -178,6 +210,7 @@ NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') +# ~ Export ok def mri(obj): m = create_instance('mytools.Mri') if m is None: @@ -195,7 +228,10 @@ def catch_exception(f): try: return f(*args, **kwargs) except Exception as e: - log.error(f.__name__, exc_info=True) + name = f.__name__ + if IS_WIN: + debug(traceback.format_exc()) + log.error(name, exc_info=True) return func @@ -203,30 +239,30 @@ class LogWin(object): def __init__(self, doc): self.doc = doc + self.doc.Title = FILE_NAME_DEBUG def write(self, info): text = self.doc.Text cursor = text.createTextCursor() cursor.gotoEnd(False) - text.insertString(cursor, str(info), 0) + text.insertString(cursor, str(info) + '\n\n', 0) return +# ~ Export ok def info(data): log.info(data) return +# ~ Export ok def debug(info): if IS_WIN: - # ~ app = LOApp(self.ctx, self.sm, self.desktop, self.toolkit) - # ~ doc = app.getDoc(FILE_NAME_DEBUG) - # ~ if not doc: - # ~ doc = app.newDoc(WRITER) - # ~ out = OutputDoc(doc) - # ~ sys.stdout = out - # ~ pprint(info) - doc = LogWin(new_doc('writer').obj) + doc = get_document(FILE_NAME_DEBUG) + if doc is None: + # ~ doc = new_doc('writer') + return + doc = LogWin(doc.obj) doc.write(info) return @@ -234,14 +270,16 @@ def debug(info): return +# ~ Export ok def error(info): log.error(info) return +# ~ Export ok def save_log(path, data): with open(path, 'a') as out: - out.write('{} -{}- '.format(str(datetime.now())[:19], LOG_NAME)) + out.write('{} -{}- '.format(str(now())[:19], LOG_NAME)) pprint(data, stream=out) return @@ -254,34 +292,40 @@ def run_in_thread(fn): return run -def get_config(key='', prefix='config'): +def now(): + return datetime.datetime.now() + + +# ~ Export ok +def get_config(key='', default=None, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) - values = {} + values = None path = join(get_config_path('UserConfig'), path_json) if not exists_path(path): - return values + return default with open(path, 'r', encoding='utf-8') as fh: data = fh.read() - if data: - values = json.loads(data) + values = json.loads(data) if key: - return values.get(key, None) + return values.get(key, default) return values +# ~ Export ok def set_config(key, value, prefix='config'): path_json = FILE_NAME_CONFIG.format(prefix) path = join(get_config_path('UserConfig'), path_json) - values = get_config(prefix=prefix) + values = get_config(default={}, prefix=prefix) values[key] = value with open(path, 'w', encoding='utf-8') as fh: json.dump(values, fh, ensure_ascii=False, sort_keys=True, indent=4) - return True + return +# ~ Export ok def sleep(seconds): time.sleep(seconds) return @@ -298,6 +342,7 @@ def _(msg): return MSG_LANG[L][msg] +# ~ Export ok def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): """ Create message box type_msg: infobox, warningbox, errorbox, querybox, messbox @@ -309,15 +354,18 @@ def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infob return mb.execute() +# ~ Export ok def question(message, title=TITLE): res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return res == YES +# ~ Export ok def warning(message, title=TITLE): return msgbox(message, title, type_msg='warningbox') +# ~ Export ok def errorbox(message, title=TITLE): return msgbox(message, title, type_msg='errorbox') @@ -326,10 +374,20 @@ def get_desktop(): return create_instance('com.sun.star.frame.Desktop', True) +# ~ Export ok def get_dispatch(): return create_instance('com.sun.star.frame.DispatchHelper') +# ~ Export ok +def call_dispatch(url, args=()): + frame = get_document().frame + dispatch = get_dispatch() + dispatch.executeDispatch(frame, url, '', 0, args) + return + + +# ~ Export ok def get_temp_file(): delete = True if IS_WIN: @@ -349,6 +407,7 @@ def _path_system(path): return path +# ~ Export ok def exists_app(name): try: dn = subprocess.DEVNULL @@ -358,33 +417,20 @@ def exists_app(name): return False return True -# ~ Delete -# ~ def exists(path): - # ~ return Path(path).exists() + +# ~ Export ok def exists_path(path): return Path(path).exists() +# ~ Export ok def get_type_doc(obj): - # ~ services = { - # ~ 'calc': 'com.sun.star.sheet.SpreadsheetDocument', - # ~ 'writer': 'com.sun.star.text.TextDocument', - # ~ 'impress': 'com.sun.star.presentation.PresentationDocument', - # ~ 'draw': 'com.sun.star.drawing.DrawingDocument', - # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', - # ~ 'math': 'com.sun.star.formula.FormulaProperties', - # ~ 'basic': 'com.sun.star.script.BasicIDE', - # ~ } for k, v in TYPE_DOC.items(): if obj.supportsService(v): return k return '' -# ~ def _properties(values): - # ~ p = [PropertyValue(Name=n, Value=v) for n, v in values.items()] - # ~ return tuple(p) - def dict_to_property(values, uno_any=False): ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: @@ -397,82 +443,12 @@ def property_to_dict(values): return d -# ~ Third classes - - -# ~ https://github.com/psf/requests/blob/v2.22.0/requests/structures.py -class CaseInsensitiveDict(MutableMapping): - """A case-insensitive ``dict``-like object. - Implements all methods and operations of - ``MutableMapping`` as well as dict's ``copy``. Also - provides ``lower_items``. - All keys are expected to be strings. The structure remembers the - case of the last key to be set, and ``iter(instance)``, - ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` - will contain case-sensitive keys. However, querying and contains - testing is case insensitive:: - cid = CaseInsensitiveDict() - cid['Accept'] = 'application/json' - cid['aCCEPT'] == 'application/json' # True - list(cid) == ['Accept'] # True - For example, ``headers['content-encoding']`` will return the - value of a ``'Content-Encoding'`` response header, regardless - of how the header name was originally stored. - If the constructor, ``.update``, or equality comparison - operations are given keys that have equal ``.lower()``s, the - behavior is undefined. - """ - - def __init__(self, data=None, **kwargs): - self._store = OrderedDict() - if data is None: - data = {} - self.update(data, **kwargs) - - def __setitem__(self, key, value): - # Use the lowercased key for lookups, but store the actual - # key alongside the value. - self._store[key.lower()] = (key, value) - - def __getitem__(self, key): - return self._store[key.lower()][1] - - def __delitem__(self, key): - del self._store[key.lower()] - - def __iter__(self): - return (casedkey for casedkey, mappedvalue in self._store.values()) - - def __len__(self): - return len(self._store) - - def lower_items(self): - """Like iteritems(), but with all lowercase keys.""" - return ( - (lowerkey, keyval[1]) - for (lowerkey, keyval) - in self._store.items() - ) - - def __eq__(self, other): - if isinstance(other, Mapping): - other = CaseInsensitiveDict(other) - else: - return NotImplemented - # Compare insensitively - return dict(self.lower_items()) == dict(other.lower_items()) - - # Copy is required - def copy(self): - return CaseInsensitiveDict(self._store.values()) - - def __repr__(self): - return str(dict(self.items())) +def array_to_dict(values): + d = {r[0]: r[1] for r in values} + return d # ~ Custom classes - - class LODocument(object): def __init__(self, obj): @@ -481,7 +457,10 @@ class LODocument(object): def _init_values(self): self._type_doc = get_type_doc(self.obj) - self._cc = self.obj.getCurrentController() + if self._type_doc == 'base': + self._cc = self.obj.DatabaseDocument.getCurrentController() + else: + self._cc = self.obj.getCurrentController() return @property @@ -516,6 +495,10 @@ class LODocument(object): def path(self): return _path_system(self.obj.getURL()) + @property + def statusbar(self): + return self._cc.getStatusIndicator() + @property def visible(self): w = self._cc.getFrame().getContainerWindow() @@ -560,6 +543,30 @@ class LODocument(object): self._cc.insertTransferable(transferable) return self.obj.getCurrentSelection() + def to_pdf(self, path, **kwargs): + path_pdf = path + if path: + if is_dir(path): + _, _, n, _ = get_info_path(self.path) + path_pdf = join(path, '{}.{}'.format(n, EXT['pdf'])) + else: + path_pdf = replace_ext(self.path, EXT['pdf']) + + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(kwargs, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + args = dict_to_property(args) + try: + self.obj.storeToURL(_path_url(path_pdf), args) + except Exception as e: + error(e) + path_pdf = '' + + return path_pdf + class LOCalc(LODocument): @@ -710,7 +717,6 @@ class LODrawImpress(LODocument): def draw_page(self): return self._cc.getCurrentPage() - @catch_exception def insert_image(self, path, **kwargs): w = kwargs.get('width', 3000) h = kwargs.get('Height', 1000) @@ -1121,6 +1127,10 @@ class UnoListBox(UnoBaseObject): super().__init__(obj) self._data = [] + @property + def type(self): + return 'listbox' + @property def value(self): return self.obj.SelectedItem @@ -1381,7 +1391,6 @@ class LODialog(object): return _path_url(path) return '' - @catch_exception def add_control(self, properties): tipo = properties.pop('Type').lower() @@ -1419,14 +1428,35 @@ def _get_class_doc(obj): return classes[type_doc](obj) -def get_document(): +# ~ Export ok +def get_document(title=''): doc = None desktop = get_desktop() - try: + if not title: doc = _get_class_doc(desktop.getCurrentComponent()) - except Exception as e: - log.error(e) - return doc + return doc + + for d in desktop.getComponents(): + if d.Title == title: + doc = d + break + + if doc is None: + return + + return _get_class_doc(doc) + + +# ~ Export ok +def get_documents(custom=True): + docs = [] + desktop = get_desktop() + for doc in desktop.getComponents(): + if custom: + docs.append(_get_class_doc(doc)) + else: + docs.append(doc) + return docs def get_selection(): @@ -1463,6 +1493,7 @@ def set_properties(model, properties): return +# ~ Export ok def get_config_path(name='Work'): """ Return de path name in config @@ -1472,6 +1503,7 @@ def get_config_path(name='Work'): return _path_system(getattr(path, name)) +# ~ Export ok def get_file(init_dir='', multiple=False, filters=()): """ init_folder: folder default open @@ -1490,32 +1522,109 @@ def get_file(init_dir='', multiple=False, filters=()): file_picker.setDisplayDirectory(init_dir) file_picker.setMultiSelectionMode(multiple) + path = '' if filters: file_picker.setCurrentFilter(filters[0][0]) for f in filters: file_picker.appendFilter(f[0], f[1]) if file_picker.execute(): + path = _path_system(file_picker.getSelectedFiles()[0]) if multiple: - return [_path_system(f) for f in file_picker.getSelectedFiles()] - return _path_system(file_picker.getSelectedFiles()[0]) + path = [_path_system(f) for f in file_picker.getSelectedFiles()] - return '' + return path +# ~ Export ok +def get_path(init_dir='', filters=()): + """ + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: Example + ( + ('XML', '*.xml'), + ('TXT', '*.txt'), + ) + """ + if not init_dir: + init_dir = get_config_path() + init_dir = _path_url(init_dir) + file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.initialize((2,)) + if filters: + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + path = _path_system(file_picker.getSelectedFiles()[0]) + return path + + +# ~ Export ok +def get_dir(init_dir=''): + folder_picker = create_instance('com.sun.star.ui.dialogs.FolderPicker') + if not init_dir: + init_dir = get_config_path() + init_dir = _path_url(init_dir) + folder_picker.setDisplayDirectory(init_dir) + + path = '' + if folder_picker.execute(): + path = _path_system(folder_picker.getDirectory()) + return path + + +# ~ Export ok def get_info_path(path): path, filename = os.path.split(path) name, extension = os.path.splitext(filename) return (path, filename, name, extension) +# ~ Export ok +def read_file(path, mode='r', array=False): + data = '' + with open(path, mode) as f: + if array: + data = tuple(f.read().splitlines()) + else: + data = f.read() + return data + + +# ~ Export ok +def save_file(path, mode='w', data=None): + with open(path, mode) as f: + f.write(data) + return + + +# ~ Export ok +def to_json(path, data): + with open(path, 'w') as f: + f.write(json.dumps(data, indent=4, sort_keys=True)) + return + + +# ~ Export ok +def from_json(path): + with open(path) as f: + data = json.loads(f.read()) + return data + + def get_path_extension(id): pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') path = _path_system(pip.getPackageLocation(id)) return path -def inputbox(message, default='', title=TITLE): +# ~ Export ok +def inputbox(message, default='', title=TITLE, echochar=''): class ControllersInput(object): @@ -1554,6 +1663,8 @@ def inputbox(message, default='', title=TITLE): 'Width': 190, 'Height': 15, } + if echochar: + args['EchoChar'] = ord(echochar[0]) dlg.add_control(args) dlg.txt_value.move(dlg.lbl_msg) @@ -1586,12 +1697,24 @@ def inputbox(message, default='', title=TITLE): return '' -def new_doc(type_doc=CALC): +# ~ Export ok +def new_doc(type_doc=CALC, **kwargs): path = 'private:factory/s{}'.format(type_doc) - doc = get_desktop().loadComponentFromURL(path, '_default', 0, ()) + opt = dict_to_property(kwargs) + doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt) return _get_class_doc(doc) +# ~ Export ok +def new_db(path): + dbc = create_instance('com.sun.star.sdb.DatabaseContext') + db = dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' # hsqldb + db.DatabaseDocument.storeAsURL(_path_url(path), ()) + return _get_class_doc(db) + + +# ~ Export ok def open_doc(path, **kwargs): """ Open document in path Usually options: @@ -1606,7 +1729,6 @@ def open_doc(path, **kwargs): http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html """ path = _path_url(path) - # ~ opt = _properties(kwargs) opt = dict_to_property(kwargs) doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) if doc is None: @@ -1615,6 +1737,7 @@ def open_doc(path, **kwargs): return _get_class_doc(doc) +# ~ Export ok def open_file(path): if IS_WIN: os.startfile(path) @@ -1623,37 +1746,45 @@ def open_file(path): return +# ~ Export ok def join(*paths): return os.path.join(*paths) +# ~ Export ok def is_dir(path): return Path(path).is_dir() +# ~ Export ok def is_file(path): return Path(path).is_file() +# ~ Export ok def get_file_size(path): return Path(path).stat().st_size +# ~ Export ok def is_created(path): return is_file(path) and bool(get_file_size(path)) +# ~ Export ok def replace_ext(path, extension): path, _, name, _ = get_info_path(path) return '{}/{}.{}'.format(path, name, extension) -def zip_names(path): +# ~ Export ok +def zip_content(path): with zipfile.ZipFile(path) as z: names = z.namelist() return names +# ~ Export ok def run(command, wait=False): # ~ debug(command) # ~ debug(shlex.split(command)) @@ -1708,6 +1839,7 @@ def _zippwd(source, target, pwd): return is_created(target) +# ~ Export ok def zip(source, target='', mode='w', pwd=''): if pwd: return _zippwd(source, target, pwd) @@ -1749,6 +1881,7 @@ def zip(source, target='', mode='w', pwd=''): return is_created(target) +# ~ Export ok def unzip(source, path='', members=None, pwd=None): if not path: path, _, _, _ = get_info_path(source) @@ -1761,6 +1894,7 @@ def unzip(source, path='', members=None, pwd=None): return True +# ~ Export ok def merge_zip(target, zips): try: with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: @@ -1775,18 +1909,20 @@ def merge_zip(target, zips): return True +# ~ Export ok def kill(path): p = Path(path) - if p.is_file(): - try: + try: + if p.is_file(): p.unlink() - except: - pass - elif p.is_dir(): - p.rmdir() + elif p.is_dir(): + shutil.rmtree(path) + except OSError as e: + log.error(e) return +# ~ Export ok def get_size_screen(): if IS_WIN: user32 = ctypes.windll.user32 @@ -1797,6 +1933,7 @@ def get_size_screen(): return res.strip() +# ~ Export ok def get_clipboard(): df = None text = '' @@ -1842,6 +1979,7 @@ class TextTransferable(unohelper.Base, XTransferable): return False +# ~ Export ok def set_clipboard(value): ts = TextTransferable(value) sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') @@ -1849,40 +1987,38 @@ def set_clipboard(value): return -def copy(doc=None): - if doc is None: - doc = get_document() - if hasattr(doc, 'frame'): - frame = doc.frame - else: - frame = doc.getCurrentController().getFrame() - dispatch = get_dispatch() - dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) +# ~ Todo +def copy(): + call_dispatch('.uno:Copy') return +# ~ Export ok def get_epoch(): - now = datetime.datetime.now() - return int(time.mktime(now.timetuple())) + n = now() + return int(time.mktime(n.timetuple())) +# ~ Export ok def file_copy(source, target='', name=''): p, f, n, e = get_info_path(source) if target: p = target if name: + e = '' n = name path_new = join(p, '{}{}'.format(n, e)) shutil.copy(source, path_new) - return + return path_new -def get_files(path, ext='*'): - docs = [] +# ~ Export ok +def get_path_content(path, filters='*'): + paths = [] for folder, _, files in os.walk(path): - pattern = re.compile(r'\.{}'.format(ext), re.IGNORECASE) - docs += [join(folder, f) for f in files if pattern.search(f)] - return docs + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [join(folder, f) for f in files if pattern.search(f)] + return paths def _get_menu(type_doc, name_menu): @@ -2022,6 +2158,460 @@ def get_app_menus(name_app, index=-1): return menus +# ~ Export ok +def start(): + global _start + _start = now() + log.info(_start) + return + + +# ~ Export ok +def end(): + global _start + e = now() + return str(e - _start).split('.')[0] + + +# ~ Export ok +# ~ https://en.wikipedia.org/wiki/Web_colors +def get_color(value): + COLORS = { + 'aliceblue': 15792383, + 'antiquewhite': 16444375, + 'aqua': 65535, + 'aquamarine': 8388564, + 'azure': 15794175, + 'beige': 16119260, + 'bisque': 16770244, + 'black': 0, + 'blanchedalmond': 16772045, + 'blue': 255, + 'blueviolet': 9055202, + 'brown': 10824234, + 'burlywood': 14596231, + 'cadetblue': 6266528, + 'chartreuse': 8388352, + 'chocolate': 13789470, + 'coral': 16744272, + 'cornflowerblue': 6591981, + 'cornsilk': 16775388, + 'crimson': 14423100, + 'cyan': 65535, + 'darkblue': 139, + 'darkcyan': 35723, + 'darkgoldenrod': 12092939, + 'darkgray': 11119017, + 'darkgreen': 25600, + 'darkgrey': 11119017, + 'darkkhaki': 12433259, + 'darkmagenta': 9109643, + 'darkolivegreen': 5597999, + 'darkorange': 16747520, + 'darkorchid': 10040012, + 'darkred': 9109504, + 'darksalmon': 15308410, + 'darkseagreen': 9419919, + 'darkslateblue': 4734347, + 'darkslategray': 3100495, + 'darkslategrey': 3100495, + 'darkturquoise': 52945, + 'darkviolet': 9699539, + 'deeppink': 16716947, + 'deepskyblue': 49151, + 'dimgray': 6908265, + 'dimgrey': 6908265, + 'dodgerblue': 2003199, + 'firebrick': 11674146, + 'floralwhite': 16775920, + 'forestgreen': 2263842, + 'fuchsia': 16711935, + 'gainsboro': 14474460, + 'ghostwhite': 16316671, + 'gold': 16766720, + 'goldenrod': 14329120, + 'gray': 8421504, + 'grey': 8421504, + 'green': 32768, + 'greenyellow': 11403055, + 'honeydew': 15794160, + 'hotpink': 16738740, + 'indianred': 13458524, + 'indigo': 4915330, + 'ivory': 16777200, + 'khaki': 15787660, + 'lavender': 15132410, + 'lavenderblush': 16773365, + 'lawngreen': 8190976, + 'lemonchiffon': 16775885, + 'lightblue': 11393254, + 'lightcoral': 15761536, + 'lightcyan': 14745599, + 'lightgoldenrodyellow': 16448210, + 'lightgray': 13882323, + 'lightgreen': 9498256, + 'lightgrey': 13882323, + 'lightpink': 16758465, + 'lightsalmon': 16752762, + 'lightseagreen': 2142890, + 'lightskyblue': 8900346, + 'lightslategray': 7833753, + 'lightslategrey': 7833753, + 'lightsteelblue': 11584734, + 'lightyellow': 16777184, + 'lime': 65280, + 'limegreen': 3329330, + 'linen': 16445670, + 'magenta': 16711935, + 'maroon': 8388608, + 'mediumaquamarine': 6737322, + 'mediumblue': 205, + 'mediumorchid': 12211667, + 'mediumpurple': 9662683, + 'mediumseagreen': 3978097, + 'mediumslateblue': 8087790, + 'mediumspringgreen': 64154, + 'mediumturquoise': 4772300, + 'mediumvioletred': 13047173, + 'midnightblue': 1644912, + 'mintcream': 16121850, + 'mistyrose': 16770273, + 'moccasin': 16770229, + 'navajowhite': 16768685, + 'navy': 128, + 'oldlace': 16643558, + 'olive': 8421376, + 'olivedrab': 7048739, + 'orange': 16753920, + 'orangered': 16729344, + 'orchid': 14315734, + 'palegoldenrod': 15657130, + 'palegreen': 10025880, + 'paleturquoise': 11529966, + 'palevioletred': 14381203, + 'papayawhip': 16773077, + 'peachpuff': 16767673, + 'peru': 13468991, + 'pink': 16761035, + 'plum': 14524637, + 'powderblue': 11591910, + 'purple': 8388736, + 'red': 16711680, + 'rosybrown': 12357519, + 'royalblue': 4286945, + 'saddlebrown': 9127187, + 'salmon': 16416882, + 'sandybrown': 16032864, + 'seagreen': 3050327, + 'seashell': 16774638, + 'sienna': 10506797, + 'silver': 12632256, + 'skyblue': 8900331, + 'slateblue': 6970061, + 'slategray': 7372944, + 'slategrey': 7372944, + 'snow': 16775930, + 'springgreen': 65407, + 'steelblue': 4620980, + 'tan': 13808780, + 'teal': 32896, + 'thistle': 14204888, + 'tomato': 16737095, + 'turquoise': 4251856, + 'violet': 15631086, + 'wheat': 16113331, + 'white': 16777215, + 'whitesmoke': 16119285, + 'yellow': 16776960, + 'yellowgreen': 10145074, + } + + if isinstance(value, tuple): + return (value[0] << 16) + (value[1] << 8) + value[2] + + if isinstance(value, str) and value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + return (r << 16) + (g << 8) + b + + return COLORS.get(value.lower(), -1) + + +# ~ Export ok +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +def _to_date(value): + new_value = value + if isinstance(value, Time): + new_value = datetime.time(value.Hours, value.Minutes, value.Seconds) + elif isinstance(value, Date): + new_value = datetime.date(value.Year, value.Month, value.Day) + elif isinstance(value, DateTime): + new_value = datetime.datetime( + value.Year, value.Month, value.Day, + value.Hours, value.Minutes, value.Seconds) + return new_value + + +# ~ Export ok +def format(template, data): + """ + https://pyformat.info/ + """ + if isinstance(data, (str, int, float)): + # ~ print(template.format(data)) + return template.format(data) + + if isinstance(data, (Time, Date, DateTime)): + return template.format(_to_date(data)) + + if isinstance(data, tuple) and isinstance(data[0], tuple): + data = {r[0]: _to_date(r[1]) for r in data} + return template.format(**data) + + data = [_to_date(v) for v in data] + result = template.format(*data) + return result + + +def _call_macro(macro): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + name = 'com.sun.star.script.provider.MasterScriptProviderFactory' + factory = create_instance(name, False) + + data = macro.copy() + if macro['language'] == 'Python': + data['module'] = '.py$' + elif macro['language'] == 'Basic': + data['module'] = '.{}.'.format(macro['module']) + if macro['location'] == 'user': + data['location'] = 'application' + else: + data['module'] = '.' + + args = macro.get('args', ()) + url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}' + path = url.format(**data) + script = factory.createScriptProvider('').getScript(path) + return script.invoke(args, None, None)[0] + + +# ~ Export ok +def call_macro(macro): + in_thread = macro.pop('thread') + if in_thread: + t = threading.Thread(target=_call_macro, args=(macro,)) + t.start() + return + + return _call_macro(macro) + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +# ~ Export ok +def timer(name, seconds, macro): + global _stop_thread + _stop_thread[name] = threading.Event() + thread = TimerThread(_stop_thread[name], seconds, macro) + thread.start() + return + + +# ~ Export ok +def stop_timer(name): + global _stop_thread + _stop_thread[name].set() + del _stop_thread[name] + return + + +def _get_key(password): + digest = hashlib.sha256(password.encode()).digest() + key = base64.urlsafe_b64encode(digest) + return key + + +# ~ Export ok +def encrypt(data, password): + f = Fernet(_get_key(password)) + token = f.encrypt(data).decode() + return token + + +# ~ Export ok +def decrypt(token, password): + data = '' + f = Fernet(_get_key(password)) + try: + data = f.decrypt(token.encode()).decode() + except InvalidToken as e: + error('Invalid Token') + return data + + +class SmtpServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + self._sender = '' + self._is_connect = self._login(config) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self, config): + name = config['server'] + port = config['port'] + is_ssl = config['ssl'] + self._sender = config['user'] + hosts = ('gmail' in name or 'outlook' in name) + try: + if is_ssl and hosts: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + self._server.ehlo() + self._server.starttls() + self._server.ehlo() + elif is_ssl: + self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) + self._server.ehlo() + else: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + + self._server.login(self._sender, config['pass']) + msg = 'Connect to: {}'.format(name) + debug(msg) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = _('Incorrect user or password') + return False + if '534' in str(e) and 'gmail' in name: + self._error = _('Allow less secure apps in GMail') + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def _body(self, msg): + body = msg.replace('\\n', '
') + return body + + def send(self, message): + file_name = 'attachment; filename={}' + email = MIMEMultipart() + email['From'] = self._sender + email['To'] = message['to'] + email['Cc'] = message.get('cc', '') + email['Subject'] = message['subject'] + email['Date'] = formatdate(localtime=True) + if message.get('confirm', False): + email['Disposition-Notification-To'] = email['From'] + email.attach(MIMEText(self._body(message['body']), 'html')) + + for path in message.get('files', ()): + _, fn, _, _ = get_info_path(path) + part = MIMEBase('application', 'octet-stream') + part.set_payload(read_file(path, 'rb')) + encoders.encode_base64(part) + part.add_header('Content-Disposition', file_name.format(fn)) + email.attach(part) + + receivers = ( + email['To'].split(',') + + email['CC'].split(',') + + message.get('bcc', '').split(',')) + try: + self._server.sendmail(self._sender, receivers, email.as_string()) + msg = 'Email sent...' + debug(msg) + if message.get('path', ''): + self.save_message(email, message['path']) + return True + except Exception as e: + self._error = str(e) + return False + return False + + def save_message(self, email, path): + mbox = mailbox.mbox(path, create=True) + mbox.lock() + try: + msg = mailbox.mboxMessage(email) + mbox.add(msg) + mbox.flush() + finally: + mbox.unlock() + return + + def close(self): + try: + self._server.quit() + msg = 'Close connection...' + debug(msg) + except: + pass + return + + +def _send_email(server, messages): + with SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + error(server.error) + return server.error + + +def send_email(server, message): + messages = message + if isinstance(message, dict): + messages = (message,) + t = threading.Thread(target=_send_email, args=(server, messages)) + t.start() + return + + +def server_smtp_test(config): + with SmtpServer(config) as server: + if server.error: + error(server.error) + return server.error + + # ~ name = 'com.sun.star.configuration.ConfigurationProvider' # ~ cp = create_instance(name, True) # ~ node = PropertyValue(Name='nodepath', Value=NODE_SETTING)