Coverage for gws-app/gws/plugin/csv_helper/__init__.py: 0%

82 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""Common csv writer helper.""" 

2 

3from typing import Optional 

4 

5import decimal 

6import datetime 

7 

8import gws 

9import gws.lib.intl 

10 

11gws.ext.new.helper('csv') 

12 

13 

14class FormatConfig(gws.Config): 

15 """CSV format settings""" 

16 

17 delimiter: str = ',' 

18 """Field delimiter.""" 

19 encoding: str = 'utf8' 

20 """Text encoding.""" 

21 formulaHack: bool = True 

22 """Prepend numeric strings with an equals sign.""" 

23 quote: str = '"' 

24 """Quote character.""" 

25 quoteAll: bool = False 

26 """Quote all fields.""" 

27 rowDelimiter: str = 'LF' 

28 """Row delimiter.""" 

29 

30 

31class Config(gws.Config): 

32 """CSV helper.""" 

33 

34 format: FormatConfig 

35 """CSV format settings.""" 

36 

37 

38class Format(gws.Data): 

39 delimiter: str 

40 encoding: str 

41 formulaHack: bool 

42 quote: str 

43 quoteAll: bool 

44 rowDelimiter: str 

45 

46 

47class Object(gws.Node): 

48 format: Format 

49 

50 def configure(self): 

51 self.format = Format( 

52 delimiter=self.cfg('format.delimiter', default=','), 

53 encoding=self.cfg('format.encoding', default='utf8'), 

54 formulaHack=self.cfg('format.formulaHack', default=True), 

55 quote=self.cfg('format.quote', default='"'), 

56 quoteAll=self.cfg('format.quoteAll', default=False), 

57 rowDelimiter=self.cfg('format.rowDelimiter', default='LF').replace('CR', '\r').replace('LF', '\n'), 

58 ) 

59 

60 def writer(self, locale: gws.Locale): 

61 """Creates a new csv Writer. 

62 

63 Args: 

64 locale: Locale to use. 

65 """ 

66 

67 return _Writer(self, locale) 

68 

69 

70class _Writer: 

71 def __init__(self, helper, locale: gws.Locale): 

72 self.helper: Object = helper 

73 self.format = self.helper.format 

74 

75 self.rows = [] 

76 self.headers = '' 

77 

78 f = gws.lib.intl.formatters(locale) 

79 self.dateFormatter = f[0] 

80 self.timeFormatter = f[1] 

81 self.numberFormatter = f[2] 

82 

83 def write_headers(self, headers: list[str]): 

84 """Writes headers into the header attribute. 

85 

86 Args: 

87 headers: Multiple header names. 

88 """ 

89 

90 self.headers = self.format.delimiter.join(self._quote(s) for s in headers) 

91 return self 

92 

93 def write_row(self, row: list): 

94 """Writes entries into rows attribute. 

95 

96 Args: 

97 row: Row entries. 

98 """ 

99 self.rows.append(self.format.delimiter.join(self._format(v) for v in row)) 

100 return self 

101 

102 def to_str(self): 

103 """Converts the headers and rows to a CSV string.""" 

104 

105 rows = [] 

106 if self.headers: 

107 rows.append(self.headers) 

108 rows.extend(self.rows) 

109 return self.format.rowDelimiter.join(rows) 

110 

111 def to_bytes(self, encoding=None): 

112 """Converts the table the writer object describes to a CSV byte string.""" 

113 

114 return self.to_str().encode(encoding or self.format.encoding, errors='replace') 

115 

116 def _format(self, val): 

117 if val is None: 

118 return self._quote('') 

119 

120 if isinstance(val, (float, decimal.Decimal)): 

121 s = self.numberFormatter.decimal(val) 

122 return self._quote(s) if self.format.quoteAll else s 

123 

124 if isinstance(val, int): 

125 s = str(val) 

126 return self._quote(s) if self.format.quoteAll else s 

127 

128 if isinstance(val, (datetime.datetime, datetime.date)): 

129 s = self.dateFormatter.short(val) 

130 return self._quote(s) 

131 

132 if isinstance(val, datetime.time): 

133 s = self.timeFormatter.short(val) 

134 return self._quote(s) 

135 

136 val = gws.u.to_str(val) 

137 

138 if val and val.isdigit() and self.format.formulaHack: 

139 val = '=' + self._quote(val) 

140 

141 return self._quote(val) 

142 

143 def _quote(self, val): 

144 q = self.format.quote 

145 s = gws.u.to_str(val).replace(q, q + q) 

146 return q + s + q