gazpar2haws 0.2.1__py3-none-any.whl → 0.3.0b15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gazpar2haws/pricer.py ADDED
@@ -0,0 +1,571 @@
1
+ import calendar
2
+ from datetime import date, timedelta
3
+ from typing import Optional, Tuple, overload
4
+
5
+ from gazpar2haws.model import (
6
+ BaseUnit,
7
+ ConsumptionPriceArray,
8
+ ConsumptionQuantityArray,
9
+ CostArray,
10
+ EnergyTaxesPriceArray,
11
+ PriceUnit,
12
+ PriceValue,
13
+ Pricing,
14
+ QuantityUnit,
15
+ SubscriptionPriceArray,
16
+ TimeUnit,
17
+ TransportPriceArray,
18
+ Value,
19
+ ValueArray,
20
+ ValueUnit,
21
+ VatRate,
22
+ VatRateArray,
23
+ )
24
+
25
+
26
+ class Pricer:
27
+
28
+ # ----------------------------------
29
+ def __init__(self, pricing: Pricing):
30
+ self._pricing = pricing
31
+
32
+ # ----------------------------------
33
+ def pricing_data(self) -> Pricing:
34
+ return self._pricing
35
+
36
+ # ----------------------------------
37
+ def compute( # pylint: disable=too-many-branches
38
+ self, quantities: ConsumptionQuantityArray, price_unit: PriceUnit
39
+ ) -> CostArray:
40
+
41
+ if quantities is None:
42
+ raise ValueError("quantities is None")
43
+
44
+ if quantities.start_date is None:
45
+ raise ValueError("quantities.start_date is None")
46
+
47
+ start_date = quantities.start_date
48
+
49
+ if quantities.end_date is None:
50
+ raise ValueError("quantities.end_date is None")
51
+
52
+ end_date = quantities.end_date
53
+
54
+ if quantities.value_array is None:
55
+ raise ValueError("quantities.value_array is None")
56
+
57
+ if quantities.value_unit is None:
58
+ raise ValueError("quantities.value_unit is None")
59
+
60
+ if quantities.base_unit is None:
61
+ raise ValueError("quantities.base_unit is None")
62
+
63
+ quantity_array = quantities.value_array
64
+
65
+ # Convert all pricing data to the same unit as the quantities.
66
+ consumption_prices = Pricer.convert(self._pricing.consumption_prices, (price_unit, quantities.value_unit))
67
+
68
+ if self._pricing.subscription_prices is not None and len(self._pricing.subscription_prices) > 0:
69
+ subscription_prices = Pricer.convert(self._pricing.subscription_prices, (price_unit, quantities.base_unit))
70
+ else:
71
+ subscription_prices = None
72
+
73
+ if self._pricing.transport_prices is not None and len(self._pricing.transport_prices) > 0:
74
+ transport_prices = Pricer.convert(self._pricing.transport_prices, (price_unit, quantities.base_unit))
75
+ else:
76
+ transport_prices = None
77
+
78
+ if self._pricing.energy_taxes is not None and len(self._pricing.energy_taxes) > 0:
79
+ energy_taxes = Pricer.convert(self._pricing.energy_taxes, (price_unit, quantities.value_unit))
80
+ else:
81
+ energy_taxes = None
82
+
83
+ # Transform to the vectorized form.
84
+ if self._pricing.vat is not None and len(self._pricing.vat) > 0:
85
+ vat_rate_array_by_id = self.get_vat_rate_array_by_id(
86
+ start_date=start_date, end_date=end_date, vat_rates=self._pricing.vat
87
+ )
88
+ else:
89
+ vat_rate_array_by_id = dict[str, VatRateArray]()
90
+
91
+ consumption_price_array = self.get_consumption_price_array(
92
+ start_date=start_date,
93
+ end_date=end_date,
94
+ consumption_prices=consumption_prices,
95
+ vat_rate_array_by_id=vat_rate_array_by_id,
96
+ )
97
+
98
+ # Subscription price is optional.
99
+ if subscription_prices is not None and len(subscription_prices) > 0:
100
+ subscription_price_array = self.get_subscription_price_array(
101
+ start_date=start_date,
102
+ end_date=end_date,
103
+ subscription_prices=subscription_prices,
104
+ vat_rate_array_by_id=vat_rate_array_by_id,
105
+ )
106
+ else:
107
+ subscription_price_array = SubscriptionPriceArray(
108
+ start_date=start_date,
109
+ end_date=end_date,
110
+ value_unit=price_unit,
111
+ base_unit=quantities.base_unit,
112
+ )
113
+
114
+ # Transport price is optional.
115
+ if transport_prices is not None and len(transport_prices) > 0:
116
+ transport_price_array = self.get_transport_price_array(
117
+ start_date=start_date,
118
+ end_date=end_date,
119
+ transport_prices=transport_prices,
120
+ vat_rate_array_by_id=vat_rate_array_by_id,
121
+ )
122
+ else:
123
+ transport_price_array = TransportPriceArray(
124
+ start_date=start_date,
125
+ end_date=end_date,
126
+ value_unit=price_unit,
127
+ base_unit=quantities.base_unit,
128
+ )
129
+
130
+ # Energy taxes are optional.
131
+ if energy_taxes is not None and len(energy_taxes) > 0:
132
+ energy_taxes_price_array = self.get_energy_taxes_price_array(
133
+ start_date=start_date,
134
+ end_date=end_date,
135
+ energy_taxes_prices=energy_taxes,
136
+ vat_rate_array_by_id=vat_rate_array_by_id,
137
+ )
138
+ else:
139
+ energy_taxes_price_array = EnergyTaxesPriceArray(
140
+ start_date=start_date,
141
+ end_date=end_date,
142
+ value_unit=price_unit,
143
+ base_unit=quantities.value_unit,
144
+ )
145
+
146
+ res = CostArray(
147
+ start_date=start_date,
148
+ end_date=end_date,
149
+ value_unit=price_unit,
150
+ base_unit=quantities.base_unit,
151
+ )
152
+
153
+ # Compute pricing formula
154
+ res.value_array = quantity_array * (consumption_price_array.value_array + energy_taxes_price_array.value_array) + subscription_price_array.value_array + transport_price_array.value_array # type: ignore
155
+
156
+ return res
157
+
158
+ # ----------------------------------
159
+ @classmethod
160
+ def get_vat_rate_array_by_id(
161
+ cls, start_date: date, end_date: date, vat_rates: list[VatRate]
162
+ ) -> dict[str, VatRateArray]:
163
+
164
+ if vat_rates is None or len(vat_rates) == 0:
165
+ raise ValueError("vat_rates is None or empty")
166
+
167
+ res = dict[str, VatRateArray]()
168
+ vat_rate_by_id = dict[str, list[VatRate]]()
169
+ for vat_rate in vat_rates:
170
+ res[vat_rate.id] = VatRateArray(id=vat_rate.id, start_date=start_date, end_date=end_date)
171
+ if vat_rate.id not in vat_rate_by_id:
172
+ vat_rate_by_id[vat_rate.id] = list[VatRate]()
173
+ vat_rate_by_id[vat_rate.id].append(vat_rate)
174
+
175
+ for vat_id, vat_rate_list in vat_rate_by_id.items():
176
+ cls._fill_value_array(res[vat_id], vat_rate_list) # type: ignore
177
+
178
+ return res
179
+
180
+ # ----------------------------------
181
+ @classmethod
182
+ def get_consumption_price_array(
183
+ cls,
184
+ start_date: date,
185
+ end_date: date,
186
+ consumption_prices: list[PriceValue[PriceUnit, QuantityUnit]],
187
+ vat_rate_array_by_id: dict[str, VatRateArray],
188
+ ) -> ConsumptionPriceArray:
189
+
190
+ if consumption_prices is None or len(consumption_prices) == 0:
191
+ raise ValueError("consumption_prices is None or empty")
192
+
193
+ first_consumption_price = consumption_prices[0]
194
+
195
+ res = ConsumptionPriceArray(
196
+ start_date=start_date,
197
+ end_date=end_date,
198
+ value_unit=first_consumption_price.value_unit,
199
+ base_unit=first_consumption_price.base_unit,
200
+ vat_id=first_consumption_price.vat_id,
201
+ )
202
+
203
+ cls._fill_price_array(res, consumption_prices, vat_rate_array_by_id) # type: ignore
204
+
205
+ return res
206
+
207
+ # ----------------------------------
208
+ @classmethod
209
+ def get_subscription_price_array(
210
+ cls,
211
+ start_date: date,
212
+ end_date: date,
213
+ subscription_prices: list[PriceValue[PriceUnit, TimeUnit]],
214
+ vat_rate_array_by_id: dict[str, VatRateArray],
215
+ ) -> SubscriptionPriceArray:
216
+
217
+ if subscription_prices is None or len(subscription_prices) == 0:
218
+ raise ValueError("subscription_prices is None or empty")
219
+
220
+ first_subscription_price = subscription_prices[0]
221
+
222
+ res = SubscriptionPriceArray(
223
+ start_date=start_date,
224
+ end_date=end_date,
225
+ value_unit=first_subscription_price.value_unit,
226
+ base_unit=first_subscription_price.base_unit,
227
+ vat_id=first_subscription_price.vat_id,
228
+ )
229
+
230
+ cls._fill_price_array(res, subscription_prices, vat_rate_array_by_id) # type: ignore
231
+
232
+ return res
233
+
234
+ # ----------------------------------
235
+ @classmethod
236
+ def get_transport_price_array(
237
+ cls,
238
+ start_date: date,
239
+ end_date: date,
240
+ transport_prices: list[PriceValue[PriceUnit, TimeUnit]],
241
+ vat_rate_array_by_id: dict[str, VatRateArray],
242
+ ) -> TransportPriceArray:
243
+
244
+ if transport_prices is None or len(transport_prices) == 0:
245
+ raise ValueError("transport_prices is None or empty")
246
+
247
+ first_transport_price = transport_prices[0]
248
+
249
+ res = TransportPriceArray(
250
+ start_date=start_date,
251
+ end_date=end_date,
252
+ value_unit=first_transport_price.value_unit,
253
+ base_unit=first_transport_price.base_unit,
254
+ vat_id=first_transport_price.vat_id,
255
+ )
256
+
257
+ cls._fill_price_array(res, transport_prices, vat_rate_array_by_id) # type: ignore
258
+
259
+ return res
260
+
261
+ # ----------------------------------
262
+ @classmethod
263
+ def get_energy_taxes_price_array(
264
+ cls,
265
+ start_date: date,
266
+ end_date: date,
267
+ energy_taxes_prices: list[PriceValue[PriceUnit, QuantityUnit]],
268
+ vat_rate_array_by_id: dict[str, VatRateArray],
269
+ ) -> EnergyTaxesPriceArray:
270
+
271
+ if energy_taxes_prices is None or len(energy_taxes_prices) == 0:
272
+ raise ValueError("energy_taxes_prices is None or empty")
273
+
274
+ first_energy_taxes_price = energy_taxes_prices[0]
275
+
276
+ res = EnergyTaxesPriceArray(
277
+ start_date=start_date,
278
+ end_date=end_date,
279
+ value_unit=first_energy_taxes_price.value_unit,
280
+ base_unit=first_energy_taxes_price.base_unit,
281
+ vat_id=first_energy_taxes_price.vat_id,
282
+ )
283
+
284
+ cls._fill_price_array(res, energy_taxes_prices, vat_rate_array_by_id) # type: ignore
285
+
286
+ return res
287
+
288
+ # ----------------------------------
289
+ @classmethod
290
+ def _fill_value_array(cls, out_value_array: ValueArray, in_values: list[Value]) -> None:
291
+
292
+ if out_value_array is None:
293
+ raise ValueError("out_value_array is None")
294
+
295
+ if out_value_array.start_date is None:
296
+ raise ValueError("out_value_array.start_date is None")
297
+
298
+ start_date = out_value_array.start_date
299
+
300
+ if out_value_array.end_date is None:
301
+ raise ValueError("out_value_array.end_date is None")
302
+
303
+ end_date = out_value_array.end_date
304
+
305
+ if out_value_array.value_array is None:
306
+ raise ValueError("out_value_array.value_array is None")
307
+
308
+ value_array = out_value_array.value_array
309
+
310
+ if in_values is None or len(in_values) == 0:
311
+ raise ValueError("in_values is None or empty")
312
+
313
+ first_value = in_values[0]
314
+ last_value = in_values[-1]
315
+
316
+ if first_value.start_date > end_date:
317
+ # Fully before first value period.
318
+ value_array[start_date:end_date] = first_value.value # type: ignore
319
+ elif last_value.end_date is not None and last_value.end_date < start_date:
320
+ # Fully after last value period.
321
+ value_array[start_date:end_date] = last_value.value # type: ignore
322
+ else:
323
+ if start_date < first_value.start_date:
324
+ # Partially before first value period.
325
+ value_array[start_date : first_value.start_date] = first_value.value # type: ignore
326
+ if last_value.end_date is not None and end_date > last_value.end_date:
327
+ # Partially after last value period.
328
+ value_array[last_value.end_date : end_date] = last_value.value # type: ignore
329
+ # Inside value periods.
330
+ for value in in_values:
331
+ latest_start = max(value.start_date, start_date)
332
+ earliest_end = min(value.end_date if value.end_date is not None else end_date, end_date)
333
+ current_date = latest_start
334
+ while current_date <= earliest_end:
335
+ value_array[current_date] = value.value
336
+ current_date += timedelta(days=1)
337
+
338
+ # ----------------------------------
339
+ @classmethod
340
+ def _fill_price_array( # pylint: disable=too-many-branches
341
+ cls,
342
+ out_value_array: ValueArray,
343
+ in_values: list[PriceValue],
344
+ vat_rate_array_by_id: dict[str, VatRateArray],
345
+ ) -> None:
346
+
347
+ if out_value_array is None:
348
+ raise ValueError("out_value_array is None")
349
+
350
+ if out_value_array.start_date is None:
351
+ raise ValueError("out_value_array.start_date is None")
352
+
353
+ start_date = out_value_array.start_date
354
+
355
+ if out_value_array.end_date is None:
356
+ raise ValueError("out_value_array.end_date is None")
357
+
358
+ end_date = out_value_array.end_date
359
+
360
+ if out_value_array.value_array is None:
361
+ raise ValueError("out_value_array.value_array is None")
362
+
363
+ value_array = out_value_array.value_array
364
+
365
+ if in_values is None or len(in_values) == 0:
366
+ raise ValueError("in_values is None or empty")
367
+
368
+ first_value = in_values[0]
369
+ last_value = in_values[-1]
370
+
371
+ if first_value.start_date > end_date:
372
+ # Fully before first value period.
373
+ if vat_rate_array_by_id is not None and first_value.vat_id in vat_rate_array_by_id:
374
+ vat_value = vat_rate_array_by_id[first_value.vat_id].value_array[start_date:end_date] # type: ignore
375
+ else:
376
+ vat_value = 0.0
377
+ value_array[start_date:end_date] = first_value.value * (1 + vat_value) # type: ignore
378
+ elif last_value.end_date is not None and last_value.end_date < start_date:
379
+ # Fully after last value period.
380
+ if vat_rate_array_by_id is not None and last_value.vat_id in vat_rate_array_by_id:
381
+ vat_value = vat_rate_array_by_id[last_value.vat_id].value_array[start_date:end_date] # type: ignore
382
+ else:
383
+ vat_value = 0.0
384
+ value_array[start_date:end_date] = last_value.value * (1 + vat_value) # type: ignore
385
+ else:
386
+ if start_date < first_value.start_date:
387
+ # Partially before first value period.
388
+ if vat_rate_array_by_id is not None and first_value.vat_id in vat_rate_array_by_id:
389
+ vat_value = vat_rate_array_by_id[first_value.vat_id].value_array[start_date : first_value.start_date] # type: ignore
390
+ else:
391
+ vat_value = 0.0
392
+ value_array[start_date : first_value.start_date] = first_value.value * (1 + vat_value) # type: ignore
393
+ if last_value.end_date is not None and end_date > last_value.end_date:
394
+ # Partially after last value period.
395
+ if vat_rate_array_by_id is not None and last_value.vat_id in vat_rate_array_by_id:
396
+ vat_value = vat_rate_array_by_id[last_value.vat_id].value_array[last_value.end_date : end_date] # type: ignore
397
+ else:
398
+ vat_value = 0.0
399
+ value_array[last_value.end_date : end_date] = last_value.value * (1 + vat_value) # type: ignore
400
+ # Inside value periods.
401
+ for value in in_values:
402
+ latest_start = max(value.start_date, start_date)
403
+ earliest_end = min(value.end_date if value.end_date is not None else end_date, end_date)
404
+ current_date = latest_start
405
+ while current_date <= earliest_end:
406
+ if vat_rate_array_by_id is not None and value.vat_id in vat_rate_array_by_id:
407
+ vat_value = vat_rate_array_by_id[value.vat_id].value_array[current_date] # type: ignore
408
+ else:
409
+ vat_value = 0.0
410
+ value_array[current_date] = value.value * (1 + vat_value) # type: ignore
411
+ current_date += timedelta(days=1)
412
+
413
+ # ----------------------------------
414
+ @classmethod
415
+ def get_time_unit_convertion_factor(cls, from_time_unit: TimeUnit, to_time_unit: TimeUnit, dt: date) -> float:
416
+
417
+ if from_time_unit == to_time_unit:
418
+ return 1.0
419
+
420
+ def days_in_month(year: int, month: int) -> int:
421
+ return calendar.monthrange(year, month)[1]
422
+
423
+ def days_in_year(year: int) -> int:
424
+ return 366 if calendar.isleap(year) else 365
425
+
426
+ if TimeUnit.MONTH in (from_time_unit, to_time_unit):
427
+ switcher = {
428
+ TimeUnit.DAY: days_in_month(dt.year, dt.month),
429
+ TimeUnit.WEEK: days_in_month(dt.year, dt.month) / 7.0,
430
+ TimeUnit.MONTH: 1.0,
431
+ TimeUnit.YEAR: 1.0 / 12.0,
432
+ }
433
+ else:
434
+ switcher = {
435
+ TimeUnit.DAY: 1.0,
436
+ TimeUnit.WEEK: 1 / 7.0,
437
+ TimeUnit.MONTH: 1 / days_in_month(dt.year, dt.month),
438
+ TimeUnit.YEAR: 1 / days_in_year(dt.year),
439
+ }
440
+
441
+ if from_time_unit not in switcher:
442
+ raise ValueError(f"Invalid 'from' time unit: {from_time_unit}")
443
+
444
+ if to_time_unit not in switcher:
445
+ raise ValueError(f"Invalid 'to' time unit: {to_time_unit}")
446
+
447
+ return switcher[to_time_unit] / switcher[from_time_unit]
448
+
449
+ # ----------------------------------
450
+ @classmethod
451
+ def get_price_unit_convertion_factor(cls, from_price_unit: PriceUnit, to_price_unit: PriceUnit) -> float:
452
+
453
+ if from_price_unit == to_price_unit:
454
+ return 1.0
455
+
456
+ switcher = {
457
+ PriceUnit.EURO: 1.0,
458
+ PriceUnit.CENT: 100.0,
459
+ }
460
+
461
+ if from_price_unit not in switcher:
462
+ raise ValueError(f"Invalid 'from' price unit: {from_price_unit}")
463
+
464
+ if to_price_unit not in switcher:
465
+ raise ValueError(f"Invalid 'to' price unit: {to_price_unit}")
466
+
467
+ return switcher[to_price_unit] / switcher[from_price_unit]
468
+
469
+ # ----------------------------------
470
+ @classmethod
471
+ def get_quantity_unit_convertion_factor(
472
+ cls, from_quantity_unit: QuantityUnit, to_quantity_unit: QuantityUnit
473
+ ) -> float:
474
+
475
+ if from_quantity_unit == to_quantity_unit:
476
+ return 1.0
477
+
478
+ switcher = {
479
+ QuantityUnit.WH: 1.0,
480
+ QuantityUnit.KWH: 0.001,
481
+ QuantityUnit.MWH: 0.000001,
482
+ }
483
+
484
+ if from_quantity_unit not in switcher:
485
+ raise ValueError(f"Invalid 'from' quantity unit: {from_quantity_unit}")
486
+
487
+ if to_quantity_unit not in switcher:
488
+ raise ValueError(f"Invalid 'to' quantity unit: {to_quantity_unit}")
489
+
490
+ return switcher[to_quantity_unit] / switcher[from_quantity_unit]
491
+
492
+ # ----------------------------------
493
+ @overload
494
+ @classmethod
495
+ def get_convertion_factor(
496
+ cls,
497
+ from_unit: Tuple[PriceUnit, QuantityUnit],
498
+ to_unit: Tuple[PriceUnit, QuantityUnit],
499
+ dt: Optional[date] = None,
500
+ ) -> float: ...
501
+
502
+ @overload
503
+ @classmethod
504
+ def get_convertion_factor(
505
+ cls,
506
+ from_unit: Tuple[PriceUnit, TimeUnit],
507
+ to_unit: Tuple[PriceUnit, TimeUnit],
508
+ dt: Optional[date] = None,
509
+ ) -> float: ...
510
+
511
+ @classmethod
512
+ def get_convertion_factor(cls, from_unit, to_unit, dt: Optional[date] = None) -> float:
513
+ if type(from_unit) is not type(to_unit):
514
+ raise ValueError(f"from_unit {from_unit} and to_unit {to_unit} must be of the same type")
515
+ if (
516
+ isinstance(from_unit, tuple)
517
+ and isinstance(from_unit[0], PriceUnit)
518
+ and isinstance(from_unit[1], QuantityUnit)
519
+ ):
520
+ return cls.get_price_unit_convertion_factor(
521
+ from_unit[0], to_unit[0]
522
+ ) / cls.get_quantity_unit_convertion_factor(from_unit[1], to_unit[1])
523
+ if isinstance(from_unit, tuple) and isinstance(from_unit[0], PriceUnit) and isinstance(from_unit[1], TimeUnit):
524
+ if dt is None:
525
+ raise ValueError(
526
+ f"dt must not be None when from_unit {from_unit} and to_unit {to_unit} are of type Tuple[PriceUnit, TimeUnit]"
527
+ )
528
+ return cls.get_price_unit_convertion_factor(from_unit[0], to_unit[0]) / cls.get_time_unit_convertion_factor(
529
+ from_unit[1], to_unit[1], dt
530
+ )
531
+
532
+ raise ValueError(
533
+ f"from_unit {from_unit} and to_unit {to_unit} must be of type Tuple[PriceUnit, QuantityUnit] or Tuple[PriceUnit, TimeUnit]"
534
+ )
535
+
536
+ # ----------------------------------
537
+ @classmethod
538
+ def convert(
539
+ cls,
540
+ price_values: list[PriceValue[ValueUnit, BaseUnit]],
541
+ to_unit: Tuple[ValueUnit, BaseUnit],
542
+ ) -> list[PriceValue[ValueUnit, BaseUnit]]:
543
+
544
+ if price_values is None or len(price_values) == 0:
545
+ raise ValueError("price_values is None or empty")
546
+
547
+ if to_unit is None:
548
+ raise ValueError("to_unit is None")
549
+
550
+ res = list[PriceValue[ValueUnit, BaseUnit]]()
551
+ for price_value in price_values:
552
+ if price_value.value_unit is None:
553
+ raise ValueError("price_value.value_unit is None")
554
+ if price_value.base_unit is None:
555
+ raise ValueError("price_value.base_unit is None")
556
+
557
+ res.append(
558
+ PriceValue(
559
+ start_date=price_value.start_date,
560
+ end_date=price_value.end_date,
561
+ value=price_value.value
562
+ * cls.get_convertion_factor(
563
+ (price_value.value_unit, price_value.base_unit), to_unit, price_value.start_date # type: ignore
564
+ ),
565
+ value_unit=to_unit[0],
566
+ base_unit=to_unit[1],
567
+ vat_id=price_value.vat_id,
568
+ )
569
+ )
570
+
571
+ return res