Source code for rdial.utils

#
"""utils - Utility functions for rdial."""
# Copyright © 2012-2019  James Rowe <jnrowe@gmail.com>
#
# SPDX-License-Identifier: GPL-3.0+
#
# This file is part of rdial.
#
# rdial 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.
#
# rdial 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
# rdial.  If not, see <http://www.gnu.org/licenses/>.

import configparser
import functools
import os
import subprocess
from contextlib import contextmanager
from datetime import date, datetime, timedelta
from typing import Callable, ContextManager, Dict, Optional, Tuple, Union
try:
    from importlib import resources
except ImportError:  # pragma: no cover
    import importlib_resources as resources

import click

from jnrbase import xdg_basedir
from jnrbase.iso_8601 import parse_datetime


[docs]class RdialError(ValueError): """Generic exception for rdial."""
#: Map duration string keys to timedelta args _MAPPER = {'D': 'days', 'H': 'hours', 'M': 'minutes', 'S': 'seconds'} \ # type : Dict[str, str]
[docs]def parse_datetime_user(__string: str) -> datetime: """Parse datetime string from user. We accept the normal |ISO|-8601 formats, but kick through to the formats supported by the system’s :command:`date` command if parsing fails. Args: __string: Datetime string to parse Returns: Parsed datetime object """ try: datetime_ = parse_datetime(__string) except ValueError: try: proc = subprocess.run( ['date', '--utc', '--iso-8601=seconds', '-d', __string], stdout=subprocess.PIPE, check=True) output = proc.stdout.decode() datetime_ = parse_datetime(output.strip()[:19]) except subprocess.CalledProcessError: datetime_ = None if not datetime_: raise ValueError(f'Unable to parse timestamp {__string!r}') return datetime_.replace(tzinfo=None)
[docs]def iso_week_to_date(__year: int, __week: int) -> Tuple[date, date]: """Generate date range for a given |ISO|-8601 week. |ISO|-8601 defines a week as Monday to Sunday, with the first week of a year being the first week containing a Thursday. Args: __year: Year to process __week: Week number to process Returns: Date range objects for given week """ bound = date(__year, 1, 4) iso_start = bound - timedelta(days=bound.isocalendar()[2] - 1) start = iso_start + timedelta(weeks=__week - 1) end = start + timedelta(weeks=1) return start, end
[docs]def read_config(user_config: Optional[str] = None, cli_options: Optional[Dict[str, Union[bool, str]]] = None ) -> configparser.ConfigParser: """Read configuration data. Args: user_config: User defined config file cli_options: Command line options Returns: Parsed configuration data """ # Only base *must* exist conf = configparser.ConfigParser() # No, it *really* must conf.read_string(resources.read_text('rdial', 'config'), 'pkg config') conf['DEFAULT'] = {'xdg_data_location': xdg_basedir.user_data('rdial')} for f in xdg_basedir.get_configs('rdial'): conf.read(f) conf.read(os.path.abspath('.rdialrc')) if user_config: conf.read(user_config) if cli_options: conf.read_dict({ 'rdial': {k: v for k, v in cli_options.items() if v is not None} }) return conf
[docs]def write_current(__fun: Callable) -> Callable: """Decorator to write :file:`.current` file on function exit. See also: :doc:`/taskbars` Args: __fun: Function to wrap Returns: Wrapped function """ @functools.wraps(__fun) def wrapper(*args, **kwargs): """Write value of ``task`` argument to ``.current`` on exit. Args: args: Positional arguments kwargs: Keyword arguments """ globs = args[0] __fun(*args, **kwargs) with click.open_file(f'{globs.directory}/.current', 'w') as f: f.write(kwargs['task']) return wrapper
[docs]def remove_current(__fun: Callable) -> Callable: """Decorator to remove :file:`.current` file on function exit. See also: :doc:`/taskbars` Args: __fun: Function to wrap Returns: Wrapped function """ @functools.wraps(__fun) def wrapper(*args, **kwargs): """Remove ``.current`` file on exit. Args: args: Positional arguments kwargs: Keyword arguments """ globs = args[0] __fun(*args, **kwargs) if os.path.isfile(f'{globs.directory}/.current'): os.unlink(f'{globs.directory}/.current') return wrapper
[docs]def newer(__fname: str, __reference: str) -> bool: """Check whether given file is newer than reference file. Args: __fname: File to check __reference: file to test against Returns: ``True`` if ``__fname`` is newer than ``__reference`` """ return os.stat(__fname).st_mtime > os.stat(__reference).st_mtime
[docs]def maybe_profile() -> ContextManager: # pragma: no cover """Profile the wrapped code block. When :envvar:`RDIAL_PROFILE` is set execute the enclosed block under bprofile_. The envvar’s value should be the name of the output file to generate. When :envvar:`RDIAL_PROFILE` is unset, this is just a no-op. .. _bprofile: https://pypi.org/project/bprofile/ """ profile = os.getenv('RDIAL_PROFILE') if profile: from bprofile import BProfile profiler = BProfile(profile) else: @contextmanager def noop(): yield profiler = noop() return profiler