pygazpar 0.1.21__py3-none-any.whl → 1.3.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pygazpar/datasource.py ADDED
@@ -0,0 +1,590 @@
1
+ import glob
2
+ import json
3
+ import logging
4
+ import os
5
+ from abc import ABC, abstractmethod
6
+ from datetime import date, timedelta
7
+ from typing import Any, Optional, cast
8
+
9
+ import pandas as pd
10
+
11
+ from pygazpar.api_client import APIClient, ConsumptionType
12
+ from pygazpar.api_client import Frequency as APIClientFrequency
13
+ from pygazpar.enum import Frequency, PropertyName
14
+ from pygazpar.excelparser import ExcelParser
15
+ from pygazpar.jsonparser import JsonParser
16
+
17
+ Logger = logging.getLogger(__name__)
18
+
19
+ MeterReading = dict[str, Any]
20
+
21
+ MeterReadings = list[MeterReading]
22
+
23
+ MeterReadingsByFrequency = dict[str, MeterReadings]
24
+
25
+
26
+ # ------------------------------------------------------------------------------------------------------------
27
+ class IDataSource(ABC): # pylint: disable=too-few-public-methods
28
+
29
+ @abstractmethod
30
+ def login(self):
31
+ pass
32
+
33
+ @abstractmethod
34
+ def logout(self):
35
+ pass
36
+
37
+ @abstractmethod
38
+ def get_pce_identifiers(self) -> list[str]:
39
+ pass
40
+
41
+ @abstractmethod
42
+ def load(
43
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
44
+ ) -> MeterReadingsByFrequency:
45
+ pass
46
+
47
+
48
+ # ------------------------------------------------------------------------------------------------------------
49
+ class WebDataSource(IDataSource): # pylint: disable=too-few-public-methods
50
+
51
+ # ------------------------------------------------------
52
+ def __init__(self, username: str, password: str):
53
+
54
+ self._api_client = APIClient(username, password)
55
+
56
+ # ------------------------------------------------------
57
+ def login(self):
58
+
59
+ if not self._api_client.is_logged_in():
60
+ self._api_client.login()
61
+
62
+ # ------------------------------------------------------
63
+ def logout(self):
64
+
65
+ if self._api_client.is_logged_in():
66
+ self._api_client.logout()
67
+
68
+ # ------------------------------------------------------
69
+ def get_pce_identifiers(self) -> list[str]:
70
+
71
+ if not self._api_client.is_logged_in():
72
+ self._api_client.login()
73
+
74
+ pce_list = self._api_client.get_pce_list()
75
+
76
+ if pce_list is None:
77
+ return []
78
+
79
+ return [pce["idObject"] for pce in pce_list]
80
+
81
+ # ------------------------------------------------------
82
+ def load(
83
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
84
+ ) -> MeterReadingsByFrequency:
85
+
86
+ if not self._api_client.is_logged_in():
87
+ self._api_client.login()
88
+
89
+ res = self._loadFromSession(pceIdentifier, startDate, endDate, frequencies)
90
+
91
+ Logger.debug("The data update terminates normally")
92
+
93
+ return res
94
+
95
+ @abstractmethod
96
+ def _loadFromSession(
97
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
98
+ ) -> MeterReadingsByFrequency:
99
+ pass
100
+
101
+
102
+ # ------------------------------------------------------------------------------------------------------------
103
+ class ExcelWebDataSource(WebDataSource): # pylint: disable=too-few-public-methods
104
+
105
+ DATE_FORMAT = "%Y-%m-%d"
106
+
107
+ FREQUENCY_VALUES = {
108
+ Frequency.HOURLY: "Horaire",
109
+ Frequency.DAILY: "Journalier",
110
+ Frequency.WEEKLY: "Hebdomadaire",
111
+ Frequency.MONTHLY: "Mensuel",
112
+ Frequency.YEARLY: "Journalier",
113
+ }
114
+
115
+ DATA_FILENAME = "Donnees_informatives_*.xlsx"
116
+
117
+ # ------------------------------------------------------
118
+ def __init__(self, username: str, password: str, tmpDirectory: str):
119
+
120
+ super().__init__(username, password)
121
+
122
+ self.__tmpDirectory = tmpDirectory
123
+
124
+ # ------------------------------------------------------
125
+ def _loadFromSession( # pylint: disable=too-many-branches
126
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
127
+ ) -> MeterReadingsByFrequency: # pylint: disable=too-many-branches
128
+
129
+ res = {}
130
+
131
+ # XLSX is in the TMP directory
132
+ data_file_path_pattern = self.__tmpDirectory + "/" + ExcelWebDataSource.DATA_FILENAME
133
+
134
+ # We remove an eventual existing data file (from a previous run that has not deleted it).
135
+ file_list = glob.glob(data_file_path_pattern)
136
+ for filename in file_list:
137
+ if os.path.isfile(filename):
138
+ try:
139
+ os.remove(filename)
140
+ except PermissionError:
141
+ pass
142
+
143
+ if frequencies is None:
144
+ # Transform Enum in List.
145
+ frequencyList = list(Frequency)
146
+ else:
147
+ # Get distinct values.
148
+ frequencyList = list(set(frequencies))
149
+
150
+ for frequency in frequencyList:
151
+
152
+ Logger.debug(
153
+ f"Loading data of frequency {ExcelWebDataSource.FREQUENCY_VALUES[frequency]} from {startDate.strftime(ExcelWebDataSource.DATE_FORMAT)} to {endDate.strftime(ExcelWebDataSource.DATE_FORMAT)}"
154
+ )
155
+
156
+ response = self._api_client.get_pce_consumption_excelsheet(
157
+ ConsumptionType.INFORMATIVE,
158
+ startDate,
159
+ endDate,
160
+ APIClientFrequency(ExcelWebDataSource.FREQUENCY_VALUES[frequency]),
161
+ [pceIdentifier],
162
+ )
163
+
164
+ filename = response["filename"]
165
+ content = response["content"]
166
+
167
+ with open(f"{self.__tmpDirectory}/{filename}", "wb") as file:
168
+ file.write(content)
169
+
170
+ # Load the XLSX file into the data structure
171
+ file_list = glob.glob(data_file_path_pattern)
172
+
173
+ if len(file_list) == 0:
174
+ Logger.warning(f"Not any data file has been found in '{self.__tmpDirectory}' directory")
175
+
176
+ for filename in file_list:
177
+ res[frequency.value] = ExcelParser.parse(
178
+ filename, frequency if frequency != Frequency.YEARLY else Frequency.DAILY
179
+ )
180
+ try:
181
+ # openpyxl does not close the file properly.
182
+ os.remove(filename)
183
+ except PermissionError:
184
+ pass
185
+
186
+ # We compute yearly from daily data.
187
+ if frequency == Frequency.YEARLY:
188
+ res[frequency.value] = FrequencyConverter.computeYearly(res[frequency.value])
189
+
190
+ return res
191
+
192
+
193
+ # ------------------------------------------------------------------------------------------------------------
194
+ class ExcelFileDataSource(IDataSource): # pylint: disable=too-few-public-methods
195
+
196
+ def __init__(self, excelFile: str):
197
+
198
+ self.__excelFile = excelFile
199
+
200
+ # ------------------------------------------------------
201
+ def login(self):
202
+ pass
203
+
204
+ # ------------------------------------------------------
205
+ def logout(self):
206
+ pass
207
+
208
+ # ------------------------------------------------------
209
+ def get_pce_identifiers(self) -> list[str]:
210
+
211
+ return ["0123456789"]
212
+
213
+ # ------------------------------------------------------
214
+ def load(
215
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
216
+ ) -> MeterReadingsByFrequency:
217
+
218
+ res = {}
219
+
220
+ if frequencies is None:
221
+ # Transform Enum in List.
222
+ frequencyList = list(Frequency)
223
+ else:
224
+ # Get unique values.
225
+ frequencyList = list(set(frequencies))
226
+
227
+ for frequency in frequencyList:
228
+ if frequency != Frequency.YEARLY:
229
+ res[frequency.value] = ExcelParser.parse(self.__excelFile, frequency)
230
+ else:
231
+ daily = ExcelParser.parse(self.__excelFile, Frequency.DAILY)
232
+ res[frequency.value] = FrequencyConverter.computeYearly(daily)
233
+
234
+ return res
235
+
236
+
237
+ # ------------------------------------------------------------------------------------------------------------
238
+ class JsonWebDataSource(WebDataSource): # pylint: disable=too-few-public-methods
239
+
240
+ INPUT_DATE_FORMAT = "%Y-%m-%d"
241
+
242
+ OUTPUT_DATE_FORMAT = "%d/%m/%Y"
243
+
244
+ # ------------------------------------------------------
245
+ def _loadFromSession(
246
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
247
+ ) -> MeterReadingsByFrequency:
248
+
249
+ res = dict[str, Any]()
250
+
251
+ computeByFrequency = {
252
+ Frequency.HOURLY: FrequencyConverter.computeHourly,
253
+ Frequency.DAILY: FrequencyConverter.computeDaily,
254
+ Frequency.WEEKLY: FrequencyConverter.computeWeekly,
255
+ Frequency.MONTHLY: FrequencyConverter.computeMonthly,
256
+ Frequency.YEARLY: FrequencyConverter.computeYearly,
257
+ }
258
+
259
+ data = self._api_client.get_pce_consumption(ConsumptionType.INFORMATIVE, startDate, endDate, [pceIdentifier])
260
+
261
+ Logger.debug("Json meter data: %s", data)
262
+
263
+ # Temperatures URL: Inject parameters.
264
+ endDate = date.today() - timedelta(days=1) if endDate >= date.today() else endDate
265
+ days = max(
266
+ min((endDate - startDate).days, 730), 10
267
+ ) # At least 10 days, at most 730 days, to avoid HTTP 500 error.
268
+
269
+ # Get weather data.
270
+ try:
271
+ temperatures = self._api_client.get_pce_meteo(endDate, days, pceIdentifier)
272
+ except Exception: # pylint: disable=broad-except
273
+ # Not a blocking error.
274
+ temperatures = None
275
+
276
+ Logger.debug("Json temperature data: %s", temperatures)
277
+
278
+ # Transform all the data into the target structure.
279
+ if data is None or len(data) == 0:
280
+ return res
281
+
282
+ daily = JsonParser.parse(json.dumps(data), json.dumps(temperatures), pceIdentifier)
283
+
284
+ Logger.debug("Processed daily data: %s", daily)
285
+
286
+ if frequencies is None:
287
+ # Transform Enum in List.
288
+ frequencyList = list(Frequency)
289
+ else:
290
+ # Get unique values.
291
+ frequencyList = list(set(frequencies))
292
+
293
+ for frequency in frequencyList:
294
+ res[frequency.value] = computeByFrequency[frequency](daily)
295
+
296
+ return res
297
+
298
+
299
+ # ------------------------------------------------------------------------------------------------------------
300
+ class JsonFileDataSource(IDataSource): # pylint: disable=too-few-public-methods
301
+
302
+ # ------------------------------------------------------
303
+ def __init__(self, consumptionJsonFile: str, temperatureJsonFile):
304
+
305
+ self.__consumptionJsonFile = consumptionJsonFile
306
+ self.__temperatureJsonFile = temperatureJsonFile
307
+
308
+ # ------------------------------------------------------
309
+ def login(self):
310
+ pass
311
+
312
+ # ------------------------------------------------------
313
+ def logout(self):
314
+ pass
315
+
316
+ # ------------------------------------------------------
317
+ def get_pce_identifiers(self) -> list[str]:
318
+
319
+ return ["0123456789"]
320
+
321
+ # ------------------------------------------------------
322
+ def load(
323
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
324
+ ) -> MeterReadingsByFrequency:
325
+
326
+ res = {}
327
+
328
+ with open(self.__consumptionJsonFile, mode="r", encoding="utf-8") as consumptionJsonFile:
329
+ with open(self.__temperatureJsonFile, mode="r", encoding="utf-8") as temperatureJsonFile:
330
+ daily = JsonParser.parse(consumptionJsonFile.read(), temperatureJsonFile.read(), pceIdentifier)
331
+
332
+ computeByFrequency = {
333
+ Frequency.HOURLY: FrequencyConverter.computeHourly,
334
+ Frequency.DAILY: FrequencyConverter.computeDaily,
335
+ Frequency.WEEKLY: FrequencyConverter.computeWeekly,
336
+ Frequency.MONTHLY: FrequencyConverter.computeMonthly,
337
+ Frequency.YEARLY: FrequencyConverter.computeYearly,
338
+ }
339
+
340
+ if frequencies is None:
341
+ # Transform Enum in List.
342
+ frequencyList = list(Frequency)
343
+ else:
344
+ # Get unique values.
345
+ frequencyList = list(set(frequencies))
346
+
347
+ for frequency in frequencyList:
348
+ res[frequency.value] = computeByFrequency[frequency](daily)
349
+
350
+ return res
351
+
352
+
353
+ # ------------------------------------------------------------------------------------------------------------
354
+ class TestDataSource(IDataSource): # pylint: disable=too-few-public-methods
355
+
356
+ __test__ = False # Will not be discovered as a test
357
+
358
+ # ------------------------------------------------------
359
+ def __init__(self):
360
+
361
+ pass
362
+
363
+ # ------------------------------------------------------
364
+ def login(self):
365
+ pass
366
+
367
+ # ------------------------------------------------------
368
+ def logout(self):
369
+ pass
370
+
371
+ # ------------------------------------------------------
372
+ def get_pce_identifiers(self) -> list[str]:
373
+
374
+ return ["0123456789"]
375
+
376
+ # ------------------------------------------------------
377
+ def load(
378
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
379
+ ) -> MeterReadingsByFrequency:
380
+
381
+ res = dict[str, Any]()
382
+
383
+ dataSampleFilenameByFrequency = {
384
+ Frequency.HOURLY: "hourly_data_sample.json",
385
+ Frequency.DAILY: "daily_data_sample.json",
386
+ Frequency.WEEKLY: "weekly_data_sample.json",
387
+ Frequency.MONTHLY: "monthly_data_sample.json",
388
+ Frequency.YEARLY: "yearly_data_sample.json",
389
+ }
390
+
391
+ if frequencies is None:
392
+ # Transform Enum in List.
393
+ frequencyList = list(Frequency)
394
+ else:
395
+ # Get unique values.
396
+ frequencyList = list(set(frequencies))
397
+
398
+ for frequency in frequencyList:
399
+ dataSampleFilename = (
400
+ f"{os.path.dirname(os.path.abspath(__file__))}/resources/{dataSampleFilenameByFrequency[frequency]}"
401
+ )
402
+
403
+ with open(dataSampleFilename, mode="r", encoding="utf-8") as jsonFile:
404
+ res[frequency.value] = cast(list[dict[PropertyName, Any]], json.load(jsonFile))
405
+
406
+ return res
407
+
408
+
409
+ # ------------------------------------------------------------------------------------------------------------
410
+ class FrequencyConverter:
411
+
412
+ MONTHS = [
413
+ "Janvier",
414
+ "Février",
415
+ "Mars",
416
+ "Avril",
417
+ "Mai",
418
+ "Juin",
419
+ "Juillet",
420
+ "Août",
421
+ "Septembre",
422
+ "Octobre",
423
+ "Novembre",
424
+ "Décembre",
425
+ ]
426
+
427
+ # ------------------------------------------------------
428
+ @staticmethod
429
+ def computeHourly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]: # pylint: disable=unused-argument
430
+
431
+ return []
432
+
433
+ # ------------------------------------------------------
434
+ @staticmethod
435
+ def computeDaily(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
436
+
437
+ return daily
438
+
439
+ # ------------------------------------------------------
440
+ @staticmethod
441
+ def computeWeekly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
442
+
443
+ df = pd.DataFrame(daily)
444
+
445
+ # Trimming head and trailing spaces and convert to datetime.
446
+ df["date_time"] = pd.to_datetime(df["time_period"].str.strip(), format=JsonWebDataSource.OUTPUT_DATE_FORMAT)
447
+
448
+ # Get the first day of week.
449
+ df["first_day_of_week"] = pd.to_datetime(df["date_time"].dt.strftime("%W %Y 1"), format="%W %Y %w")
450
+
451
+ # Get the last day of week.
452
+ df["last_day_of_week"] = pd.to_datetime(df["date_time"].dt.strftime("%W %Y 0"), format="%W %Y %w")
453
+
454
+ # Reformat the time period.
455
+ df["time_period"] = (
456
+ "Du "
457
+ + df["first_day_of_week"].dt.strftime(JsonWebDataSource.OUTPUT_DATE_FORMAT).astype(str)
458
+ + " au "
459
+ + df["last_day_of_week"].dt.strftime(JsonWebDataSource.OUTPUT_DATE_FORMAT).astype(str)
460
+ )
461
+
462
+ # Aggregate rows by month_year.
463
+ df = (
464
+ df[
465
+ [
466
+ "first_day_of_week",
467
+ "time_period",
468
+ "start_index_m3",
469
+ "end_index_m3",
470
+ "volume_m3",
471
+ "energy_kwh",
472
+ "timestamp",
473
+ ]
474
+ ]
475
+ .groupby("time_period")
476
+ .agg(
477
+ first_day_of_week=("first_day_of_week", "min"),
478
+ start_index_m3=("start_index_m3", "min"),
479
+ end_index_m3=("end_index_m3", "max"),
480
+ volume_m3=("volume_m3", "sum"),
481
+ energy_kwh=("energy_kwh", "sum"),
482
+ timestamp=("timestamp", "min"),
483
+ count=("energy_kwh", "count"),
484
+ )
485
+ .reset_index()
486
+ )
487
+
488
+ # Sort rows by month ascending.
489
+ df = df.sort_values(by=["first_day_of_week"])
490
+
491
+ # Select rows where we have a full week (7 days) except for the current week.
492
+ df = pd.concat([df[(df["count"] >= 7)], df.tail(1)[df.tail(1)["count"] < 7]])
493
+
494
+ # Select target columns.
495
+ df = df[["time_period", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
496
+
497
+ res = cast(list[dict[str, Any]], df.to_dict("records"))
498
+
499
+ return res
500
+
501
+ # ------------------------------------------------------
502
+ @staticmethod
503
+ def computeMonthly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
504
+
505
+ df = pd.DataFrame(daily)
506
+
507
+ # Trimming head and trailing spaces and convert to datetime.
508
+ df["date_time"] = pd.to_datetime(df["time_period"].str.strip(), format=JsonWebDataSource.OUTPUT_DATE_FORMAT)
509
+
510
+ # Get the corresponding month-year.
511
+ df["month_year"] = (
512
+ df["date_time"].apply(lambda x: FrequencyConverter.MONTHS[x.month - 1]).astype(str)
513
+ + " "
514
+ + df["date_time"].dt.strftime("%Y").astype(str)
515
+ )
516
+
517
+ # Aggregate rows by month_year.
518
+ df = (
519
+ df[["date_time", "month_year", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
520
+ .groupby("month_year")
521
+ .agg(
522
+ first_day_of_month=("date_time", "min"),
523
+ start_index_m3=("start_index_m3", "min"),
524
+ end_index_m3=("end_index_m3", "max"),
525
+ volume_m3=("volume_m3", "sum"),
526
+ energy_kwh=("energy_kwh", "sum"),
527
+ timestamp=("timestamp", "min"),
528
+ count=("energy_kwh", "count"),
529
+ )
530
+ .reset_index()
531
+ )
532
+
533
+ # Sort rows by month ascending.
534
+ df = df.sort_values(by=["first_day_of_month"])
535
+
536
+ # Select rows where we have a full month (more than 27 days) except for the current month.
537
+ df = pd.concat([df[(df["count"] >= 28)], df.tail(1)[df.tail(1)["count"] < 28]])
538
+
539
+ # Rename columns for their target names.
540
+ df = df.rename(columns={"month_year": "time_period"})
541
+
542
+ # Select target columns.
543
+ df = df[["time_period", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
544
+
545
+ res = cast(list[dict[str, Any]], df.to_dict("records"))
546
+
547
+ return res
548
+
549
+ # ------------------------------------------------------
550
+ @staticmethod
551
+ def computeYearly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
552
+
553
+ df = pd.DataFrame(daily)
554
+
555
+ # Trimming head and trailing spaces and convert to datetime.
556
+ df["date_time"] = pd.to_datetime(df["time_period"].str.strip(), format=JsonWebDataSource.OUTPUT_DATE_FORMAT)
557
+
558
+ # Get the corresponding year.
559
+ df["year"] = df["date_time"].dt.strftime("%Y")
560
+
561
+ # Aggregate rows by month_year.
562
+ df = (
563
+ df[["year", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
564
+ .groupby("year")
565
+ .agg(
566
+ start_index_m3=("start_index_m3", "min"),
567
+ end_index_m3=("end_index_m3", "max"),
568
+ volume_m3=("volume_m3", "sum"),
569
+ energy_kwh=("energy_kwh", "sum"),
570
+ timestamp=("timestamp", "min"),
571
+ count=("energy_kwh", "count"),
572
+ )
573
+ .reset_index()
574
+ )
575
+
576
+ # Sort rows by month ascending.
577
+ df = df.sort_values(by=["year"])
578
+
579
+ # Select rows where we have almost a full year (more than 360) except for the current year.
580
+ df = pd.concat([df[(df["count"] >= 360)], df.tail(1)[df.tail(1)["count"] < 360]])
581
+
582
+ # Rename columns for their target names.
583
+ df = df.rename(columns={"year": "time_period"})
584
+
585
+ # Select target columns.
586
+ df = df[["time_period", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
587
+
588
+ res = cast(list[dict[str, Any]], df.to_dict("records"))
589
+
590
+ return res
pygazpar/enum.py CHANGED
@@ -1,12 +1,35 @@
1
1
  from enum import Enum
2
2
 
3
- class PropertyNameEnum(Enum):
4
- DATE = "date"
5
- START_INDEX_M3 = "start_index_m3"
6
- END_INDEX_M3 = "end_index_m3"
7
- VOLUME_M3 = "volume_m3"
8
- ENERGY_KWH = "energy_kwh"
9
- CONVERTER_FACTOR = "converter_factor"
10
- LOCAL_TEMPERATURE = "local_temperature"
3
+
4
+ # ------------------------------------------------------------------------------------------------------------
5
+ class PropertyName(Enum):
6
+ TIME_PERIOD = "time_period"
7
+ START_INDEX = "start_index_m3"
8
+ END_INDEX = "end_index_m3"
9
+ VOLUME = "volume_m3"
10
+ ENERGY = "energy_kwh"
11
+ CONVERTER_FACTOR = "converter_factor_kwh/m3"
12
+ TEMPERATURE = "temperature_degC"
11
13
  TYPE = "type"
12
14
  TIMESTAMP = "timestamp"
15
+
16
+ def __str__(self):
17
+ return self.value
18
+
19
+ def __repr__(self):
20
+ return self.__str__()
21
+
22
+
23
+ # ------------------------------------------------------------------------------------------------------------
24
+ class Frequency(Enum):
25
+ HOURLY = "hourly"
26
+ DAILY = "daily"
27
+ WEEKLY = "weekly"
28
+ MONTHLY = "monthly"
29
+ YEARLY = "yearly"
30
+
31
+ def __str__(self):
32
+ return self.value
33
+
34
+ def __repr__(self):
35
+ return self.__str__()