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 | class AbstractEngine:
"""General Engine interface."""
def __init__(self, wb, path: MaybePathlike = None):
self.wb = wb
self.path = path
@abstractmethod
def name_address(self, name: str) -> XLSXAddress:
pass
@property
@abstractmethod
def names(self):
pass
@staticmethod
def _load_address(x: Addresslike, /) -> XLSXAddress:
if isinstance(x, str):
out = XLSXAddress(x)
else:
out = x
return out
def read(self, addr: Addresslike, read_as=None, hook=None):
addr = self._load_address(addr)
if addr.is_range:
v = self._read_range(addr, dtype=read_as)
else:
v = self._read(addr, dtype=read_as)
if hook:
v = hook(v)
return v
@abstractmethod
def _read(self, addr, dtype=None):
pass
def _read_range(self, addr, dtype=None):
coords = XLSXAddress(addr).as_array()
return np.array([self.read(i) for i in coords], dtype=dtype)
@abstractmethod
def read_via_name(self, name, **kwargs):
pass
def write(self, addr: Addresslike, value: Any):
addr = self._load_address(addr)
if addr.is_range:
self._write_range(addr.format(), value)
else:
self._write(addr.format(), value)
@abstractmethod
def write_via_name(self, name, value: Any):
pass
@abstractmethod
def _write(self, addr: Addresslike, value: Any):
pass
def _write_range(self, addr: Addresslike, values: list[Any]):
addr = self._load_address(addr)
addr_range = [f"{addr.sheet}!{coord}" for coord in addr.as_array()]
if len(addr_range) != len(values):
raise ValueError(f"Cannot broadcast {values=} to {addr_range=}.")
for cell_addr, cell_value in zip(addr_range, values):
self.write(cell_addr, cell_value)
return self
def save(self, f: MaybePathlike = None):
if f is None:
if self.path is None:
raise ValueError(f"Need a file path. {f=} and {self.path=}")
out = self.path
else:
out = f
self._save(out)
@abstractmethod
def close(self):
pass
@abstractmethod
def _save(self, f):
pass
@classmethod
@abstractmethod
def from_file(cls, path: Pathlike, **kwargs):
pass
def __repr__(self):
return f"{self.__class__.__name__}({self.path})"
def names_as_dict(self, filter_prefix: MaybeStr = None):
if filter_prefix is None:
filter_prefix = ""
out = {
name: self.read_via_name(name)
for name in self.names
if name.startswith(filter_prefix)
}
return out
def specifications(self, filter_prefix: MaybeStr = None) -> pd.DataFrame:
names = self.names_as_dict(filter_prefix=filter_prefix)
addrs = {name: self.name_address(name) for name in names}
records = [
dict(name=k, addr=v, sheet=v.sheet, coord=v.coord, value=names[k])
for k, v in addrs.items()
]
return pd.DataFrame.from_records(records)
def export(self, filter_prefix: MaybeStr = None) -> str:
df = self.specifications(filter_prefix=filter_prefix)
parts = [
{
g: df_.set_index("name")["value"].to_dict()
for g, df_ in df.groupby("sheet")
}
]
out = "\n\n".join([toml.dumps(part) for part in parts])
return out
|