‘Contexts’ en Python

En Python, un contexto (context) es una clase que implementa los métodos ‘__enter__’ y ‘__exit__’ los cuales son llamados si la clase en llamada con la palabra reservada ‘with’. Por ejemplo, los descriptores de archivo (file handle) en Python se pueden llamar con un contexto, ahorrando llamar ‘finally’ para cerrar archivos, sin importar si hay un error.

Les traigo de vuelta el programa de ‘DolarToday’ el cual escribe datos en un archivo binario, pero ahora utilizando contextos:

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
#!/usr/bin/env python3
# Simple program to save the 'Dolartoday' extra official dollar rates from CSV to a custom format
# Revisited to use 'contexts' to avoid using finally on exception handling
# @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, "{} for date {} is invalid!".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):
 
    # https://en.wikipedia.org/wiki/List_of_file_signatures
    __FILE_MAGIC = b"DLR\x00"
    __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):
        try:
            with gzip.open(file, "wb") as fh:
                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
 
    def readBinary(self, file, verbose=False):
        try:
            with gzip.open(file, "rb") as fh:
                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
 
    def readCsv(self, file):
        tempMap = {}
        try:
            with open(file, "r", encoding="UTF-8") as fh:
                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
 
    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="Full path to CSV file with date,value pairs per line")
    op.add_option(
                  "-w", "--write", 
                  action="store", 
                  dest="write", 
                  help="Full path to destination file in binary format")
    op.add_option(
                  "-p", "--report", 
                  action="store", 
                  dest="report",
                  help="Read the contents of the binary storage and generate a report. Incompatible with --read and --write")
    op.add_option(
                  "-v", "--verbose", 
                  action="store_true", 
                  default=False, 
                  dest="verbose", 
                  help="Enable verbose mode")
 
    (options, values) = op.parse_args()
 
    main(options)
</id>