Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

# Copyright (C) 2015 Chintalagiri Shashank 

# 

# This file is part of Tendril. 

# 

# This program is free software: you can redistribute it and/or modify 

# it under the terms of the GNU Affero General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# This program 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 Affero General Public License for more details. 

# 

# You should have received a copy of the GNU Affero General Public License 

# along with this program.  If not, see <http://www.gnu.org/licenses/>. 

""" 

Currency Types (:mod:`tendril.utils.types.currency`) 

==================================================== 

 

The :mod:`tendril.utils.types.currency` contains classes which allow for easy 

use and manipulation of currency values. The primary focus is on the primary 

use cases of Currencies within tendril, i.e. : 

 

- Handling foreign exchange conversions and exchange rates in application code 

  without too much fuss. 

- Handling currency arithmetic and comparisons. 

 

This module uses a specific `Base Currency`, defined by 

:const:`tendril.utils.config.BASE_CURRENCY` and 

:const:`tendril.utils.config.BASE_CURRENCY_SYMBOL` and available as this 

module's :data:`native_currency_defn` module variable. In case this module is 

to be used independent of Tendril, at least those configuration options 

*must* be defined in :mod:`tendril.utils.config`. 

 

.. rubric:: Module Contents 

 

.. autosummary:: 

 

    native_currency_defn 

    CurrencyDefinition 

    CurrencyValue 

 

.. seealso:: :mod:`tendril.utils.types`, for an overview applicable to most 

             types defined in Tendril. 

 

.. todo:: The core numbers in this module need to switched to 

          :class:`decimal.Decimal`. 

 

""" 

 

from tendril.utils.config import BASE_CURRENCY 

from tendril.utils.config import BASE_CURRENCY_SYMBOL 

 

from tendril.utils import www 

 

from six.moves.urllib.request import Request 

from six.moves.urllib.parse import urlencode 

 

import json 

import codecs 

import numbers 

 

from .unitbase import TypedComparisonMixin 

 

 

class CurrencyDefinition(object): 

    """ 

    Instances of this class define a currency. 

 

    The minimal requirement to define a currency is a :attr:`code`, which 

    would usually be a standard internationally recognized currency code. 

 

    In addition to the :attr:`code`, a currency definition also includes 

    an optional :attr:`symbol`, which is used to create string representations 

    of currency values in that currency. In the absence of a :attr:`symbol`, 

    the :attr:`code` is used in it's place. 

 

    Unless otherwise specified during the instantiation of the class, 

    the exchange rate is obtained from internet services by the 

    :meth:`_get_exchval` method. 

 

    :param code: Standard currency code. 

    :param symbol: Symbol to use to represent the currency. Optional. 

    :param exchval: Exchange rate to use, if not automatic. Optional. 

 

    """ 

    def __init__(self, code, symbol=None, exchval=None): 

        self._code = code 

        self._symbol = symbol 

        if exchval is not None: 

            self._exchval = exchval 

        else: 

            self._exchval = self._get_exchval(self._code) 

 

    @property 

    def code(self): 

        """ 

 

        :return: The currency code. 

        :rtype: str 

 

        """ 

        return self._code 

 

    @property 

    def symbol(self): 

        """ 

 

        :return: The currency symbol, or code if no symbol. 

        :rtype: str 

 

        """ 

        if self._symbol is not None: 

            return self._symbol 

        else: 

            return self._code 

 

    @property 

    def exchval(self): 

        """ 

        :return: The exchange rate 

        :rtype: float 

 

        """ 

        return self._exchval 

 

    @staticmethod 

    def _get_exchval(code): 

        """ 

        Obtains the exchange rate of the currency definition's :attr:`code` 

        using the `<http://fixer.io>`_ JSON API. The native currency is used 

        as the reference. 

 

        :param code: The currency code for which the exchange rate is needed. 

        :type code: str 

        :return: The exchange rate of currency specified by code vs the native 

                 currency. 

        :rtype: float 

 

        """ 

 

        if BASE_CURRENCY == code: 

            return 1 

        apiurl = 'http://api.fixer.io/latest?' 

        params = {'base': BASE_CURRENCY, 

                  'symbols': code} 

        request = Request(apiurl + urlencode(params)) 

        response = www.urlopen(request) 

        reader = codecs.getreader("utf-8") 

        data = json.load(reader(response)) 

        try: 

            rate = 1 / float(data['rates'][code]) 

        except KeyError: 

            raise KeyError(code) 

        return rate 

 

    def __eq__(self, other): 

        """ 

        Two instances of :class:`CurrencyDefinition` will evaluate to be equal 

        only when all three parameters of the instances are equal. 

        """ 

        if self.code != other.code: 

            return False 

        if self.symbol != other.symbol: 

            return False 

        if self.exchval != other.exchval: 

            return False 

        return True 

 

#: The native currency definition used by the module 

#: 

#: This definition uses the code contained in 

#: :const:`tendril.utils.config.BASE_CURRENCY` and symbol 

#: :const:`tendril.utils.config.BASE_CURRENCY_SYMBOL`. Application 

#: code should import this definition instead of creating new currency 

#: definitions whenever one is needed to represent a native currency value. 

native_currency_defn = CurrencyDefinition(BASE_CURRENCY, BASE_CURRENCY_SYMBOL) 

 

 

class CurrencyValue(TypedComparisonMixin): 

    """ 

    Instances of this class define a specific currency value, or a certain 

    sum of money. 

 

    The `currency_def` can either be a :class:`CurrencyDefinition` instance 

    (recommended), or a string containing the code for the currency. 

 

    :param val: The numerical value. 

    :param currency_def: The currency definition within which the value 

                         is defined. 

    :type currency_def: :class:`CurrencyDefinition` or str 

 

    .. note:: Since the exchange rate is obtained at the instantiation of 

              the :class:`CurrencyDefinition`, using a string instead of a 

              predefined :class:`CurrencyDefinition` instance may result in 

              instances of the same currency, but with different exchange 

              rates. 

 

    :ivar _currency_def: The currency definition of the source value of the 

                         instance. 

    :ivar _val: The numerical value in the source currency of the instance. 

 

    .. rubric:: Arithmetic Operations 

 

    .. autosummary:: 

 

        __add__ 

        __sub__ 

        __mul__ 

        __div__ 

        _cmpkey 

 

    """ 

    def __init__(self, val, currency_def): 

        if isinstance(currency_def, CurrencyDefinition): 

            self._currency_def = currency_def 

        else: 

            self._currency_def = CurrencyDefinition(currency_def) 

        self._val = val 

 

    @property 

    def native_value(self): 

        """ 

        The numerical value of the currency value in the native currency, 

        i.e., that defined by :data:`native_currency_defn`. 

 

        :rtype: float 

 

        """ 

        return self._val * self._currency_def.exchval 

 

    @property 

    def native_string(self): 

        """ 

        The string representation of the currency value in the native 

        currency, i.e., that defined by :data:`native_currency_defn`. 

 

        :rtype: str 

 

        """ 

        return BASE_CURRENCY_SYMBOL + "{0:,.2f}".format(self.native_value) 

 

    @property 

    def source_value(self): 

        """ 

        The numerical value of the currency value in the source currency, 

        i.e., that defined by :attr:`source_currency`. 

 

        :rtype: float 

 

        """ 

        return self._val 

 

    @property 

    def source_string(self): 

        """ 

        The string representation of the currency value in the source 

        currency, i.e., that defined by :attr:`source_currency`. 

 

        :rtype: str 

 

        """ 

        return self._currency_def.symbol + "{0:,.2f}".format(self._val) 

 

    @property 

    def source_currency(self): 

        """ 

        The currency definition of the source currency, i.e, the instance 

        variable :data:`_currency_def`. 

 

        :rtype: :class:`CurrencyDefinition` 

 

        """ 

        return self._currency_def 

 

    def __repr__(self): 

        return self.native_string 

 

    def __float__(self): 

        return float(self.native_value) 

 

    def __add__(self, other): 

        """ 

        Addition of two :class:`CurrencyValue` instances returns a 

        :class:`CurrencyValue` instance with the sum of the two operands, 

        with currency conversion applied if necessary. 

 

        If the :attr:`source_currency` of the two operands are equal, 

        the result uses the the same :attr:`source_currency`. If not, 

        the result is uses the :data:`native_currency_defn` as it's 

        :attr:`source_currency`. 

 

        If the other operand is a numerical type and evaluates to 0, this 

        object is simply returned unchanged. 

 

        Addition with all other Types / Classes is not supported. 

 

        :rtype: :class:`CurrencyValue` 

        """ 

        if isinstance(other, numbers.Number) and other == 0: 

            return self 

        if not isinstance(other, CurrencyValue): 

            raise NotImplementedError 

        if self._currency_def.symbol == other.source_currency.symbol: 

            return CurrencyValue( 

                self.source_value + other.source_value, self.source_currency 

            ) 

        else: 

            return CurrencyValue( 

                self.native_value + other.native_value, native_currency_defn 

            ) 

 

    def __radd__(self, other): 

        if other == 0: 

            return self 

        else: 

            return self.__add__(other) 

 

    def __mul__(self, other): 

        """ 

        Multiplication of one :class:`CurrencyValue` instance with a 

        numerical type results in a :class:`CurrencyValue` instance, 

        whose value is is the currency type operand's value multiplied 

        by the numerical operand's value. 

 

        The :attr:`source_currency` of the returned :class:`CurrencyValue` 

        is the same as that of the currency type operand. 

 

        Multiplication with all other Types / Classes is not supported. 

 

        :rtype: :class:`CurrencyValue` 

        """ 

        if isinstance(other, numbers.Number): 

            return CurrencyValue( 

                self.source_value * other, self.source_currency 

            ) 

        else: 

            raise NotImplementedError 

 

    def __div__(self, other): 

        """ 

        Division of one :class:`CurrencyValue` instance with a numerical type 

        results in a :class:`CurrencyValue` instance, whose value is is the 

        currency type operand's value divided by the numerical operand's 

        value. 

 

        The :attr:`source_currency` of the returned :class:`CurrencyValue` 

        is the same as that of the currency type operand. In this case, the 

        first operand must be a :class:`CurrencyValue`, and not the reverse. 

 

        Division of one :class:`CurrencyValue` instance by another returns a 

        numerical value, which is obtained by performing the division with the 

        operands' :attr:`native_value`. 

 

        Division with all other Types / Classes is not supported. 

 

        :rtype: :class:`CurrencyValue` 

        """ 

        if isinstance(other, numbers.Number): 

            return CurrencyValue( 

                self.source_value / other, self.source_currency 

            ) 

        elif isinstance(other, CurrencyValue): 

            return self.native_value / other.native_value 

        else: 

            raise NotImplementedError 

 

    def __truediv__(self, other): 

        return self.__div__(other) 

 

    def __rmul__(self, other): 

        return self.__mul__(other) 

 

    def __sub__(self, other): 

        """ 

        Subtraction of two :class:`CurrencyValue` instances returns a 

        :class:`CurrencyValue` instance with the difference of the two 

        operands, with currency conversion applied if necessary. 

 

        If :attr:`source_currency` of the two operands are equal, 

        the result uses the the same :attr:`source_currency`. If not, 

        the result is in the :data:`native_currency_defn`. 

 

        If the other operand is a numerical type and evaluates to 0, this 

        object is simply returned unchanged. 

 

        Subtraction with all other Types / Classes is not supported. 

 

        :rtype: :class:`CurrencyValue` 

        """ 

        if isinstance(other, numbers.Number) and other == 0: 

            return self 

        elif not isinstance(other, CurrencyValue): 

            raise NotImplementedError 

        else: 

            return self.__add__(other.__mul__(-1)) 

 

    def _cmpkey(self): 

        """ 

        The comparison of two :class:`CurrencyValue` instances behaves 

        identically to the comparison of the operands' :attr:`native_value`. 

 

        Comparison with all other Types / Classes is not supported. 

        """ 

        return self.native_value