stage-foundary 0.1.4__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.
- stage_foundary/__init__.py +9 -0
- stage_foundary/csv_stages.py +247 -0
- stage_foundary/document_structure.py +146 -0
- stage_foundary/jira_parser.py +598 -0
- stage_foundary/jira_stages.py +468 -0
- stage_foundary/odt_stages.py +0 -0
- stage_foundary/reports.py +268 -0
- stage_foundary/stages.py +358 -0
- stage_foundary/teetime.py +278 -0
- stage_foundary/xls_stages.py +59 -0
- stage_foundary-0.1.4.dist-info/LICENSE +201 -0
- stage_foundary-0.1.4.dist-info/METADATA +20 -0
- stage_foundary-0.1.4.dist-info/RECORD +15 -0
- stage_foundary-0.1.4.dist-info/WHEEL +5 -0
- stage_foundary-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from stage_foundary.teetime import ConsumerStage, ProducerStage, SinkStage
|
|
7
|
+
|
|
8
|
+
##########################################
|
|
9
|
+
# Generic functions
|
|
10
|
+
##########################################
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CsvException(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Abstract csv record processing
|
|
19
|
+
class AbstractCsvRecordProcessing:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
logger: logging.Logger,
|
|
23
|
+
delimiter: str,
|
|
24
|
+
skip_header: int,
|
|
25
|
+
use_header: bool,
|
|
26
|
+
encoding: str = "utf-8",
|
|
27
|
+
):
|
|
28
|
+
self.logger = logger
|
|
29
|
+
self.skip_header = skip_header
|
|
30
|
+
self.delimiter = delimiter
|
|
31
|
+
self.use_header = use_header
|
|
32
|
+
self.encoding = encoding
|
|
33
|
+
self.columns: dict[str, list[int]] = {}
|
|
34
|
+
|
|
35
|
+
def read_file(self, filename: str):
|
|
36
|
+
self.logger.info(f"reading {filename}")
|
|
37
|
+
self.current_filename = filename
|
|
38
|
+
with open(filename, newline="", encoding=self.encoding) as csv_file:
|
|
39
|
+
try:
|
|
40
|
+
reader = csv.reader(csv_file, delimiter=self.delimiter, quotechar='"')
|
|
41
|
+
|
|
42
|
+
self.columns = {}
|
|
43
|
+
self.process_start(filename)
|
|
44
|
+
for i, row in enumerate(reader):
|
|
45
|
+
if self.skip_header > i:
|
|
46
|
+
if i == 0 and self.use_header:
|
|
47
|
+
for j, column_name in enumerate(row):
|
|
48
|
+
column = self.columns.get(column_name)
|
|
49
|
+
if column is None:
|
|
50
|
+
self.columns[column_name] = [j]
|
|
51
|
+
else:
|
|
52
|
+
self.columns[column_name].append(j)
|
|
53
|
+
else:
|
|
54
|
+
self.process_row(row)
|
|
55
|
+
|
|
56
|
+
self.process_complete(filename)
|
|
57
|
+
except UnicodeDecodeError as e:
|
|
58
|
+
self.logger.error(
|
|
59
|
+
f"The file '{filename}' contains an UTF error in [{i}] {e}"
|
|
60
|
+
)
|
|
61
|
+
except FileNotFoundError as e:
|
|
62
|
+
self.logger.error(f"The file '{filename}' does not exist. {e}")
|
|
63
|
+
sys.exit(-1)
|
|
64
|
+
|
|
65
|
+
def get_value(self, name: str, row: list, optional=None) -> list:
|
|
66
|
+
columns: list[int] | None = self.columns.get(name)
|
|
67
|
+
if columns is None:
|
|
68
|
+
if optional is None:
|
|
69
|
+
self.logger.error(
|
|
70
|
+
f"Column name {name} not found in CSV file {self.current_filename}"
|
|
71
|
+
)
|
|
72
|
+
raise CsvException(
|
|
73
|
+
f"Column name {name} not found in CSV file {self.current_filename}"
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
return [optional]
|
|
77
|
+
result = []
|
|
78
|
+
for column in columns:
|
|
79
|
+
if len(row) > column:
|
|
80
|
+
result.append(row[column])
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def get_scalar_value(self, name: str, row: list, optional=None) -> any:
|
|
84
|
+
result = self.get_value(name, row, optional)
|
|
85
|
+
if len(result) == 1:
|
|
86
|
+
return result[0]
|
|
87
|
+
elif len(result) > 1:
|
|
88
|
+
raise CsvException("scalar expected, but list found.")
|
|
89
|
+
else:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def get_date_value(self, name: str, row):
|
|
93
|
+
date_string = self.get_scalar_value(name, row)
|
|
94
|
+
if date_string is None or len(date_string) == 0:
|
|
95
|
+
return None
|
|
96
|
+
else:
|
|
97
|
+
return datetime.datetime.strptime(date_string, "%d.%m.%Y %H:%M")
|
|
98
|
+
|
|
99
|
+
def get_int_value(self, name: str, row: list, optional=None) -> int:
|
|
100
|
+
try:
|
|
101
|
+
return self.convert_int(self.get_scalar_value(name, row, optional))
|
|
102
|
+
except CsvException as e:
|
|
103
|
+
if optional is not None:
|
|
104
|
+
self.logger.warning(f"Error for attribute {name}: {e}")
|
|
105
|
+
return optional
|
|
106
|
+
else:
|
|
107
|
+
self.logger.error(f"Error for attribute {name}: {e}")
|
|
108
|
+
raise CsvException("Error for attribute {name}: {e}")
|
|
109
|
+
except ValueError as e:
|
|
110
|
+
if optional is not None:
|
|
111
|
+
self.logger.warning(f"Error for attribute {name}: {e}")
|
|
112
|
+
return optional
|
|
113
|
+
else:
|
|
114
|
+
self.logger.error(f"Error for attribute {name}: {e}")
|
|
115
|
+
raise CsvException("Error for attribute {name}: {e}")
|
|
116
|
+
|
|
117
|
+
def get_float_value(self, name: str, row: list, optional=None) -> float:
|
|
118
|
+
try:
|
|
119
|
+
return self.convert_float(self.get_scalar_value(name, row, optional))
|
|
120
|
+
except CsvException as e:
|
|
121
|
+
if optional is not None:
|
|
122
|
+
self.logger.warning(f"Error for attribute {name}: {e}")
|
|
123
|
+
return optional
|
|
124
|
+
else:
|
|
125
|
+
self.logger.error(f"Error for attribute {name}: {e}")
|
|
126
|
+
raise CsvException("Error for attribute {name}: {e}")
|
|
127
|
+
except ValueError as e:
|
|
128
|
+
if optional is not None:
|
|
129
|
+
self.logger.warning(f"Error for attribute {name}: {e}")
|
|
130
|
+
return optional
|
|
131
|
+
else:
|
|
132
|
+
self.logger.error(f"Error for attribute {name}: {e}")
|
|
133
|
+
raise CsvException("Error for attribute {name}: {e}")
|
|
134
|
+
|
|
135
|
+
def process_row(self, row: list) -> None:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def process_start(self, filename: str):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
def process_complete(self, filename: str):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def convert_int(self, value: str) -> int:
|
|
145
|
+
if value is not None and isinstance(value, str):
|
|
146
|
+
if value.strip() != "":
|
|
147
|
+
return int(value)
|
|
148
|
+
else:
|
|
149
|
+
raise CsvException("Empty value is not an integer number")
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
def convert_float(self, value: str) -> float:
|
|
153
|
+
if value is not None and isinstance(value, str):
|
|
154
|
+
if value.strip() != "":
|
|
155
|
+
return float(value)
|
|
156
|
+
else:
|
|
157
|
+
raise CsvException("Empty value is not an floating point number")
|
|
158
|
+
return 0.0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
#
|
|
162
|
+
# Abstract record producer for CSV files
|
|
163
|
+
#
|
|
164
|
+
class AbstractCsvRecordProducer(ProducerStage, AbstractCsvRecordProcessing):
|
|
165
|
+
# @param skip_header = skip number of lines from the header
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
logger: logging.Logger,
|
|
169
|
+
filenames,
|
|
170
|
+
delimiter: str = ";",
|
|
171
|
+
skip_header: int = 1,
|
|
172
|
+
use_header: bool = False,
|
|
173
|
+
encoding: str = "utf-8",
|
|
174
|
+
):
|
|
175
|
+
ProducerStage.__init__(self, logger)
|
|
176
|
+
AbstractCsvRecordProcessing.__init__(
|
|
177
|
+
self,
|
|
178
|
+
logger=logger,
|
|
179
|
+
delimiter=delimiter,
|
|
180
|
+
skip_header=skip_header,
|
|
181
|
+
use_header=use_header,
|
|
182
|
+
encoding=encoding,
|
|
183
|
+
)
|
|
184
|
+
self.filenames = filenames
|
|
185
|
+
self.columns: dict[str, list[int]] = {}
|
|
186
|
+
|
|
187
|
+
def execute(self):
|
|
188
|
+
if isinstance(self.filenames, str):
|
|
189
|
+
self.read_file(filename=self.filenames)
|
|
190
|
+
else:
|
|
191
|
+
for filename in self.filenames:
|
|
192
|
+
self.read_file(filename=filename)
|
|
193
|
+
self.work_complete()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
#
|
|
197
|
+
# Abstract record reader for CSV files
|
|
198
|
+
#
|
|
199
|
+
class AbstractCsvRecordReader(ConsumerStage, AbstractCsvRecordProcessing):
|
|
200
|
+
# @param skip_header = skip number of lines from the header
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
logger: logging.Logger,
|
|
204
|
+
delimiter: str = ";",
|
|
205
|
+
skip_header: int = 1,
|
|
206
|
+
use_header: bool = False,
|
|
207
|
+
encoding: str = "utf-8",
|
|
208
|
+
):
|
|
209
|
+
ConsumerStage.__init__(self, logger)
|
|
210
|
+
AbstractCsvRecordProcessing.__init__(
|
|
211
|
+
self,
|
|
212
|
+
logger=logger,
|
|
213
|
+
delimiter=delimiter,
|
|
214
|
+
skip_header=skip_header,
|
|
215
|
+
use_header=use_header,
|
|
216
|
+
encoding=encoding,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def consume(self, filename: str):
|
|
220
|
+
self.columns: dict[str, list[int]] = {}
|
|
221
|
+
self.read_file(filename=filename)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
############################################
|
|
225
|
+
# Writer
|
|
226
|
+
############################################
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class AbstractCsvRecordSink(SinkStage):
|
|
230
|
+
def __init__(
|
|
231
|
+
self, logger: logging.Logger, filename: str, delimiter: str, fieldnames: list
|
|
232
|
+
):
|
|
233
|
+
super().__init__(logger)
|
|
234
|
+
with open(filename, "w", newline="") as self.output_file:
|
|
235
|
+
self.csv_writer = csv.writer(
|
|
236
|
+
self.output_file,
|
|
237
|
+
delimiter=delimiter,
|
|
238
|
+
quotechar='"',
|
|
239
|
+
quoting=csv.QUOTE_ALL,
|
|
240
|
+
)
|
|
241
|
+
self.csv_writer.writerow(fieldnames)
|
|
242
|
+
|
|
243
|
+
def consume(self, entry):
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
def on_terminate(self):
|
|
247
|
+
self.output_file.close()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on Mon Mar 30 08:32:13 2026
|
|
3
|
+
|
|
4
|
+
@author: Reiner Jung
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EHorizontalOrientation(Enum):
|
|
12
|
+
LEFT = 1
|
|
13
|
+
CENTER = 2
|
|
14
|
+
RIGHT = 3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EVerticalOrientation(Enum):
|
|
18
|
+
TOP = 1
|
|
19
|
+
CENTER = 2
|
|
20
|
+
BOTTOM = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class AbstractPageElement:
|
|
25
|
+
def print(self, indent):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Table(AbstractPageElement):
|
|
31
|
+
header: list # [str]
|
|
32
|
+
orientations: list # [EHorizontalOrientation]
|
|
33
|
+
widths: list # [str]
|
|
34
|
+
rows: list # [list[AbstractPageElement]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Container(AbstractPageElement):
|
|
39
|
+
children: list
|
|
40
|
+
|
|
41
|
+
def print(self, indent):
|
|
42
|
+
for item in self.children:
|
|
43
|
+
item.print(f" {indent}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Page(Container):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PlainText(AbstractPageElement):
|
|
53
|
+
text: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Link(Container):
|
|
58
|
+
link: str
|
|
59
|
+
format: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Headline(Container):
|
|
64
|
+
level: int
|
|
65
|
+
|
|
66
|
+
def print(self, indent):
|
|
67
|
+
print(f"{indent}Headline {self.level}")
|
|
68
|
+
super().print(indent)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class NoFormat(Container):
|
|
73
|
+
def print(self, indent):
|
|
74
|
+
print(f"{indent}No Format")
|
|
75
|
+
super().print(indent)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class CodeLine(PlainText):
|
|
80
|
+
def print(self, indent):
|
|
81
|
+
print(f"{indent}CODE {self.text}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Code(Container):
|
|
86
|
+
def print(self, indent):
|
|
87
|
+
print(f"{indent}Code")
|
|
88
|
+
super().print(indent)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Paragraph(Container):
|
|
93
|
+
def print(self, indent):
|
|
94
|
+
print(f"{indent}Paragraph")
|
|
95
|
+
super().print(indent)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Line(Container):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Span(Container):
|
|
105
|
+
format: str
|
|
106
|
+
|
|
107
|
+
def print(self, indent):
|
|
108
|
+
print(f"{indent}Span {self.format}")
|
|
109
|
+
super().print(indent + " ")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class Item(Container):
|
|
114
|
+
level: int
|
|
115
|
+
|
|
116
|
+
def print(self, indent):
|
|
117
|
+
print(f"{indent}LI {self.level}")
|
|
118
|
+
super().print(f"{indent} ")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class Enumeration(Container):
|
|
123
|
+
level: int
|
|
124
|
+
|
|
125
|
+
def print(self, indent):
|
|
126
|
+
print(f"{indent}LI {self.level}")
|
|
127
|
+
super().print(f"{indent} ")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class ItemList(Container):
|
|
132
|
+
def print(self, indent):
|
|
133
|
+
print(f"{indent}List")
|
|
134
|
+
super().print(indent)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class EnumerationList(Container):
|
|
139
|
+
def print(self, indent):
|
|
140
|
+
print(f"{indent}Enumeration")
|
|
141
|
+
super().print(indent)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class Newline(AbstractPageElement):
|
|
146
|
+
pass
|