1# -*- coding: utf-8 -*-
2
3# Copyright(C) 2010-2011 Romain Bignon
4#
5# This file is part of weboob.
6#
7# weboob is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# weboob is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with weboob. If not, see <http://www.gnu.org/licenses/>.
19
20
21import re
22import time
23from collections import OrderedDict
24
25from weboob.tools.compat import unicode
26
27from .misc import to_unicode
28
29
30__all__ = ['ValuesDict', 'Value', 'ValueBackendPassword', 'ValueInt', 'ValueFloat', 'ValueBool']
31
32
33class ValuesDict(OrderedDict):
34    """
35    Ordered dictionarry which can take values in constructor.
36
37    >>> ValuesDict(Value('a', label='Test'), ValueInt('b', label='Test2'))
38    """
39
40    def __init__(self, *values):
41        super(ValuesDict, self).__init__()
42        for v in values:
43            self[v.id] = v
44
45
46class Value(object):
47    """
48    Value.
49
50    :param label: human readable description of a value
51    :type label: str
52    :param required: if ``True``, the backend can't load if the key isn't found in its configuration
53    :type required: bool
54    :param default: an optional default value, used when the key is not in config. If there is no default value and the key
55                    is not found in configuration, the **required** parameter is implicitly set
56    :param masked: if ``True``, the value is masked. It is useful for applications to know if this key is a password
57    :type masked: bool
58    :param regexp: if specified, on load the specified value is checked against this regexp, and an error is raised if it doesn't match
59    :type regexp: str
60    :param choices: if this parameter is set, the value must be in the list
61    :type choices: (list,dict)
62    :param aliases: mapping of old choices values that should be accepted but not presented
63    :type aliases: dict
64    :param tiny: the value of choices can be entered by an user (as they are small)
65    :type tiny: bool
66    :param transient: this value is not persistent (asked only if needed)
67    :type transient: bool
68    """
69
70    def __init__(self, *args, **kwargs):
71        if len(args) > 0:
72            self.id = args[0]
73        else:
74            self.id = ''
75        self.label = kwargs.get('label', kwargs.get('description', None))
76        self.description = kwargs.get('description', kwargs.get('label', None))
77        self.default = kwargs.get('default', None)
78        if isinstance(self.default, str):
79            self.default = to_unicode(self.default)
80        self.regexp = kwargs.get('regexp', None)
81        self.choices = kwargs.get('choices', None)
82        self.aliases = kwargs.get('aliases')
83        if isinstance(self.choices, (list, tuple)):
84            self.choices = OrderedDict(((v, v) for v in self.choices))
85        self.tiny = kwargs.get('tiny', None)
86        self.transient = kwargs.get('transient', None)
87        self.masked = kwargs.get('masked', False)
88        self.required = kwargs.get('required', self.default is None)
89        self._value = kwargs.get('value', None)
90
91    def show_value(self, v):
92        if self.masked:
93            return u''
94        else:
95            return v
96
97    def check_valid(self, v):
98        """
99        Check if the given value is valid.
100
101        :raises: ValueError
102        """
103        if self.required and v is None:
104            raise ValueError('Value is required and thus must be set')
105        if v == self.default:
106            return
107        if v == '' and self.default != '' and (self.choices is None or v not in self.choices):
108            raise ValueError('Value can\'t be empty')
109        if self.regexp is not None and not re.match(self.regexp + '$', unicode(v) if v is not None else ''):
110            raise ValueError('Value "%s" does not match regexp "%s"' % (self.show_value(v), self.regexp))
111        if self.choices is not None and v not in self.choices:
112            if not self.aliases or v not in self.aliases:
113                raise ValueError('Value "%s" is not in list: %s' % (
114                    self.show_value(v), ', '.join(unicode(s) for s in self.choices)))
115
116    def load(self, domain, v, requests):
117        """
118        Load value.
119
120        :param domain: what is the domain of this value
121        :type domain: str
122        :param v: value to load
123        :param requests: list of weboob requests
124        :type requests: weboob.core.requests.Requests
125        """
126        return self.set(v)
127
128    def set(self, v):
129        """
130        Set a value.
131        """
132        if isinstance(v, str):
133            v = to_unicode(v)
134        self.check_valid(v)
135        if self.aliases and v in self.aliases:
136            v = self.aliases[v]
137        self._value = v
138
139    def dump(self):
140        """
141        Dump value to be stored.
142        """
143        return self.get()
144
145    def get(self):
146        """
147        Get the value.
148        """
149        return self._value
150
151
152class ValueTransient(Value):
153    def __init__(self, *args, **kwargs):
154        kwargs.setdefault('transient', True)
155        kwargs.setdefault('default', None)
156        kwargs.setdefault('required', False)
157        super(ValueTransient, self).__init__(*args, **kwargs)
158
159    def dump(self):
160        return ''
161
162
163class ValueBackendPassword(Value):
164    _domain = None
165    _requests = None
166    _stored = True
167
168    def __init__(self, *args, **kwargs):
169        kwargs['masked'] = kwargs.pop('masked', True)
170        self.noprompt = kwargs.pop('noprompt', False)
171        super(ValueBackendPassword, self).__init__(*args, **kwargs)
172        self.default = kwargs.get('default', '')
173
174    def load(self, domain, password, requests):
175        self.check_valid(password)
176        self._domain = domain
177        self._value = to_unicode(password)
178        self._requests = requests
179
180    def check_valid(self, passwd):
181        if passwd == '':
182            # always allow empty passwords
183            return True
184        return super(ValueBackendPassword, self).check_valid(passwd)
185
186    def set(self, passwd):
187        self.check_valid(passwd)
188        if passwd is None:
189            # no change
190            return
191        self._value = ''
192        if passwd == '':
193            return
194        if self._domain is None:
195            self._value = to_unicode(passwd)
196            return
197
198        self._value = to_unicode(passwd)
199
200    def dump(self):
201        if self._stored:
202            return self._value
203        else:
204            return ''
205
206    def get(self):
207        if self._value != '' or self._domain is None:
208            return self._value
209
210        passwd = None
211
212        if passwd is not None:
213            # Password has been read in the keyring.
214            return to_unicode(passwd)
215
216        # Prompt user to enter password by hand.
217        if not self.noprompt and self._requests:
218            self._value = self._requests.request('login', self._domain, self)
219            if self._value is None:
220                self._value = ''
221            else:
222                self._value = to_unicode(self._value)
223                self._stored = False
224        return self._value
225
226
227class ValueInt(Value):
228    def __init__(self, *args, **kwargs):
229        kwargs['regexp'] = '^\d+$'
230        super(ValueInt, self).__init__(*args, **kwargs)
231        self.default = kwargs.get('default', 0)
232
233    def get(self):
234        return int(self._value)
235
236
237class ValueFloat(Value):
238    def __init__(self, *args, **kwargs):
239        kwargs['regexp'] = '^[\d\.]+$'
240        super(ValueFloat, self).__init__(*args, **kwargs)
241        self.default = kwargs.get('default', 0.0)
242
243    def check_valid(self, v):
244        try:
245            float(v)
246        except ValueError:
247            raise ValueError('Value "%s" is not a float value' % self.show_value(v))
248
249    def get(self):
250        return float(self._value)
251
252
253class ValueBool(Value):
254    def __init__(self, *args, **kwargs):
255        kwargs['choices'] = {'y': 'True', 'n': 'False'}
256        super(ValueBool, self).__init__(*args, **kwargs)
257        self.default = kwargs.get('default', False)
258
259    def check_valid(self, v):
260        if not isinstance(v, bool) and \
261            unicode(v).lower() not in ('y', 'yes', '1', 'true',  'on',
262                                       'n', 'no',  '0', 'false', 'off'):
263            raise ValueError('Value "%s" is not a boolean (y/n)' % self.show_value(v))
264
265    def get(self):
266        return (isinstance(self._value, bool) and self._value) or \
267                unicode(self._value).lower() in ('y', 'yes', '1', 'true', 'on')
268
269
270class ValueDate(Value):
271    DEFAULT_FORMATS = ('%Y-%m-%d',)
272
273    def __init__(self, *args, **kwargs):
274        super(ValueDate, self).__init__(*args, **kwargs)
275        self.formats = tuple(kwargs.get('formats', ()))
276        self.formats_tuple = self.DEFAULT_FORMATS + self.formats
277
278    def get_format(self, v=None):
279        for format in self.formats_tuple:
280            try:
281                dateval = time.strptime(v or self._value, format)
282                # year < 1900 is handled by strptime but not strftime, check it
283                time.strftime(self.formats_tuple[0], dateval)
284            except ValueError:
285                continue
286            return format
287
288    def check_valid(self, v):
289        super(ValueDate, self).check_valid(v)
290        if v is not None and not self.get_format(v):
291            raise ValueError('Value "%s" does not match format in %s' % (self.show_value(v), self.show_value(self.formats_tuple)))
292
293    def get(self):
294        if self.formats:
295            self._value = time.strftime(self.formats[0], time.strptime(self._value, self.get_format()))
296        return self._value
297