diff --git a/docs/source/_static/images/tools_23.png b/docs/source/_static/images/tools_23.png new file mode 100644 index 0000000..c528b21 Binary files /dev/null and b/docs/source/_static/images/tools_23.png differ diff --git a/docs/source/_static/images/tools_24.png b/docs/source/_static/images/tools_24.png new file mode 100644 index 0000000..b980518 Binary files /dev/null and b/docs/source/_static/images/tools_24.png differ diff --git a/docs/source/generated/easymacro.rst b/docs/source/generated/easymacro.rst index 2550bc0..0166ab0 100644 --- a/docs/source/generated/easymacro.rst +++ b/docs/source/generated/easymacro.rst @@ -25,6 +25,7 @@ mri msgbox question + render run_in_thread save_log set_app_config @@ -39,6 +40,8 @@ .. autosummary:: + Color + Config Dates Hash Json @@ -49,6 +52,7 @@ Paths Shell Timer + Url commands diff --git a/docs/source/paths.rst b/docs/source/paths.rst index d4e315b..24a9b20 100644 --- a/docs/source/paths.rst +++ b/docs/source/paths.rst @@ -55,7 +55,7 @@ Get home path app.debug(p.home) -Get documento path +Get document path ------------------ .. code-block:: python @@ -99,3 +99,75 @@ Get python executable path p = app.path app.debug(p.python) + +Path URL to system +------------------ + +.. code-block:: python + + path = 'file:///home/mau/myfile.ods' + app.debug(app.path.to_system(path)) + + +Path system to URL +------------------ + +.. code-block:: python + + path = 'file:///home/mau/myfile.ods' + path = app.path.to_system(path) + + app.debug(app.path.to_url(path)) + + +Get path from user config +------------------------- + +Default get path documents. `See Api XPathSettings `_ + +.. code-block:: python + + path = app.path.config() + app.debug(path) + + path = app.path.config('UserConfig') + app.debug(path) + +.. note:: + + Some paths can be more than one path separated by a semicolon, in this case, you get a `list` of paths. + + +Path join +--------- + +.. code-block:: python + + path = app.path.join('/home/mau', 'test', 'file.ods') + app.debug(path) + + +Exists path +----------- + +.. code-block:: python + + exists = app.path.exists('/home/mau/test/file.ods') + app.debug(exists) + + +Verify if application exists +---------------------------- + +.. code-block:: python + + app_name = 'nosoffice' + app.debug(app.path.exists_app(app_name)) + + app_name = 'soffice' + app.debug(app.path.exists_app(app_name)) + + + + + diff --git a/docs/source/tools.rst b/docs/source/tools.rst index 487067d..69eb486 100644 --- a/docs/source/tools.rst +++ b/docs/source/tools.rst @@ -660,17 +660,24 @@ You can save any data. 'save_data': True, } - app.set_config('config', data, my_app) + if app.config.set(my_app, data): + app.msgbox('Save config') - app.msgbox('Save config') - - data = app.get_config('config', my_app) + path = app.config.get(my_app) app.msgbox(data) +You can get any key + +.. code-block:: python + + path = app.config.get(my_app, 'path') + app.msgbox(path) + + Render string -^^^^^^^^^^^^^ +------------- .. code-block:: python @@ -688,72 +695,63 @@ Render string app.msgbox(render) -Encrypt decrypt -^^^^^^^^^^^^^^^ - -You need install library `cryptography`_ - -.. code-block:: python - - import easymacro as app - from conf import PASSWORD - - def encrypt_decrypt(): - - data = 'My super secret data' - token = app.encrypt(data, PASSWORD) - app.msgbox(token) - - data = app.decrypt(token, PASSWORD) - app.msgbox(data) - - return - - Simple url open -^^^^^^^^^^^^^^^ +--------------- -* Get text data +Get text data +^^^^^^^^^^^^^ .. code-block:: python url = 'https://api.ipify.org' - data = app.url_open(url) - app.msgbox(data) + result, headers, err = app.url.get(url) + if err: + app.error(err) + else: + app.debug(type(result), result) + app.debug(headers) -* Get json data +.. image:: _static/images/tools_23.png + +| + +Get json data +^^^^^^^^^^^^^ .. code-block:: python url = 'https://api.ipify.org?format=json' - data = app.url_open(url, get_json=True) - app.msgbox(data) + result, headers, err = app.url.get(url, json=True) + if err: + app.error(err) + else: + app.debug(type(result), result) + app.debug(headers) -For more complex case, you can used `requests`_ or `httpx`_ +.. image:: _static/images/tools_24.png + +| Color -^^^^^ +----- Look colors that you can used in `web colors`_ .. code-block:: python color_name = 'darkblue' - color = app.get_color(color_name) - app.msgbox(color) + color = app.color(color_name) + app.debug(color) color_rgb = (125, 200, 10) - color = app.get_color(color_rgb) - app.msgbox(color) + color = app.color(color_rgb) + app.debug(color) color_html = '#008080' - color = app.get_color(color_html) - app.msgbox(color) + color = app.color(color_html) + app.debug(color) .. _Unix Time: https://en.wikipedia.org/wiki/Unix_time -.. _cryptography: https://github.com/pyca/cryptography -.. _requests: https://docs.python-requests.org -.. _httpx: https://www.python-httpx.org/ .. _web colors: https://en.wikipedia.org/wiki/Web_colors diff --git a/source/easymacro.py b/source/easymacro.py index d3496b3..327acab 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -28,6 +28,7 @@ import platform import shlex import shutil import socket +import ssl import subprocess import sys import tempfile @@ -38,7 +39,13 @@ import traceback from functools import wraps from pathlib import Path from pprint import pprint -from typing import Any +from string import Template +from typing import Any, Union + +from socket import timeout +from urllib import parse +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError import uno from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS @@ -46,15 +53,6 @@ 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', '') @@ -64,9 +62,23 @@ IS_WIN = OS == 'Windows' IS_MAC = OS == 'Darwin' +LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +LOG_DATE = '%d/%m/%Y %H:%M:%S' +if IS_WIN: + logging.addLevelName(logging.ERROR, 'ERROR') + logging.addLevelName(logging.DEBUG, 'DEBUG') + logging.addLevelName(logging.INFO, 'INFO') +else: + 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__) + + _info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path) - +TIMEOUT = 10 SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc' _EVENTS = {} @@ -74,6 +86,11 @@ PYTHON = 'python' if IS_WIN: PYTHON = 'python.exe' +FILES = { + 'CONFIG': 'zaz-{}.json', +} +DIRS = {} + CTX = uno.getComponentContext() SM = CTX.getServiceManager() @@ -462,6 +479,14 @@ def data_to_dict(data) -> dict: return d +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +# Classes + + class _classproperty: def __init__(self, method=None): self.fget = method @@ -486,7 +511,7 @@ class Dates(object): :return: Return the current local date and time :rtype: datetime """ - return datetime.datetime.now() + return datetime.datetime.now().replace(microsecond=0) @_classproperty def today(cls): @@ -936,17 +961,6 @@ class Paths(object): """Get temporary directory in system""" return tempfile.gettempdir() - @_classproperty - def python(self): - """Get path executable python""" - if IS_WIN: - path = self.join(self.config('Module'), PYTHON) - elif IS_MAC: - path = self.join(self.config('Module'), '..', 'Resources', PYTHON) - else: - path = sys.executable - return path - @_classproperty def user_profile(self): """Get path user profile""" @@ -960,6 +974,17 @@ class Paths(object): path = self.config('UserConfig') return path + @_classproperty + def python(self): + """Get path executable python""" + if IS_WIN: + path = self.join(self.config('Module'), PYTHON) + elif IS_MAC: + path = self.join(self.config('Module'), '..', 'Resources', PYTHON) + else: + path = sys.executable + return path + @classmethod def to_system(cls, path:str) -> str: """Convert paths in URL to system @@ -987,18 +1012,20 @@ class Paths(object): return path @classmethod - def config(cls, name: str='Work') -> str: + def config(cls, name: str='Work') -> Union[str, list]: """Return path from config :param name: Name in service PathSettings, default get path documents :type name: str :return: Path in config, if exists. - :rtype: str + :rtype: str or list `See Api XPathSettings `_ """ path = create_instance('com.sun.star.util.PathSettings') - path = cls.to_system(getattr(path, name)) + path = cls.to_system(getattr(path, name)).split(';') + if len(path) == 1: + path = path[0] return path @classmethod @@ -1038,6 +1065,384 @@ class Paths(object): result = bool(shutil.which(name_app)) return result + # ~ Save/read data + + @classmethod + def save(cls, path: str, data: str, encoding: str='utf-8') -> bool: + """Save data in path with encoding + + :param path: Path to file save + :type path: str + :param data: Data to save + :type data: str + :param encoding: Encoding for save data, default utf-8 + :type encoding: str + :return: True, if save corrrectly + :rtype: bool + """ + result = bool(Path(path).write_text(data, encoding=encoding)) + return result + + @classmethod + def save_bin(cls, path: str, data: bytes) -> bool: + """Save binary data in path + + :param path: Path to file save + :type path: str + :param data: Data to save + :type data: bytes + :return: True, if save corrrectly + :rtype: bool + """ + result = bool(Path(path).write_bytes(data)) + return result + + @classmethod + def read(cls, path: str, get_lines: bool=False, encoding: str='utf-8') -> Union[str, list]: + """Read data in path + + :param path: Path to file read + :type path: str + :param get_lines: If read file line by line + :type get_lines: bool + :return: File content + :rtype: str or list + """ + if get_lines: + with Path(path).open(encoding=encoding) as f: + data = f.readlines() + else: + data = Path(path).read_text(encoding=encoding) + return data + + @classmethod + def read_bin(cls, path: str) -> bytes: + """Read binary data in path + + :param path: Path to file read + :type path: str + :return: File content + :rtype: bytes + """ + data = Path(path).read_bytes() + return data + + # ~ Import/export data + + @classmethod + def from_json(cls, path: str) -> Any: + """Read path file and load json data + + :param path: Path to file + :type path: str + :return: Any data + :rtype: Any + """ + data = json.loads(cls.read(path)) + return data + + @classmethod + def to_json(cls, path: str, data: str): + """Save data in path file like json + + :param path: Path to file + :type path: str + :return: True if save correctly + :rtype: bool + """ + data = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) + return cls.save(path, data) + + +class Config(object): + """Class for set and get configurations + """ + @classmethod + def set(cls, prefix: str, value: Any, key: str='') -> bool: + """Save data config in user config like json + + :param prefix: Unique prefix for this data + :type prefix: str + :param value: Value for save + :type value: Any + :param key: Key for value + :type key: str + :return: True if save correctly + :rtype: bool + """ + name_file = FILES['CONFIG'].format(prefix) + path = Paths.join(Paths.user_config, name_file) + data = value + if key: + data = cls.get(prefix) + data[key] = value + result = Paths.to_json(path, data) + return result + + @classmethod + def get(cls, prefix: str, key: str='', default: Any={}) -> Any: + """Get data config from user config like json + + :param prefix: Unique prefix for this data + :type prefix: str + :param key: Key for value + :type key: str + :param default: Get if not exists key + :type default: Any + :return: data + :rtype: Any + """ + data = {} + name_file = FILES['CONFIG'].format(prefix) + path = Paths.join(Paths.user_config, name_file) + if not Paths.exists(path): + return data + + data = Paths.from_json(path) + if key: + data = data.get(key, default) + + return data + + +class Url(object): + """Class for simple url open + """ + @classmethod + def _open(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ + json: bool=False, timeout: int=TIMEOUT, method: str='GET') -> tuple: + """URL Open""" + + debug(url) + result = None + context = None + rheaders = {} + err = '' + + if verify: + if not data is None and isinstance(data, str): + data = data.encode() + else: + context = ssl._create_unverified_context() + + try: + req = Request(url, data=data, headers=headers, method=method) + response = urlopen(req, timeout=timeout, context=context) + except HTTPError as e: + error(e) + err = str(e) + except URLError as e: + error(e.reason) + err = str(e.reason) + # ToDo + # ~ except timeout: + # ~ err = 'timeout' + # ~ error(err) + else: + rheaders = dict(response.info()) + result = response.read().decode() + if json: + result = Json.loads(result) + + return result, rheaders, err + + @classmethod + def get(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ + json: bool=False, timeout: int=TIMEOUT) -> tuple: + """Method GET + + :param url: Url to open + :type url: str + :return: result, headers and error + :rtype: tuple + """ + return cls._open(url, data, headers, verify, json, timeout) + + # ToDo + @classmethod + def _post(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \ + json: bool=False, timeout: int=TIMEOUT) -> tuple: + """Method POST + """ + data = parse.urlencode(data).encode('ascii') + return cls._open(url, data, headers, verify, json, timeout, 'POST') + + +class Color(object): + """Class for colors + + `See Web Colors `_ + """ + 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, + } + + def _get_color(self, index): + if isinstance(index, tuple): + color = (index[0] << 16) + (index[1] << 8) + index[2] + else: + if index[0] == '#': + r, g, b = bytes.fromhex(index[1:]) + color = (r << 16) + (g << 8) + b + else: + color = self.COLORS.get(index.lower(), -1) + return color + + def __call__(self, index): + return self._get_color(index) + + def __getitem__(self, index): + return self._get_color(index) + + +COLOR_ON_FOCUS = Color()('LightYellow') + def __getattr__(name): classes = { @@ -1048,6 +1453,9 @@ def __getattr__(name): 'timer': Timer, 'hash': Hash, 'path': Paths, + 'config': Config, + 'url': Url, + 'color': Color(), } if name in classes: return classes[name]