Guardando datos usando formatos de archivo a la medida, en Python 3

En Venezuela una de las pocas formas de saber la paridad entre el dolar “paralelo” y el Bolivar fuerte es utilizando el portal “DolarToday”. El sitio web (https://dolartoday.com/historico-dolar/) ofrece datos que van desde el 2010 hasta el presente, en los cuales puede ver la paridad entre las dos monedas.

En un arranque de ociosidad, decidí bajarme el archivo de Excel con las tasas de conversión, lo exporte a CSV y de allí escribí un programa en Python 3 que hace lo siguiente:

  • Extendiende la clase ‘dict’ en Python para mantener un diccionario con claves ordenadas, el cual se pueda guardar y recuperar a si mismo desde el disco duro
  • Usar struct.Struct para guardar y leer data binaria (también muestro como leer un archivo comprimido con gzip).

 

El código a continuación:

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
#!/usr/bin/env python3
# Simple program to save the 'Dolartoday' extra official dollar rates from CSV to a custom format
# @author Jose Vicente Nunez, josevnz@kodegeek.com
import os, sys, datetime, re, gzip, struct
from optparse import OptionParser, OptionValueError
from datetime import datetime
 
class DollarToday:
 
    def __init__(self, ddate, value):
        if isinstance(ddate, datetime):
            self.__date = ddate
        else:
            self.__date = datetime.strptime(ddate, "%m-%d-%Y")
        self.__value = float(value)
        assert self.__value > 0.0, "¡{} para la fecha {} es invalida!".format(value, self.__date)
 
    @property
    def date(self):
        return self.__date
 
    @date.setter
    def date(self, date):
        assert isinstance(date, datetime), "Invalid date {}".format(date)
        self.__date = date
 
    @property
    def value(self):
        return self.__value
 
    def __str__(self):
        return "DollarToday[date={}, value={}]".format(self.__date, self.__value)
 
    def __hash__(self):
        return str(id(self))
 
class DollarCollection(dict):
 
    __FILE_MAGIC = b"DLR\x00" # Me invente este numero mágico...
    __FILE_VERSION = b"\x00\x01"
    __dollarStruct = struct.Struct("<id ")
 
    def values(self):
        for dateId in sorted(self.keys()):
            yield self[dateId]
 
    def items(self):
        for dateId in self.keys():
            yield (dateId, self[dateId])
 
    def __iter__(self):
        for dateId in sorted(super().keys()):
            yield dateId
 
    def save(self, file):
        fh = None
 
        try:
           fh = gzip.open(file, "wb")
           fh.write(self.__FILE_MAGIC)
           fh.write(self.__FILE_VERSION)
           for dollar in self.values():
               data = bytearray()
               data.extend(
                           self.__dollarStruct.pack(
                                dollar.date.toordinal(),
                                dollar.value
                           )
                )
               fh.write(data)
        except (Exception) as err:
            raise
        finally:
            if fh is not None:
                fh.close()
 
    def readBinary(self, file, verbose=False):
        fh = None
        try:
            fh = gzip.open(file, "rb")
            magic = fh.read(len(self.__FILE_MAGIC))
            if magic != self.__FILE_MAGIC:
                raise "File doesn't look like a KodeGeek.com binary file!"
            version = fh.read(len(self.__FILE_VERSION))
            if version > self.__FILE_VERSION:
                raise "Unsupported file version: {}, expected {}".format(version, self.__FILE_VERSION)
            self.clear()
            while True:
                data = fh.read(self.__dollarStruct.size)
                if len(data) == 0:
                    break
                numbers = self.__dollarStruct.unpack(data)
                #if verbose:
                #    print("{}".format(",".join([str(x) for x in numbers])))
                dolar = DollarToday(
                                    datetime.fromordinal(numbers[0]),
                                    numbers[1]
                                    )
                self[dolar.date] = dolar
 
        except (Exception) as err:
            raise
        finally:
            if (fh is not None):
                fh.close()
    def readCsv(self, file):
        fh = None
        tempMap = {}
        try:
            fh = open(file, "r", encoding="UTF-8")
            for line in fh.readlines():
                # 6-23-2010       9.92
                (date, value) = line.strip().split("\t")
                if re.match("Fecha", date):
                    continue
                try:
                    dolVsBol = DollarToday(date, value)
                    tempMap[dolVsBol.date] = dolVsBol
                except (Exception) as err:
                    print(err)
            self.clear()
            self.update(tempMap)
        except (Exception) as err:
            raise
        finally:
            if (fh is not None):
                fh.close()
 
    def __str__(self):
        return "Records={},\n{}".format(len(self), ",\n".join([str(date) for date in self.values()]))
 
def main(options):
 
    verbose = options.verbose
    dc = DollarCollection()
    if options.report == None:
        dc.readCsv(options.read)
        dc.save(options.write)
    else:
        dc.readBinary(options.report, verbose)
    if verbose:
        print("{}".format(dc))
 
if __name__ == "__main__":
 
    usagetext = """
%prog --read csv.file --write binary.file
 
Or:
 
%prog --report binary.file
 
"""
 
    op = OptionParser(usage=usagetext)
    op.add_option(
                  "-r", "--read",
                  action="store", 
                  dest="read", 
                  help="Ruta completa del archivo CSV fuente, cada linea tiene una clave=valor")
    op.add_option(
                  "-w", "--write", 
                  action="store", 
                  dest="write", 
                  help="Ruta completa para guardar el archivo en nuevo formato binario")
    op.add_option(
                  "-p", "--report", 
                  action="store", 
                  dest="report",
                  help="Lee el archivo binario en memoria. No es compatible con --read y --write")
    op.add_option(
                  "-v", "--verbose", 
                  action="store_true", 
                  default=False, 
                  dest="verbose", 
                  help="Activar impresión de valores por pantalla")
 
    (options, values) = op.parse_args()
 
    main(options)
</id>

Un ejemplo de como correrlo:

1
2
DolarToday.py --read /Users/josevnz/Documents/dolartoday.csv --write /Users/josevnz/Documents/dolartoday.jose --verbose
DolarToday.py --report /Users/josevnz/Documents/dolartoday.jose --verbose

Y la salida luce como esto:

1
2
3
4
5
6
7
8
9
10
Records=1917,
DollarToday[date=2010-06-23 00:00:00, value=9.92],
DollarToday[date=2010-06-25 00:00:00, value=8.05],
DollarToday[date=2010-06-26 00:00:00, value=8.05],
DollarToday[date=2010-06-27 00:00:00, value=7.91],
DollarToday[date=2010-06-28 00:00:00, value=7.91],
DollarToday[date=2010-06-29 00:00:00, value=7.92],
DollarToday[date=2010-06-30 00:00:00, value=7.97],
DollarToday[date=2010-07-01 00:00:00, value=7.97],
DollarToday[date=2010-07-02 00:00:00, value=7.98],

Si tan sólo el gobierno de Nicolás Maduro fuera tan transparente como mis programas 🙂