pyfwf 1.0.0__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.
- pyfwf/__init__.py +25 -0
- pyfwf/columns.py +216 -0
- pyfwf/descriptors.py +111 -0
- pyfwf/hydrating.py +100 -0
- pyfwf/readers.py +86 -0
- pyfwf/renders.py +69 -0
- pyfwf-1.0.0.dist-info/METADATA +121 -0
- pyfwf-1.0.0.dist-info/RECORD +11 -0
- pyfwf-1.0.0.dist-info/WHEEL +5 -0
- pyfwf-1.0.0.dist-info/licenses/LICENSE.md +22 -0
- pyfwf-1.0.0.dist-info/top_level.txt +1 -0
pyfwf/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright 2015 Umbrella Tech.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__author__ = 'Kelson da Costa Medeiros <kelsoncm@gmail.com>'
|
pyfwf/columns.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright 2015 Umbrella Tech.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__author__ = 'Kelson da Costa Medeiros <kelsoncm@gmail.com>'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from datetime import datetime, date, time
|
|
30
|
+
from .hydrating import Hydrator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AbstractColumn(Hydrator):
|
|
34
|
+
to_str_assertion_types = None
|
|
35
|
+
to_str_assertion_class = None
|
|
36
|
+
to_str_none_pad = None
|
|
37
|
+
to_str_pad_template = None
|
|
38
|
+
hydrating_args = ['name', 'size', 'description']
|
|
39
|
+
|
|
40
|
+
def __init__(self, _name: str, size: int, description: str=None):
|
|
41
|
+
super(AbstractColumn, self).__init__()
|
|
42
|
+
assert isinstance(_name, str), 'O campo name deve ser uma string'
|
|
43
|
+
assert _name and _name.rstrip(), 'O campo column_name deve ser uma string válida e não branca'
|
|
44
|
+
assert isinstance(size, int), 'O campo size deve ser um inteiro'
|
|
45
|
+
assert size > 0, 'O campo size deve ser maior que 0'
|
|
46
|
+
if description is None:
|
|
47
|
+
description = _name
|
|
48
|
+
else:
|
|
49
|
+
assert isinstance(description, str), 'O campo description deve ser uma string'
|
|
50
|
+
|
|
51
|
+
self.name = _name
|
|
52
|
+
self.size = size
|
|
53
|
+
self.description = description
|
|
54
|
+
self.start = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def end(self):
|
|
58
|
+
assert isinstance(self.start, int), 'O campo start deve ser um inteiro'
|
|
59
|
+
assert self.start > 0, 'O campo start deve ser maior que 0'
|
|
60
|
+
return self.start + self.size - 1
|
|
61
|
+
|
|
62
|
+
def to_value(self, slice):
|
|
63
|
+
assert isinstance(slice, str), 'Informe uma string para converter corretamente'
|
|
64
|
+
assert len(slice) == self.size, "A string deve ter exatamente o tamanho do campo '%s' (%s)" % \
|
|
65
|
+
(self.name, self.size)
|
|
66
|
+
return slice
|
|
67
|
+
|
|
68
|
+
def _validate_to_str_size(self, value):
|
|
69
|
+
assert len(value) == self.size, "O valor a ser serializado para o campo '%s' não pode ser diferente de %d " \
|
|
70
|
+
% (self.name, self.size)
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def to_str_assertion(self, value):
|
|
74
|
+
return isinstance(value, self.to_str_assertion_class)
|
|
75
|
+
|
|
76
|
+
def to_str(self, value: str):
|
|
77
|
+
assert value is None or self.to_str_assertion(value), \
|
|
78
|
+
"O campo '%s' só aceita '%s' or 'None'" % (self.name, self.to_str_assertion_types)
|
|
79
|
+
|
|
80
|
+
if value is None:
|
|
81
|
+
return self.to_str_none_pad * self.size
|
|
82
|
+
else:
|
|
83
|
+
return self._validate_to_str_size((self.to_str_pad_template % self.size).format(value))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CharColumn(AbstractColumn):
|
|
87
|
+
|
|
88
|
+
to_str_assertion_types = 'str'
|
|
89
|
+
to_str_assertion_class = str
|
|
90
|
+
to_str_none_pad = ' '
|
|
91
|
+
to_str_pad_template = '{0: <%d}'
|
|
92
|
+
|
|
93
|
+
def to_value(self, slice: str):
|
|
94
|
+
return super(CharColumn, self).to_value(slice).strip()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class RightCharColumn(CharColumn):
|
|
98
|
+
to_str_pad_template = "{0: >%d}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PositiveIntegerColumn(AbstractColumn):
|
|
102
|
+
to_str_assertion_types = 'positive int'
|
|
103
|
+
to_str_assertion_class = int
|
|
104
|
+
to_str_none_pad = '0'
|
|
105
|
+
to_str_pad_template = "{0:0%dd}"
|
|
106
|
+
|
|
107
|
+
def to_str_assertion(self, value):
|
|
108
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
109
|
+
|
|
110
|
+
def to_value(self, slice: str):
|
|
111
|
+
_value = int(super(PositiveIntegerColumn, self).to_value(slice))
|
|
112
|
+
assert _value >= 0, \
|
|
113
|
+
"Informe uma string para converter corretamente, '%s' não é um '%s'" % (slice, self.to_str_assertion_types)
|
|
114
|
+
return _value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PositiveDecimalColumn(PositiveIntegerColumn):
|
|
118
|
+
to_str_assertion_types = 'positive decimal'
|
|
119
|
+
to_str_assertion_class = float
|
|
120
|
+
hydrating_args = ['name', 'size', 'decimals', 'description']
|
|
121
|
+
|
|
122
|
+
def __init__(self, _name: str, size: int, decimals: int=2, description: str=None):
|
|
123
|
+
super(PositiveDecimalColumn, self).__init__(_name, size, description)
|
|
124
|
+
assert isinstance(decimals, int), 'Os decimais devem ser um inteiro'
|
|
125
|
+
assert decimals > 0, 'Os decimais devem ser maior que 0'
|
|
126
|
+
assert size > decimals, 'Os decimais devem ser menores que o size'
|
|
127
|
+
self.decimals = decimals
|
|
128
|
+
|
|
129
|
+
def to_value(self, slice: str):
|
|
130
|
+
return super(PositiveDecimalColumn, self).to_value(slice) / pow(10, self.decimals)
|
|
131
|
+
|
|
132
|
+
def to_str_assertion(self, value):
|
|
133
|
+
return isinstance(value, float) and not isinstance(value, bool) and value >= 0.0
|
|
134
|
+
|
|
135
|
+
def to_str(self, value: str):
|
|
136
|
+
assert value is None or self.to_str_assertion(value), \
|
|
137
|
+
"O campo '%s' só aceita '%s' or 'None'" % (self.name, self.to_str_assertion_types)
|
|
138
|
+
|
|
139
|
+
if value is None:
|
|
140
|
+
return self.to_str_none_pad * self.size
|
|
141
|
+
else:
|
|
142
|
+
_value = int(value * pow(10, self.decimals))
|
|
143
|
+
return self._validate_to_str_size((self.to_str_pad_template % self.size).format(_value))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DateTimeColumn(AbstractColumn):
|
|
147
|
+
to_str_assertion_types = 'datetime'
|
|
148
|
+
to_str_assertion_class = datetime
|
|
149
|
+
to_str_none_pad = '0'
|
|
150
|
+
format_num_elements = 5
|
|
151
|
+
hydrating_args = ['name', 'format', 'description']
|
|
152
|
+
|
|
153
|
+
def __init__(self, _name: str, _format: str='%d%m%Y%H%M', description: str=None):
|
|
154
|
+
assert isinstance(_name, str), \
|
|
155
|
+
'O campo name deve ser uma string'
|
|
156
|
+
assert _name and _name.strip(), \
|
|
157
|
+
'O campo column_name deve ser uma string válida e não branca'
|
|
158
|
+
assert isinstance(_format, str), \
|
|
159
|
+
"O argumento '_format' do campo '%s' deve ser uma string" % _name
|
|
160
|
+
assert _format and _format.strip(), \
|
|
161
|
+
"O argumento '_format' do campo '%s' deve ser uma string válida e não branca" % _name
|
|
162
|
+
assert len([x for x in re.finditer(re.compile('(%[a-z,A-Z])'), _format)]) == self.format_num_elements, \
|
|
163
|
+
"O argumento '_format' (%s) do campo '%s' deve ter um formato de data/hora válido" % (_format, _name)
|
|
164
|
+
|
|
165
|
+
_size = len(datetime(2001, 12, 31, 13, 59).strftime(_format))
|
|
166
|
+
|
|
167
|
+
self.to_str_pad_templating = ''
|
|
168
|
+
super(DateTimeColumn, self).__init__(_name, _size, description)
|
|
169
|
+
|
|
170
|
+
self.format = _format
|
|
171
|
+
|
|
172
|
+
def to_value(self, slice: str):
|
|
173
|
+
_value = super(DateTimeColumn, self).to_value(slice)
|
|
174
|
+
try:
|
|
175
|
+
if _value == self.to_str_none_pad * self.size:
|
|
176
|
+
return None
|
|
177
|
+
else:
|
|
178
|
+
return datetime.strptime(_value, self.format)
|
|
179
|
+
except ValueError:
|
|
180
|
+
raise ValueError("O valor '%s' do campo '%s' é inválido para o formato '%s'" %
|
|
181
|
+
(_value, self.name, self.format))
|
|
182
|
+
|
|
183
|
+
def to_str(self, value: datetime):
|
|
184
|
+
assert value is None or self.to_str_assertion(value), \
|
|
185
|
+
"O campo '%s' só aceita '%s' or 'None'" % (self.name, self.to_str_assertion_types)
|
|
186
|
+
|
|
187
|
+
if value is None:
|
|
188
|
+
return self.to_str_none_pad * self.size
|
|
189
|
+
else:
|
|
190
|
+
return self._validate_to_str_size(value.strftime(self.format))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DateColumn(DateTimeColumn):
|
|
194
|
+
to_str_assertion_types = 'date'
|
|
195
|
+
to_str_assertion_class = date
|
|
196
|
+
format_num_elements = 3
|
|
197
|
+
|
|
198
|
+
def __init__(self, _name: str, _format: str='%d%m%Y', description: str=None):
|
|
199
|
+
super(DateColumn, self).__init__(_name, _format, description)
|
|
200
|
+
|
|
201
|
+
def to_value(self, slice: str):
|
|
202
|
+
result = super(DateColumn, self).to_value(slice)
|
|
203
|
+
return result.date() if result is not None else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TimeColumn(DateTimeColumn):
|
|
207
|
+
to_str_assertion_types = 'time'
|
|
208
|
+
to_str_assertion_class = time
|
|
209
|
+
format_num_elements = 2
|
|
210
|
+
|
|
211
|
+
def __init__(self, _name: str, _format: str='%H%M', description: str=None):
|
|
212
|
+
super(TimeColumn, self).__init__(_name, _format, description)
|
|
213
|
+
|
|
214
|
+
def to_value(self, slice: str):
|
|
215
|
+
result = super(TimeColumn, self).to_value(slice)
|
|
216
|
+
return result.time() if result is not None else None
|
pyfwf/descriptors.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright 2015 Umbrella Tech.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__author__ = 'Kelson da Costa Medeiros <kelsoncm@gmail.com>'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from io import StringIO
|
|
29
|
+
import importlib
|
|
30
|
+
from typing import List, Dict
|
|
31
|
+
from .columns import AbstractColumn
|
|
32
|
+
from .hydrating import Hydrator
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RowDescriptor(Hydrator):
|
|
36
|
+
|
|
37
|
+
def __init__(self, columns: List[AbstractColumn]):
|
|
38
|
+
super(RowDescriptor, self).__init__()
|
|
39
|
+
assert isinstance(columns, list), 'columns deve ser uma List'
|
|
40
|
+
assert columns != [], 'columns deve ter ao menos 1 elemento'
|
|
41
|
+
self.columns = columns
|
|
42
|
+
last = None
|
|
43
|
+
for column in columns:
|
|
44
|
+
column.start = last.end + 1 if last is not None else 1
|
|
45
|
+
last = column
|
|
46
|
+
self.validate_positions()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def line_size(self):
|
|
50
|
+
return self.columns[len(self.columns)-1].end
|
|
51
|
+
|
|
52
|
+
def validate_positions(self):
|
|
53
|
+
prev = None
|
|
54
|
+
for col in self.columns:
|
|
55
|
+
if prev is None:
|
|
56
|
+
assert col.start == 1, 'A coluna %s deve começar com 1' % col.name
|
|
57
|
+
else:
|
|
58
|
+
assert prev.end + 1 == col.start, 'A coluna %s (starts in %d) deve começar imediatamente após ' \
|
|
59
|
+
'a coluna %s (ends in %d)' % \
|
|
60
|
+
(col.name, col.start, prev.name, prev.end)
|
|
61
|
+
prev = col
|
|
62
|
+
|
|
63
|
+
def get_values(self, row):
|
|
64
|
+
return {col.name: col.to_value(row[col.start-1:col.end]) for col in self.columns}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HeaderRowDescriptor(RowDescriptor):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FooterRowDescriptor(RowDescriptor):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DetailRowDescriptor(RowDescriptor):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class FileDescriptor(Hydrator):
|
|
80
|
+
def __init__(self,
|
|
81
|
+
details: List[DetailRowDescriptor],
|
|
82
|
+
header: HeaderRowDescriptor=None,
|
|
83
|
+
footer: FooterRowDescriptor=None):
|
|
84
|
+
super(FileDescriptor, self).__init__()
|
|
85
|
+
|
|
86
|
+
assert isinstance(details, list), \
|
|
87
|
+
'details deve ser uma List'
|
|
88
|
+
assert len(details) > 0, \
|
|
89
|
+
'details deve ser uma List com ao menos 1 DetailRowDescriptor'
|
|
90
|
+
for detail in details:
|
|
91
|
+
assert isinstance(detail, DetailRowDescriptor), \
|
|
92
|
+
'details deve ser uma List de DetailRowDescriptor'
|
|
93
|
+
assert isinstance(header, HeaderRowDescriptor) or header is None, \
|
|
94
|
+
'header deve ser um HeaderRowDescriptor'
|
|
95
|
+
assert isinstance(footer, FooterRowDescriptor) or footer is None, \
|
|
96
|
+
'footer_descriptor deve ser um FooterRowDescriptor'
|
|
97
|
+
|
|
98
|
+
self.header = header
|
|
99
|
+
self.footer = footer
|
|
100
|
+
self.details = details
|
|
101
|
+
self.validate_sizes()
|
|
102
|
+
self.line_size = self.details[0].line_size
|
|
103
|
+
|
|
104
|
+
def validate_sizes(self):
|
|
105
|
+
h = self.header.line_size if self.header else 0
|
|
106
|
+
f = self.footer.line_size if self.footer else 0
|
|
107
|
+
d = self.details[0].line_size
|
|
108
|
+
ln = [x.line_size for x in self.details]
|
|
109
|
+
s = sum(ln)
|
|
110
|
+
assert (s == d * len(self.details)) and (d == h or h == 0) and (d == f or f == 0), \
|
|
111
|
+
'O tamanho das linhas header (%d), footer (%d) e das details (%s) devem ser iguais' % (h, f, ln)
|
pyfwf/hydrating.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright 2015 Umbrella Tech.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__author__ = 'Kelson da Costa Medeiros <kelsoncm@gmail.com>'
|
|
26
|
+
|
|
27
|
+
__all__ = ['hydrate_object', 'dehydrate_object', 'Hydrator']
|
|
28
|
+
|
|
29
|
+
import importlib
|
|
30
|
+
from typing import Dict, List
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_full_class_name(instance_or_class):
|
|
34
|
+
if isinstance(instance_or_class, type):
|
|
35
|
+
return instance_or_class.__module__ + "." + instance_or_class.__qualname__
|
|
36
|
+
else:
|
|
37
|
+
return instance_or_class.__module__ + "." + instance_or_class.__class__.__qualname__
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_class(full_class_name, *args, **kwargs):
|
|
41
|
+
module_name, class_name = full_class_name.rsplit(".", 1)
|
|
42
|
+
MyClass = getattr(importlib.import_module(module_name), class_name)
|
|
43
|
+
return MyClass(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assert_element_isinstance(name, lst, cls):
|
|
47
|
+
if name in lst:
|
|
48
|
+
assert isinstance(lst[name], cls), '%s has informed, but is not a %s' % (name, cls)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def assert_isinstance(name, value, cls):
|
|
52
|
+
assert isinstance(value, cls), '%s is not a %s' % (name, cls)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def hydrate_object(representation: Dict):
|
|
56
|
+
assert_isinstance('representation', representation, Dict)
|
|
57
|
+
assert '_hydrate_as' in representation, '_hydrate_as is required'
|
|
58
|
+
assert_element_isinstance('_hydrate_as', representation, str)
|
|
59
|
+
assert_element_isinstance('args', representation, List)
|
|
60
|
+
assert_element_isinstance('kwargs', representation, Dict)
|
|
61
|
+
assert_element_isinstance('attributes', representation, Dict)
|
|
62
|
+
|
|
63
|
+
def hydrate_if_hydratable(o):
|
|
64
|
+
return hydrate_object(o) if isinstance(o, Dict) and '_hydrate_as' in o else o
|
|
65
|
+
|
|
66
|
+
hydrate_as = representation['_hydrate_as']
|
|
67
|
+
args = [hydrate_if_hydratable(arg)
|
|
68
|
+
for arg in (representation['args'] if 'args' in representation else [])]
|
|
69
|
+
kwargs = {k: hydrate_if_hydratable(v)
|
|
70
|
+
for k, v in (representation['kwargs'] if 'kwargs' in representation else {}).items()}
|
|
71
|
+
attributes = representation['attributes'] if 'attributes' in representation else {}
|
|
72
|
+
|
|
73
|
+
instance = create_class(hydrate_as, *args, **kwargs)
|
|
74
|
+
for attr_name in attributes.keys():
|
|
75
|
+
setattr(instance, attr_name, attributes[attr_name])
|
|
76
|
+
return instance
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dehydrate_object(obj):
|
|
80
|
+
def dehydrate_if_hydratable(o):
|
|
81
|
+
return dehydrate_object(o) if isinstance(o, Hydrator) else o
|
|
82
|
+
|
|
83
|
+
result = {"_hydrate_as": get_full_class_name(obj)}
|
|
84
|
+
if hasattr(obj, 'hydrating_args'):
|
|
85
|
+
result["args"] = [dehydrate_if_hydratable(getattr(obj, name)) for name in obj.hydrating_args]
|
|
86
|
+
if hasattr(obj, 'hydrating_kwargs'):
|
|
87
|
+
result["kwargs"] = {name: dehydrate_if_hydratable(getattr(obj, name)) for name in obj.hydrating_kwargs}
|
|
88
|
+
if hasattr(obj, 'hydrating_attributes'):
|
|
89
|
+
result["attributes"] = {name: dehydrate_if_hydratable(getattr(obj, name)) for name in obj.hydrating_attributes}
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Hydrator(object):
|
|
94
|
+
|
|
95
|
+
def dehydrate(self):
|
|
96
|
+
return dehydrate_object(self)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def hydrate(cls, representation: Dict):
|
|
100
|
+
return hydrate_object(representation)
|
pyfwf/readers.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright 2015 Umbrella Tech.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__author__ = 'Kelson da Costa Medeiros <kelsoncm@gmail.com>'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from typing import Iterable, List
|
|
29
|
+
from io import StringIO, TextIOWrapper
|
|
30
|
+
from .descriptors import FileDescriptor
|
|
31
|
+
from collections import abc
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ['Reader']
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Reader:
|
|
38
|
+
|
|
39
|
+
def __init__(self, _iterable: Iterable[str], file_descriptor: FileDescriptor, newline: str="\n\r"):
|
|
40
|
+
assert isinstance(_iterable, abc.Iterable), \
|
|
41
|
+
'O argumento _iterable tem que ser um Iterator'
|
|
42
|
+
assert isinstance(file_descriptor, FileDescriptor), \
|
|
43
|
+
'O argumento file_descriptor tem que ser um FileDescriptor'
|
|
44
|
+
assert isinstance(newline, str) and newline in ["\n", "\r", "\n\r"], \
|
|
45
|
+
'O argumento newline tem que ser uma str e conter "\\n", "\\r" ou "\\n\\r"'
|
|
46
|
+
|
|
47
|
+
self.iterable = _iterable
|
|
48
|
+
self.file_descriptor = file_descriptor
|
|
49
|
+
self.line_num = 0
|
|
50
|
+
self.newline = newline
|
|
51
|
+
self.lines_count = 0
|
|
52
|
+
|
|
53
|
+
if isinstance(self.iterable, StringIO):
|
|
54
|
+
self.filesize = len(self.iterable.getvalue())
|
|
55
|
+
elif isinstance(self.iterable, TextIOWrapper):
|
|
56
|
+
self.filesize = len(self.iterable.read())
|
|
57
|
+
elif isinstance(self.iterable, str):
|
|
58
|
+
self.filesize = len(self.iterable)
|
|
59
|
+
self.iterable = StringIO(self.iterable)
|
|
60
|
+
elif isinstance(self.iterable, List):
|
|
61
|
+
self.iterable = iter(_iterable)
|
|
62
|
+
self.lines_count = len(_iterable)
|
|
63
|
+
self.filesize = sum([len(r) for r in _iterable])
|
|
64
|
+
else:
|
|
65
|
+
raise TypeError('Unsupported Iterable')
|
|
66
|
+
|
|
67
|
+
assert float(self.filesize) % float(self.file_descriptor.line_size + len(self.newline)) == 0, \
|
|
68
|
+
"Algumas linha não tem o tamanho correto (%d) ou não tem a quebra de linha adequada (%s), " \
|
|
69
|
+
"total de bytes %d e total de linhas %f" % \
|
|
70
|
+
(self.file_descriptor.line_size, self.newline.replace("\n", "\\n").replace("\r", "\\r"),
|
|
71
|
+
self.filesize, float(self.filesize) / float(self.file_descriptor.line_size + len(self.newline)))
|
|
72
|
+
self.lines_count = self.filesize / (self.file_descriptor.line_size + len(self.newline))
|
|
73
|
+
|
|
74
|
+
def __iter__(self):
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def __next__(self):
|
|
78
|
+
row = next(self.iterable)
|
|
79
|
+
self.line_num += 1
|
|
80
|
+
lc = row[:-len(self.newline)]
|
|
81
|
+
if self.file_descriptor.header and self.line_num == 1:
|
|
82
|
+
return self.file_descriptor.header.get_values(lc)
|
|
83
|
+
elif self.file_descriptor.footer and self.lines_count and self.lines_count == self.line_num:
|
|
84
|
+
return self.file_descriptor.footer.get_values(lc)
|
|
85
|
+
else:
|
|
86
|
+
return self.file_descriptor.details[0].get_values(lc)
|
pyfwf/renders.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright 2015 Umbrella Tech.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__author__ = 'Kelson da Costa Medeiros <kelsoncm@gmail.com>'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from io import StringIO
|
|
29
|
+
import importlib
|
|
30
|
+
from typing import List, Dict
|
|
31
|
+
from .columns import AbstractColumn
|
|
32
|
+
from .descriptors import FileDescriptor
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render_as_markdown(file_descriptor: FileDescriptor, out: StringIO):
|
|
36
|
+
def table_header(title, max_colname_size, max_coltype_size):
|
|
37
|
+
out.write(("# {title}\n\n"
|
|
38
|
+
"| # | {name:<%d} | Size | Start | End | {type:<%d} | Description\n"
|
|
39
|
+
"| ---- | {sep:-<%d} | ---- | ----- | ---- | {sep:-<%d} | -----------\n" %
|
|
40
|
+
(max_colname_size, max_coltype_size, max_colname_size, max_coltype_size)).
|
|
41
|
+
format(title=title, name='Column', type='Type', sep='-'))
|
|
42
|
+
|
|
43
|
+
def table_body(cols, max_colname_size, max_coltype_size):
|
|
44
|
+
line = 0
|
|
45
|
+
template = "| {0:>4d} | {1: <%d} | {2:>4d} | {3:>5d} | {4:>4d} | {5:<%d} | {6}\n" \
|
|
46
|
+
% (max_colname_size, max_coltype_size)
|
|
47
|
+
for col in cols:
|
|
48
|
+
line += 1
|
|
49
|
+
out.write(template.format(line, col.name, col.size, col.start, col.end, col.__class__.__name__,
|
|
50
|
+
col.description))
|
|
51
|
+
|
|
52
|
+
def table(title, cols, trailling=True):
|
|
53
|
+
max_colname_size = max([len(col.name) for col in cols])
|
|
54
|
+
max_coltype_size = max([len(col.__class__.__name__) for col in cols])
|
|
55
|
+
table_header(title, max_colname_size, max_coltype_size)
|
|
56
|
+
table_body(cols, max_colname_size, max_coltype_size)
|
|
57
|
+
if trailling:
|
|
58
|
+
out.write("\n\n")
|
|
59
|
+
|
|
60
|
+
if file_descriptor.header:
|
|
61
|
+
table("HEADER", file_descriptor.header.columns)
|
|
62
|
+
|
|
63
|
+
detail_num = 1
|
|
64
|
+
for detail in file_descriptor.details:
|
|
65
|
+
table("DETAILS %s" % detail_num, detail.columns)
|
|
66
|
+
detail_num += 1
|
|
67
|
+
|
|
68
|
+
if file_descriptor.footer:
|
|
69
|
+
table("FOOTER", file_descriptor.footer.columns, False)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfwf
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library to manipulate fixed width file (FWF)
|
|
5
|
+
Author: kelsoncm
|
|
6
|
+
License: The MIT License (MIT)
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2016 kelsoncm
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE.md
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
44
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
45
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
46
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
47
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
48
|
+
Requires-Dist: python-dotenv>=1.0; extra == "dev"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# Python FWF
|
|
52
|
+
|
|
53
|
+
[](https://pypi.org/project/python-fwf/)
|
|
54
|
+
[](https://github.com/kelsoncm/python-fwf/actions/workflows/publish.yml)
|
|
55
|
+
[](https://pypi.org/project/python-fwf/)
|
|
56
|
+
[](https://opensource.org/licenses/MIT)
|
|
57
|
+
[](https://github.com/kelsoncm/python-fwf/actions/workflows/test.yml)
|
|
58
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
59
|
+
[](https://codecov.io/gh/kelsoncm/python-fwf)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Python library for reading and manipulating **fixed-width files (FWF)**.
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- 📖 Read fixed-width format files with custom column definitions
|
|
67
|
+
- 🔧 Support for typed columns (integer, decimal, date, time, etc.)
|
|
68
|
+
- 📋 Header and footer row handling
|
|
69
|
+
- 🎯 Simple and intuitive API
|
|
70
|
+
- ✅ Fully tested (100% coverage)
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install pyfwf
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from pyfwf.columns import CharColumn, PositiveIntegerColumn
|
|
82
|
+
from pyfwf.descriptors import DetailRowDescriptor, FileDescriptor
|
|
83
|
+
from pyfwf.readers import Reader
|
|
84
|
+
|
|
85
|
+
# Define columns
|
|
86
|
+
detail = DetailRowDescriptor([
|
|
87
|
+
CharColumn(name='name', pos=1, size=20),
|
|
88
|
+
PositiveIntegerColumn(name='age', pos=21, size=3),
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
# Create file descriptor
|
|
92
|
+
fd = FileDescriptor(line_size=23, details=[detail])
|
|
93
|
+
|
|
94
|
+
# Read file
|
|
95
|
+
with open('data.fwf', 'r') as f:
|
|
96
|
+
reader = Reader(f, fd)
|
|
97
|
+
for row in reader:
|
|
98
|
+
print(row)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Supported Column Types
|
|
102
|
+
|
|
103
|
+
- `CharColumn` - Text data
|
|
104
|
+
- `RightCharColumn` - Right-aligned text
|
|
105
|
+
- `PositiveIntegerColumn` - Positive integers
|
|
106
|
+
- `PositiveDecimalColumn` - Decimal numbers
|
|
107
|
+
- `DateColumn` - Date values
|
|
108
|
+
- `TimeColumn` - Time values
|
|
109
|
+
- `DateTimeColumn` - DateTime values
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
See [docs/](docs/) for detailed documentation and examples.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT License © 2015 Umbrella Tech
|
|
118
|
+
|
|
119
|
+
## Author
|
|
120
|
+
|
|
121
|
+
Kelson da Costa Medeiros <kelsoncm@gmail.com>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyfwf/__init__.py,sha256=Wb7sizlpkPQHXq2zf7grp-9YY7Mxram7EoOcR2FEmnk,1148
|
|
2
|
+
pyfwf/columns.py,sha256=gE7oyil_l3NP8KQPLIsgDQkpKCSnP-VIpSsr3jTKjcI,8638
|
|
3
|
+
pyfwf/descriptors.py,sha256=ysVGsnoI-LJGnvsL4OKSIqFp8-lm0LtW-I_CjssXJX0,4185
|
|
4
|
+
pyfwf/hydrating.py,sha256=jjltdWoagfgyYK7U5Q7TKko1H2NMj2Jq-P_hJWQFmfY,4023
|
|
5
|
+
pyfwf/readers.py,sha256=u-Yg4u7ZsHPOz4jx5aH_gUi3SFBtpEHfMt8o_H425Sc,3755
|
|
6
|
+
pyfwf/renders.py,sha256=276NHTihjJtHgEjT4_pvjj502y1JiGuYorsYr1xIvHc,2961
|
|
7
|
+
pyfwf-1.0.0.dist-info/licenses/LICENSE.md,sha256=Ua0hpApaWay1AcvGvjg3fwF8oPX4wS8Qp8s8qN6VOcc,1076
|
|
8
|
+
pyfwf-1.0.0.dist-info/METADATA,sha256=YuuQxjdhMebbGDGcpYgPyiFLvB5K0u3TAdgGt7l_RmM,4607
|
|
9
|
+
pyfwf-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
pyfwf-1.0.0.dist-info/top_level.txt,sha256=S1Nj1uO3Wl86B1rCeozK6iBwnp7c4c7IwiUEBQ551pc,6
|
|
11
|
+
pyfwf-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 kelsoncm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyfwf
|