source: rattail/rattail/util.py @ eb37e0f

Last change on this file since eb37e0f was eb37e0f, checked in by Lance Edgar <ledgar@…>, 13 months ago

Accept hours as decimal instead of delta, for util.pretty_hours()

  • Property mode set to 100644
File size: 7.2 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2019 Lance Edgar
6#
7#  This file is part of Rattail.
8#
9#  Rattail is free software: you can redistribute it and/or modify it under the
10#  terms of the GNU General Public License as published by the Free Software
11#  Foundation, either version 3 of the License, or (at your option) any later
12#  version.
13#
14#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
15#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17#  details.
18#
19#  You should have received a copy of the GNU General Public License along with
20#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Utilities
25"""
26
27from __future__ import unicode_literals, absolute_import
28from __future__ import division
29
30import sys
31import datetime
32import decimal
33import subprocess
34
35import six
36from pkg_resources import iter_entry_points
37
38try:
39    from collections import OrderedDict
40except ImportError: # pragma no cover
41    from ordereddict import OrderedDict
42
43
44# generic singleton to indicate an arg which isn't set etc.
45NOTSET = object()
46
47
48def capture_output(command):
49    """
50    Runs ``command`` and returns any output it produces.
51    """
52    # We *need* to pipe ``stdout`` because that's how we capture the output of
53    # the ``hg`` command.  However, we must pipe *all* handles in order to
54    # prevent issues when running as a GUI but *from* the Windows console.  See
55    # also: http://bugs.python.org/issue3905
56    kwargs = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
57    output = subprocess.Popen(command, **kwargs).communicate()[0]
58    return output
59
60
61def data_diffs(local_data, host_data, fields=None):
62    """
63    Find all (relevant) fields which differ between the host and local data
64    values for a given record.
65    """
66    if fields is None:
67        fields = list(local_data)
68    diffs = []
69    for field in fields:
70        if local_data[field] != host_data[field]:
71            diffs.append(field)
72    return diffs
73
74
75def import_module_path(module_path):
76    """
77    Import an arbitrary Python module.
78
79    :param module_path: String referencing a module by its "dotted path".
80
81    :returns: The referenced module.
82    """
83    if module_path in sys.modules:
84        return sys.modules[module_path]
85    module = __import__(module_path)
86
87    def last_module(module, module_path):
88        parts = module_path.split('.')
89        parts.pop(0)
90        child = getattr(module, parts[0])
91        if len(parts) == 1:
92            return child
93        return last_module(child, '.'.join(parts))
94
95    return last_module(module, module_path)
96
97
98def get_object_spec(obj):
99    """
100    Returns the "object spec" string for the given object.
101    """
102    # TODO: this only handles a class *instance* currently
103    return '{}:{}'.format(obj.__module__, obj.__class__.__name__)
104
105
106def load_object(specifier):
107    """
108    Load an arbitrary object from a module, according to a specifier.
109
110    The specifier string should contain a dotted path to an importable module,
111    followed by a colon (``':'``), followed by the name of the object to be
112    loaded.  For example:
113
114    .. code-block:: none
115
116       rattail.files:overwriting_move
117
118    You'll notice from this example that "object" in this context refers to any
119    valid Python object, i.e. not necessarily a class instance.  The name may
120    refer to a class, function, variable etc.  Once the module is imported, the
121    ``getattr()`` function is used to obtain a reference to the named object;
122    therefore anything supported by that method should work.
123
124    :param specifier: Specifier string.
125
126    :returns: The specified object.
127    """
128    module_path, name = specifier.split(':')
129    module = import_module_path(module_path)
130    return getattr(module, name)
131
132
133def load_entry_points(group):
134    """
135    Load a set of ``setuptools``-style entry points.
136
137    This is a convenience wrapper around ``pkg_resources.iter_entry_points()``.
138
139    :param group: The group of entry points to be loaded.
140
141    :returns: A dictionary whose keys are the entry point names, and values are
142       the loaded entry points.
143    """
144    entry_points = {}
145    for entry_point in iter_entry_points(group):
146        entry_points[entry_point.name] = entry_point.load()
147    return entry_points
148
149
150def prettify(text):
151    """
152    Return a "prettified" version of text.
153    """
154    text = text.replace('_', ' ')
155    text = text.replace('-', ' ')
156    words = text.split()
157    return ' '.join([x.capitalize() for x in words])
158
159
160def pretty_boolean(value):
161    """
162    Returns ``'Yes'`` or ``'No'`` or empty string if value is ``None``
163    """
164    if value is None:
165        return ""
166    return "Yes" if value else "No"
167
168
169def hours_as_decimal(hours, places=2):
170    """
171    Convert the given ``datetime.timedelta`` object into a Decimal whose
172    value is in terms of hours.
173    """
174    if hours is None:
175        return
176    minutes = (hours.days * 1440) + (hours.seconds // 60)
177    fmt = '{{:0.{}f}}'.format(places)
178    return decimal.Decimal(fmt.format(minutes / 60.0))
179
180
181def pretty_hours(hours=None, seconds=None):
182    """
183    Format the given ``hours`` value (which is assumed to be a
184    ``datetime.timedelta`` object) as HH:MM.  Note that instead of providing
185    that, you can provide ``seconds`` instead and a delta will be generated.
186    """
187    if hours is None and seconds is None:
188        return ''
189    if hours is None:
190        hours = datetime.timedelta(seconds=seconds)
191    if isinstance(hours, decimal.Decimal):
192        hours = datetime.timedelta(seconds=int(hours * 3600))
193    minutes = (hours.days * 1440) + (hours.seconds / 60)
194    return "{}:{:02d}".format(int(minutes // 60), int(minutes % 60))
195
196
197def pretty_quantity(value, empty_zero=False):
198    """
199    Return a "pretty" version of the given value, as string.  This is meant primarily
200    for use with things like order quantities, so that e.g. 1.0000 => 1
201    """
202    if value is None:
203        return ''
204    if int(value) == value:
205        value = int(value)
206        if empty_zero and value == 0:
207            return ''
208        return six.text_type(value)
209    return six.text_type(value).rstrip('0')
210
211
212def progress_loop(func, items, factory, *args, **kwargs):
213    """
214    This will iterate over ``items`` and call ``func`` for each.  If a progress
215    ``factory`` kwarg is provided, then a progress instance will be created and
216    updated along the way.
217    """
218    message = kwargs.pop('message', None)
219    count = kwargs.pop('count', None)
220    allow_cancel = kwargs.pop('allow_cancel', False)
221    if count is None:
222        try:
223            count = len(items)
224        except TypeError:
225            count = items.count()
226    if not count:
227        return True
228
229    prog = None
230    if factory:
231        prog = factory(message, count)
232
233    canceled = False
234    for i, item in enumerate(items, 1):
235        func(item, i, *args, **kwargs)
236        if prog and not prog.update(i):
237            canceled = True
238            break
239    if prog:
240        prog.finish()
241    if canceled and not allow_cancel:
242        raise RuntimeError("Operation was canceled")
243    return not canceled
Note: See TracBrowser for help on using the repository browser.