foo-py 0.1.1__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.
Files changed (242) hide show
  1. agents.py +10690 -0
  2. app.py +16234 -0
  3. boogr/__init__.py +563 -0
  4. boogr/default_icon.ico +0 -0
  5. boogr/enums.py +381 -0
  6. boogr/minion.py +175 -0
  7. boogr/resources/ico/BooIcon.ico +0 -0
  8. boogr/resources/ico/Booger.ico +0 -0
  9. boogr/resources/ico/Save.ico +0 -0
  10. boogr/resources/ico/adobe.ico +0 -0
  11. boogr/resources/ico/atk.ico +0 -0
  12. boogr/resources/ico/b.ico +0 -0
  13. boogr/resources/ico/batch.ico +0 -0
  14. boogr/resources/ico/black_sigma.ico +0 -0
  15. boogr/resources/ico/boo.ico +0 -0
  16. boogr/resources/ico/boogr.ico +0 -0
  17. boogr/resources/ico/browse.ico +0 -0
  18. boogr/resources/ico/chart.ico +0 -0
  19. boogr/resources/ico/copy.ico +0 -0
  20. boogr/resources/ico/csv.ico +0 -0
  21. boogr/resources/ico/dataedit.ico +0 -0
  22. boogr/resources/ico/doc.ico +0 -0
  23. boogr/resources/ico/e_logo.ico +0 -0
  24. boogr/resources/ico/error.ico +0 -0
  25. boogr/resources/ico/euler_circle.ico +0 -0
  26. boogr/resources/ico/excel.ico +0 -0
  27. boogr/resources/ico/file_browse.ico +0 -0
  28. boogr/resources/ico/filter.ico +0 -0
  29. boogr/resources/ico/folder_browse.ico +0 -0
  30. boogr/resources/ico/info.ico +0 -0
  31. boogr/resources/ico/input.ico +0 -0
  32. boogr/resources/ico/koolaid.ico +0 -0
  33. boogr/resources/ico/machinelearning.ico +0 -0
  34. boogr/resources/ico/message.ico +0 -0
  35. boogr/resources/ico/pdf.ico +0 -0
  36. boogr/resources/ico/pi.ico +0 -0
  37. boogr/resources/ico/setting.ico +0 -0
  38. boogr/resources/ico/sword_ninja.ico +0 -0
  39. boogr/resources/ico/textfile.ico +0 -0
  40. boogr/resources/ico/webcam.ico +0 -0
  41. boogr/resources/img/atk.png +0 -0
  42. boogr/resources/img/boogr.png +0 -0
  43. boogr/resources/img/button/Authority.png +0 -0
  44. boogr/resources/img/button/BOC.png +0 -0
  45. boogr/resources/img/button/DERA.png +0 -0
  46. boogr/resources/img/button/DWH.png +0 -0
  47. boogr/resources/img/button/EMD.png +0 -0
  48. boogr/resources/img/button/OAR.png +0 -0
  49. boogr/resources/img/button/OECA.png +0 -0
  50. boogr/resources/img/button/OGC.png +0 -0
  51. boogr/resources/img/button/OMS.png +0 -0
  52. boogr/resources/img/button/ORD.png +0 -0
  53. boogr/resources/img/button/OW.png +0 -0
  54. boogr/resources/img/button/RCRA.png +0 -0
  55. boogr/resources/img/button/TSCA.png +0 -0
  56. boogr/resources/img/button/WIFIA.png +0 -0
  57. boogr/resources/img/button/access.png +0 -0
  58. boogr/resources/img/button/add.png +0 -0
  59. boogr/resources/img/button/adobe.png +0 -0
  60. boogr/resources/img/button/airline.png +0 -0
  61. boogr/resources/img/button/analytics.png +0 -0
  62. boogr/resources/img/button/appropriation.png +0 -0
  63. boogr/resources/img/button/atk.ico +0 -0
  64. boogr/resources/img/button/atk.png +0 -0
  65. boogr/resources/img/button/attachment.png +0 -0
  66. boogr/resources/img/button/bfy.png +0 -0
  67. boogr/resources/img/button/bluetooth.png +0 -0
  68. boogr/resources/img/button/browse.png +0 -0
  69. boogr/resources/img/button/budget.png +0 -0
  70. boogr/resources/img/button/calculator.png +0 -0
  71. boogr/resources/img/button/calendar.png +0 -0
  72. boogr/resources/img/button/cancel.png +0 -0
  73. boogr/resources/img/button/categoricalgrants.png +0 -0
  74. boogr/resources/img/button/chart.png +0 -0
  75. boogr/resources/img/button/chrome.png +0 -0
  76. boogr/resources/img/button/close.png +0 -0
  77. boogr/resources/img/button/columndelete.png +0 -0
  78. boogr/resources/img/button/columnedit.png +0 -0
  79. boogr/resources/img/button/columninsert.png +0 -0
  80. boogr/resources/img/button/commandline.png +0 -0
  81. boogr/resources/img/button/commute.png +0 -0
  82. boogr/resources/img/button/compass.png +0 -0
  83. boogr/resources/img/button/contracts.png +0 -0
  84. boogr/resources/img/button/controlpanel.png +0 -0
  85. boogr/resources/img/button/csv.png +0 -0
  86. boogr/resources/img/button/database.png +0 -0
  87. boogr/resources/img/button/databaseadd.png +0 -0
  88. boogr/resources/img/button/databasedelete.png +0 -0
  89. boogr/resources/img/button/databaserefresh.png +0 -0
  90. boogr/resources/img/button/databasesql.png +0 -0
  91. boogr/resources/img/button/databaseverify.png +0 -0
  92. boogr/resources/img/button/datagrid.png +0 -0
  93. boogr/resources/img/button/delete.png +0 -0
  94. boogr/resources/img/button/division.png +0 -0
  95. boogr/resources/img/button/document.png +0 -0
  96. boogr/resources/img/button/documentadd.png +0 -0
  97. boogr/resources/img/button/documentation.png +0 -0
  98. boogr/resources/img/button/documentdelete.png +0 -0
  99. boogr/resources/img/button/documentedit.png +0 -0
  100. boogr/resources/img/button/documenterror.png +0 -0
  101. boogr/resources/img/button/documentsearch.png +0 -0
  102. boogr/resources/img/button/edge.png +0 -0
  103. boogr/resources/img/button/edit.png +0 -0
  104. boogr/resources/img/button/efy.png +0 -0
  105. boogr/resources/img/button/environment.png +0 -0
  106. boogr/resources/img/button/ev.png +0 -0
  107. boogr/resources/img/button/excel.png +0 -0
  108. boogr/resources/img/button/expenses.png +0 -0
  109. boogr/resources/img/button/export.png +0 -0
  110. boogr/resources/img/button/file.png +0 -0
  111. boogr/resources/img/button/file_word.png +0 -0
  112. boogr/resources/img/button/fileadd.png +0 -0
  113. boogr/resources/img/button/filebrowse.png +0 -0
  114. boogr/resources/img/button/filecopy.png +0 -0
  115. boogr/resources/img/button/filedelete.png +0 -0
  116. boogr/resources/img/button/fileedit.png +0 -0
  117. boogr/resources/img/button/filereader.png +0 -0
  118. boogr/resources/img/button/filesearch.png +0 -0
  119. boogr/resources/img/button/filetransfer.png +0 -0
  120. boogr/resources/img/button/fileverify.png +0 -0
  121. boogr/resources/img/button/filewriter.png +0 -0
  122. boogr/resources/img/button/filter.png +0 -0
  123. boogr/resources/img/button/first.png +0 -0
  124. boogr/resources/img/button/folder.png +0 -0
  125. boogr/resources/img/button/folderbrowse.png +0 -0
  126. boogr/resources/img/button/foldercompress.png +0 -0
  127. boogr/resources/img/button/foldercopy.png +0 -0
  128. boogr/resources/img/button/folderdownload.png +0 -0
  129. boogr/resources/img/button/folderopen.png +0 -0
  130. boogr/resources/img/button/fte.png +0 -0
  131. boogr/resources/img/button/function.png +0 -0
  132. boogr/resources/img/button/gmail.png +0 -0
  133. boogr/resources/img/button/go.png +0 -0
  134. boogr/resources/img/button/google.png +0 -0
  135. boogr/resources/img/button/grants.png +0 -0
  136. boogr/resources/img/button/guidance.png +0 -0
  137. boogr/resources/img/button/home.png +0 -0
  138. boogr/resources/img/button/id.png +0 -0
  139. boogr/resources/img/button/image.png +0 -0
  140. boogr/resources/img/button/import.png +0 -0
  141. boogr/resources/img/button/information.png +0 -0
  142. boogr/resources/img/button/internet.png +0 -0
  143. boogr/resources/img/button/justice.png +0 -0
  144. boogr/resources/img/button/last.png +0 -0
  145. boogr/resources/img/button/ledger.png +0 -0
  146. boogr/resources/img/button/left.png +0 -0
  147. boogr/resources/img/button/levels.png +0 -0
  148. boogr/resources/img/button/logout.png +0 -0
  149. boogr/resources/img/button/lust.png +0 -0
  150. boogr/resources/img/button/menu.png +0 -0
  151. boogr/resources/img/button/metrics.png +0 -0
  152. boogr/resources/img/button/mpg.png +0 -0
  153. boogr/resources/img/button/next.png +0 -0
  154. boogr/resources/img/button/no.png +0 -0
  155. boogr/resources/img/button/oil.png +0 -0
  156. boogr/resources/img/button/ok.png +0 -0
  157. boogr/resources/img/button/omb.png +0 -0
  158. boogr/resources/img/button/onenote.png +0 -0
  159. boogr/resources/img/button/oust.png +0 -0
  160. boogr/resources/img/button/outlay.png +0 -0
  161. boogr/resources/img/button/outlook.png +0 -0
  162. boogr/resources/img/button/pause.png +0 -0
  163. boogr/resources/img/button/payroll.png +0 -0
  164. boogr/resources/img/button/pdf.png +0 -0
  165. boogr/resources/img/button/percentage.png +0 -0
  166. boogr/resources/img/button/play.png +0 -0
  167. boogr/resources/img/button/plusminus.png +0 -0
  168. boogr/resources/img/button/previous.png +0 -0
  169. boogr/resources/img/button/print.png +0 -0
  170. boogr/resources/img/button/recertification.png +0 -0
  171. boogr/resources/img/button/recycle.png +0 -0
  172. boogr/resources/img/button/redo.png +0 -0
  173. boogr/resources/img/button/refresh.png +0 -0
  174. boogr/resources/img/button/remove.png +0 -0
  175. boogr/resources/img/button/reserve.png +0 -0
  176. boogr/resources/img/button/right.png +0 -0
  177. boogr/resources/img/button/row.png +0 -0
  178. boogr/resources/img/button/rowcopy.png +0 -0
  179. boogr/resources/img/button/rowdelete.png +0 -0
  180. boogr/resources/img/button/rowedit.png +0 -0
  181. boogr/resources/img/button/rowinsert.png +0 -0
  182. boogr/resources/img/button/save.png +0 -0
  183. boogr/resources/img/button/scan.png +0 -0
  184. boogr/resources/img/button/sharepoint.png +0 -0
  185. boogr/resources/img/button/sigma.png +0 -0
  186. boogr/resources/img/button/site.png +0 -0
  187. boogr/resources/img/button/sitetravel.png +0 -0
  188. boogr/resources/img/button/sort.png +0 -0
  189. boogr/resources/img/button/spreadsheet.png +0 -0
  190. boogr/resources/img/button/statistics.png +0 -0
  191. boogr/resources/img/button/table.png +0 -0
  192. boogr/resources/img/button/tableadd.png +0 -0
  193. boogr/resources/img/button/tabledelete.png +0 -0
  194. boogr/resources/img/button/tablesettings.png +0 -0
  195. boogr/resources/img/button/text.png +0 -0
  196. boogr/resources/img/button/traffic.png +0 -0
  197. boogr/resources/img/button/travel.png +0 -0
  198. boogr/resources/img/button/undelete.png +0 -0
  199. boogr/resources/img/button/undo.png +0 -0
  200. boogr/resources/img/button/wcf.png +0 -0
  201. boogr/resources/img/button/windows.png +0 -0
  202. boogr/resources/img/button/word.png +0 -0
  203. boogr/resources/img/button/xml.png +0 -0
  204. boogr/resources/img/button/yes.png +0 -0
  205. boogr/resources/img/button/zipfile.png +0 -0
  206. boogr/resources/img/gooey.png +0 -0
  207. boogr/resources/img/web/google.png +0 -0
  208. config.py +1015 -0
  209. core.py +170 -0
  210. data.py +832 -0
  211. desktop.py +173 -0
  212. embedders.py +269 -0
  213. fetchers.py +25142 -0
  214. foo_assets/__init__.py +1 -0
  215. foo_assets/resources/images/favicon.ico +0 -0
  216. foo_assets/resources/images/foo-apikeys.png +0 -0
  217. foo_assets/resources/images/foo-architecture.png +0 -0
  218. foo_assets/resources/images/foo-workflows.png +0 -0
  219. foo_assets/resources/images/foo.ico +0 -0
  220. foo_assets/resources/images/foo.svg +60 -0
  221. foo_assets/resources/images/foo_logo.ico +0 -0
  222. foo_assets/resources/images/foo_logo.png +0 -0
  223. foo_assets/resources/images/foo_logo.svg +63 -0
  224. foo_assets/resources/images/foo_portfolio.png +0 -0
  225. foo_assets/resources/images/foo_project.png +0 -0
  226. foo_assets/resources/images/system_diagram.svg +46 -0
  227. foo_assets/resources/images/uml_class_diagram.svg +65 -0
  228. foo_assets/streamlit_config.toml +85 -0
  229. foo_cli.py +52 -0
  230. foo_py-0.1.1.dist-info/METADATA +596 -0
  231. foo_py-0.1.1.dist-info/RECORD +242 -0
  232. foo_py-0.1.1.dist-info/WHEEL +5 -0
  233. foo_py-0.1.1.dist-info/entry_points.txt +2 -0
  234. foo_py-0.1.1.dist-info/licenses/LICENSE.txt +21 -0
  235. foo_py-0.1.1.dist-info/top_level.txt +18 -0
  236. generators.py +3323 -0
  237. loaders.py +4570 -0
  238. models.py +344 -0
  239. processors.py +3216 -0
  240. scrapers.py +706 -0
  241. stores/vector.py +365 -0
  242. writers.py +268 -0
boogr/__init__.py ADDED
@@ -0,0 +1,563 @@
1
+ '''
2
+ ******************************************************************************************
3
+ Assembly: boogr
4
+ Filename: init.py
5
+ Author: Terry D. Eppler
6
+ Created: 05-31-2022
7
+
8
+ Last Modified By: Terry D. Eppler
9
+ Last Modified On: 05-01-2025
10
+ ******************************************************************************************
11
+ <copyright file="init.py" company="Terry D. Eppler">
12
+
13
+ init.py is part of a data analysis tool integrating GenAI, Text Processing,
14
+ and Machine-Learning algorithms for federal analysts.
15
+ Copyright © 2022 Terry Eppler
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the “Software”),
19
+ to deal in the Software without restriction,
20
+ including without limitation the rights to use,
21
+ copy, modify, merge, publish, distribute, sublicense,
22
+ and/or sell copies of the Software,
23
+ and to permit persons to whom the Software is furnished to do so,
24
+ subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in all
27
+ copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
30
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
32
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
33
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
34
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
35
+ DEALINGS IN THE SOFTWARE.
36
+
37
+ You can contact me at: terryeppler@gmail.com or eppler.terry@epa.gov
38
+
39
+ </copyright>
40
+ <summary>
41
+ init.py
42
+ </summary>
43
+ ******************************************************************************************
44
+ '''
45
+ from __future__ import annotations
46
+ from pathlib import Path
47
+ import traceback
48
+ from sys import exc_info
49
+ from typing import List, Optional
50
+ import re
51
+ import sqlite3
52
+ import config as cfg
53
+ import datetime
54
+ from datetime import timedelta
55
+
56
+ def get_config_bool( name: str, default: bool = False ) -> bool:
57
+ """Read a Boolean value from the configuration module.
58
+
59
+ Purpose:
60
+ Safely read a Boolean configuration value without requiring every deployment of Fiddy to
61
+ define the newest configuration switches. Missing values return the supplied default.
62
+
63
+ Args:
64
+ name (str): Configuration attribute name.
65
+ default (bool): Default value used when the attribute is not available.
66
+
67
+ Returns:
68
+ bool: Configuration Boolean value or the supplied default.
69
+ """
70
+ try:
71
+ value = getattr( cfg, name, default )
72
+ return bool( value )
73
+ except Exception:
74
+ return default
75
+
76
+ def get_config_int( name: str, default: int ) -> int:
77
+ """Read an integer value from the configuration module.
78
+
79
+ Purpose:
80
+ Safely read an integer configuration value without requiring every deployment of Fiddy to
81
+ define the newest configuration switches. Missing or invalid values return the supplied
82
+ default.
83
+
84
+ Args:
85
+ name (str): Configuration attribute name.
86
+ default (int): Default value used when the attribute is not available or invalid.
87
+
88
+ Returns:
89
+ int: Configuration integer value or the supplied default.
90
+ """
91
+ try:
92
+ value = getattr( cfg, name, default )
93
+ return int( value )
94
+ except Exception:
95
+ return default
96
+
97
+ def sanitize_text( value: object, max_length: int = 1000 ) -> str:
98
+ """Return a reviewer-safe and log-safe text representation.
99
+
100
+ Purpose:
101
+ Remove or mask sensitive values before any exception metadata is persisted. This helper
102
+ masks email addresses, Windows paths, POSIX paths, temporary-file paths, common uploaded
103
+ file references, long quoted payloads, long comma-delimited manifest-like rows, and long
104
+ free-form OCR-like text. Raw text logging can only be enabled explicitly through
105
+ ``cfg.ENABLE_RAW_TEXT_LOGGING``.
106
+
107
+ Args:
108
+ value (object): Source value to sanitize.
109
+ max_length (int): Maximum returned string length after masking.
110
+
111
+ Returns:
112
+ str: Sanitized text value suitable for local diagnostic logging.
113
+ """
114
+ try:
115
+ if value is None:
116
+ return ''
117
+
118
+ text = str( value )
119
+ text = text.replace( '\x00', '' )
120
+ text = re.sub( r'[\r\n\t]+', ' ', text )
121
+ text = re.sub( r'\s{2,}', ' ', text ).strip( )
122
+
123
+ if not get_config_bool( 'ENABLE_FILE_PATH_LOGGING', False ):
124
+ text = re.sub( r'[A-Za-z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*', '[PATH]',
125
+ text )
126
+ text = re.sub( r'/(?:tmp|mnt|var|home|Users|user|workspace|app|data)(?:/[^\s,;:)]*)+',
127
+ '[PATH]', text )
128
+ text = re.sub( r'\\(?:tmp|temp|users|appdata|programdata)\\[^\s,;:)]*', '[PATH]', text,
129
+ flags=re.IGNORECASE )
130
+
131
+ text = re.sub( r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b', '[EMAIL]', text,
132
+ flags=re.IGNORECASE )
133
+ text = re.sub( r'\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b',
134
+ '[GUID]', text, flags=re.IGNORECASE )
135
+ text = re.sub(
136
+ r'(?i)(password|secret|token|api[_-]?key|subscription[_-]?key)\s*[=:]\s*[^\s,;]+',
137
+ r'\1=[SECRET]', text )
138
+
139
+ if not get_config_bool( 'ENABLE_RAW_TEXT_LOGGING', False ):
140
+ text = re.sub( r'"[^"]{120,}"', '"[LONG_TEXT]"', text )
141
+ text = re.sub( r"'[^']{120,}'", "'[LONG_TEXT]'", text )
142
+
143
+ if len( text ) > 300 and text.count( ',' ) >= 5:
144
+ text = '[MANIFEST_ROW]'
145
+ elif len( text ) > 500 and len( re.findall( r'\b[A-Za-z]{3,}\b', text ) ) > 60:
146
+ text = '[LONG_TEXT]'
147
+
148
+ return text[ :max_length ]
149
+ except Exception:
150
+ return '[SANITIZATION_FAILED]'
151
+
152
+ def sanitize_traceback( value: object, max_length: int = 4000 ) -> str:
153
+ """Return a sanitized traceback string.
154
+
155
+ Purpose:
156
+ Normalize and mask traceback text before it is stored in SQLite. This function delegates to
157
+ ``sanitize_text`` and uses a larger maximum length appropriate for diagnostic stack traces.
158
+
159
+ Args:
160
+ value (object): Traceback value to sanitize.
161
+ max_length (int): Maximum returned string length.
162
+
163
+ Returns:
164
+ str: Sanitized traceback text.
165
+ """
166
+ try:
167
+ return sanitize_text( value, max_length=max_length )
168
+ except Exception:
169
+ return '[TRACE_SANITIZATION_FAILED]'
170
+
171
+ class Error( Exception ):
172
+ """Wrap a Python exception with structured and sanitized diagnostic metadata.
173
+
174
+ Purpose:
175
+ The ``Error`` class extends ``Exception`` and stores the original exception together with
176
+ context fields used by Fiddy logging and diagnostics. The wrapper captures a sanitized
177
+ message, exception type, sanitized traceback, component or class cause, module name, method
178
+ or function signature, optional heading, and combined information string.
179
+
180
+ The object remains intentionally lightweight. Callers generally create an ``Error`` inside an
181
+ ``except`` block, assign stable metadata fields such as ``cause``, ``module``, and ``method``,
182
+ and then pass the object to ``Logger.write`` for local persistence.
183
+
184
+ Attributes:
185
+ error (Optional[Exception]): Source exception being wrapped.
186
+ heading (Optional[str]): Optional sanitized user-facing heading or category.
187
+ cause (Optional[str]): Sanitized component, class, or module purpose associated with the failure.
188
+ method (Optional[str]): Sanitized method or function signature associated with the failure.
189
+ module (Optional[str]): Sanitized module name associated with the failure.
190
+ type (Optional[type]): Exception type captured from ``sys.exc_info``.
191
+ trace (Optional[str]): Sanitized formatted traceback captured at wrapper creation time.
192
+ info (Optional[str]): Combined sanitized exception type and traceback information.
193
+ message (Optional[str]): Sanitized string representation of the source exception.
194
+ """
195
+
196
+ error: Optional[ Exception ]
197
+ heading: Optional[ str ]
198
+ cause: Optional[ str ]
199
+ method: Optional[ str ]
200
+ module: Optional[ str ]
201
+ type: Optional[ type ]
202
+ trace: Optional[ str ]
203
+ info: Optional[ str ]
204
+ message: Optional[ str ]
205
+
206
+ def __init__( self, error: Exception, heading: str = None, cause: str = None,
207
+ method: str = None, module: str = None ) -> None:
208
+ """Initialize an error wrapper from a caught exception.
209
+
210
+ Purpose:
211
+ Store the original exception and optional context values, initialize the base
212
+ ``Exception`` with the sanitized exception message, capture the current exception type,
213
+ capture a sanitized formatted traceback, and build a combined sanitized information
214
+ string suitable for database logging.
215
+
216
+ Args:
217
+ error (Exception): Source exception being wrapped.
218
+ heading (str): Optional user-facing heading.
219
+ cause (str): Optional component, class, or module purpose that caused the error.
220
+ method (str): Optional stable method or function signature where the error occurred.
221
+ module (str): Optional module where the error occurred.
222
+
223
+ Returns:
224
+ None.
225
+ """
226
+ self.error = error
227
+ self.heading = sanitize_text( heading, 120 ) if heading else None
228
+ self.cause = sanitize_text( cause, 120 ) if cause else None
229
+ self.method = sanitize_text( method, 180 ) if method else None
230
+ self.module = sanitize_text( module, 120 ) if module else None
231
+ self.type = exc_info( )[ 0 ]
232
+ self.message = sanitize_text( str( error ) if error else '',
233
+ get_config_int( 'MAX_LOG_MESSAGE_CHARS', 1000 ) )
234
+ self.trace = sanitize_traceback( traceback.format_exc( ),
235
+ get_config_int( 'MAX_LOG_TRACE_CHARS', 4000 ) )
236
+ self.info = sanitize_text( f'{str( self.type )}: {self.trace}',
237
+ get_config_int( 'MAX_LOG_TRACE_CHARS', 4000 ) )
238
+ super( ).__init__( self.message )
239
+
240
+ def __setattr__( self, name: str, value: object ) -> None:
241
+ """Sanitize public metadata fields when they are assigned after construction.
242
+
243
+ Purpose:
244
+ Preserve the established Fiddy usage pattern where callers create ``Error(e)`` and then
245
+ assign ``cause``, ``module``, and ``method`` afterward. Metadata assignments are
246
+ sanitized automatically so delayed assignment does not bypass privacy controls.
247
+
248
+ Args:
249
+ name (str): Attribute name being assigned.
250
+ value (object): Attribute value being assigned.
251
+
252
+ Returns:
253
+ None.
254
+ """
255
+ if name in ('heading', 'cause', 'module') and value is not None:
256
+ object.__setattr__( self, name, sanitize_text( value, 120 ) )
257
+ elif name == 'method' and value is not None:
258
+ object.__setattr__( self, name, sanitize_text( value, 180 ) )
259
+ elif name == 'message' and value is not None:
260
+ object.__setattr__( self, name,
261
+ sanitize_text( value, get_config_int( 'MAX_LOG_MESSAGE_CHARS', 1000 ) ) )
262
+ elif name in ('trace', 'info') and value is not None:
263
+ object.__setattr__( self, name,
264
+ sanitize_traceback( value, get_config_int( 'MAX_LOG_TRACE_CHARS', 4000 ) ) )
265
+ else:
266
+ object.__setattr__( self, name, value )
267
+
268
+ def __str__( self ) -> str:
269
+ """Return a string representation of the wrapped error.
270
+
271
+ Purpose:
272
+ Return the sanitized information string when available, otherwise return the sanitized
273
+ source exception message. If neither value is available, return an empty string.
274
+
275
+
276
+
277
+ Returns:
278
+ str: Sanitized error information string.
279
+ """
280
+ return self.info or self.message or ''
281
+
282
+ def __dir__( self ) -> List[ str ]:
283
+ """Return public member names used by callers and display surfaces.
284
+
285
+ Purpose:
286
+ Expose the primary fields that are useful for logging, diagnostics, and reviewer-safe
287
+ display. Implementation details and inherited exception internals are intentionally
288
+ omitted from the returned member list.
289
+
290
+
291
+
292
+ Returns:
293
+ List[str]: Public error member names.
294
+ """
295
+ return [
296
+ 'message',
297
+ 'cause',
298
+ 'error',
299
+ 'method',
300
+ 'module',
301
+ 'trace',
302
+ 'info'
303
+ ]
304
+
305
+ class Logger( ):
306
+ """Persist sanitized ``Error`` objects to the configured local SQLite database.
307
+
308
+ The ``Logger`` class resolves the configured log database path and table name, creates the
309
+ database table when needed, truncates sanitized values to fit configured limits, writes one
310
+ row per error, and optionally purges old records. The logger is intentionally defensive:
311
+ logger failures return conservative fallback values rather than raising secondary exceptions
312
+ into application workflows.
313
+
314
+ Attributes:
315
+ path (Path): Resolved SQLite database path.
316
+ table_name (str): Safe SQLite table name used for inserts and maintenance.
317
+ """
318
+ path: Path
319
+ table_name: str
320
+
321
+ def __init__( self ) -> None:
322
+ """Initialize the logger from configured database and table settings.
323
+
324
+ Purpose:
325
+ Resolve the SQLite database path through ``get_database_path`` and the table name
326
+ through ``get_table_name``. No database connection is opened until ``ensure_database``,
327
+ ``write``, or ``purge_old_logs`` is called.
328
+
329
+
330
+
331
+ Returns:
332
+ None.
333
+ """
334
+ self.path = self.get_database_path( )
335
+ self.table_name = self.get_table_name( )
336
+
337
+ def get_database_path( self ) -> Path:
338
+ """Return the configured SQLite database path.
339
+
340
+ Purpose:
341
+ Read ``cfg.LOG_PATH`` and resolve it to an absolute ``Path``. Relative log paths are
342
+ resolved under ``cfg.ROOT_DIR`` when available. If path resolution fails, the fallback
343
+ path ``logging/Exceptions.db`` is resolved relative to the current working directory.
344
+
345
+
346
+
347
+ Returns:
348
+ Path: Resolved SQLite database path.
349
+ """
350
+ try:
351
+ value = getattr( cfg, 'LOG_PATH', 'logging/Exceptions.db' )
352
+ path = Path( value )
353
+
354
+ if not path.is_absolute( ):
355
+ path = getattr( cfg, 'ROOT_DIR', Path.cwd( ) ) / path
356
+
357
+ return path.resolve( )
358
+ except Exception:
359
+ return Path( 'logging/Exceptions.db' ).resolve( )
360
+
361
+ def get_table_name( self ) -> str:
362
+ """Return a safe SQLite table name.
363
+
364
+ Purpose:
365
+ Read ``cfg.LOG_FILE`` and accept it only when it matches a conservative SQLite
366
+ identifier pattern beginning with a letter or underscore and containing only letters,
367
+ digits, and underscores. Unsafe or unavailable values fall back to ``Exceptions``.
368
+
369
+
370
+
371
+ Returns:
372
+ str: Safe SQLite table name.
373
+ """
374
+ try:
375
+ value = str( getattr( cfg, 'LOG_FILE', 'Exceptions' ) ).strip( )
376
+
377
+ if re.fullmatch( r'[A-Za-z_][A-Za-z0-9_]*', value ):
378
+ return value
379
+
380
+ return 'Exceptions'
381
+ except Exception:
382
+ return 'Exceptions'
383
+
384
+ def truncate( self, value: object, length: int ) -> str:
385
+ """Sanitize, convert, and truncate a value to a maximum length.
386
+
387
+ Purpose:
388
+ Standardize values before database insertion. ``None`` values become empty strings, all
389
+ other values are sanitized and converted to text, and the text is sliced to the requested
390
+ length so it fits the logging schema.
391
+
392
+ Args:
393
+ value (object): Source value to sanitize, convert, and truncate.
394
+ length (int): Maximum string length.
395
+
396
+ Returns:
397
+ str: Sanitized and truncated string, or an empty string when conversion fails.
398
+ """
399
+ try:
400
+ if value is None:
401
+ return ''
402
+
403
+ text = sanitize_text( value, max_length=length )
404
+ return text[ :length ]
405
+ except Exception:
406
+ return ''
407
+
408
+ def ensure_database( self ) -> None:
409
+ """Create the log directory and configured error table when needed.
410
+
411
+ Purpose:
412
+ Create the parent directory for the configured SQLite database path and create the
413
+ configured table if it does not already exist. The table stores timestamp, cause, module,
414
+ method, message, info, and trace values, with an autoincrementing primary key.
415
+
416
+
417
+
418
+ Returns:
419
+ None.
420
+ """
421
+ try:
422
+ if not get_config_bool( 'ENABLE_EXCEPTION_LOGGING', True ):
423
+ return None
424
+
425
+ self.path.parent.mkdir( parents=True, exist_ok=True )
426
+ sql = f'''
427
+ CREATE TABLE IF NOT EXISTS "{self.table_name}" (
428
+ "ID" INTEGER NOT NULL UNIQUE,
429
+ "created_on" TEXT NOT NULL,
430
+ "cause" TEXT,
431
+ "module" TEXT,
432
+ "method" TEXT,
433
+ "message" TEXT,
434
+ "info" TEXT,
435
+ "trace" TEXT,
436
+ PRIMARY KEY("ID" AUTOINCREMENT)
437
+ )
438
+ '''
439
+
440
+ with sqlite3.connect( self.path ) as connection:
441
+ connection.execute( sql )
442
+ connection.commit( )
443
+ except Exception:
444
+ return None
445
+
446
+ def write( self, error: Error ) -> int:
447
+ """Write one sanitized ``Error`` object to the configured SQLite error table.
448
+
449
+ Purpose:
450
+ Ensure the database exists, build a parameterized insert statement, sanitize and truncate
451
+ error fields, write the row, commit the transaction, optionally purge expired log rows,
452
+ and return the inserted row identifier. If exception logging is disabled or logging fails,
453
+ return ``0`` without raising another exception.
454
+
455
+ Args:
456
+ error (Error): Error object to persist.
457
+
458
+ Returns:
459
+ int: Inserted row identifier, or ``0`` when the error is missing, disabled, or logging fails.
460
+ """
461
+ try:
462
+ if not error or not get_config_bool( 'ENABLE_EXCEPTION_LOGGING', True ):
463
+ return 0
464
+
465
+ self.ensure_database( )
466
+ sql = f'''
467
+ INSERT INTO "{self.table_name}"
468
+ (
469
+ "created_on",
470
+ "cause",
471
+ "module",
472
+ "method",
473
+ "message",
474
+ "info",
475
+ "trace"
476
+ )
477
+ VALUES
478
+ (
479
+ ?,
480
+ ?,
481
+ ?,
482
+ ?,
483
+ ?,
484
+ ?,
485
+ ?
486
+ )
487
+ '''
488
+
489
+ values = (datetime.utcnow( ).strftime( '%Y-%m-%d %H:%M:%S' ),
490
+ self.truncate( error.cause, 120 ),
491
+ self.truncate( error.module, 120 ),
492
+ self.truncate( error.method, 180 ),
493
+ self.truncate( error.message,
494
+ get_config_int( 'MAX_LOG_MESSAGE_CHARS', 1000 ) ),
495
+ self.truncate( error.info, get_config_int( 'MAX_LOG_TRACE_CHARS', 4000 ) ),
496
+ self.truncate( error.trace, get_config_int( 'MAX_LOG_TRACE_CHARS', 4000 ) ))
497
+
498
+ with sqlite3.connect( self.path ) as connection:
499
+ cursor = connection.execute( sql, values )
500
+ connection.commit( )
501
+ row_id = int( cursor.lastrowid or 0 )
502
+
503
+ self.purge_old_logs( )
504
+ return row_id
505
+ except Exception:
506
+ return 0
507
+
508
+ def purge_old_logs( self, retention_days: int = None ) -> int:
509
+ """Delete log rows older than the configured retention period.
510
+
511
+ Purpose:
512
+ Support the prototype data-handling requirement by keeping local diagnostic logs short
513
+ lived. The method deletes rows whose ``created_on`` timestamp is older than the configured
514
+ retention window. If retention is set to zero or a negative value, no purge is performed.
515
+
516
+ Args:
517
+ retention_days (int): Optional retention window in days. When omitted, the value is read
518
+ from ``cfg.LOG_RETENTION_DAYS`` and defaults to seven days.
519
+
520
+ Returns:
521
+ int: Number of deleted rows, or ``0`` when no rows are deleted or purge fails.
522
+ """
523
+ try:
524
+ self.ensure_database( )
525
+ days = retention_days if retention_days is not None else get_config_int(
526
+ 'LOG_RETENTION_DAYS', 7 )
527
+
528
+ if days <= 0:
529
+ return 0
530
+
531
+ cutoff = datetime.utcnow( ) - timedelta( days=days )
532
+ sql = f'DELETE FROM "{self.table_name}" WHERE "created_on" < ?'
533
+
534
+ with sqlite3.connect( self.path ) as connection:
535
+ cursor = connection.execute( sql, (cutoff.strftime( '%Y-%m-%d %H:%M:%S' ),) )
536
+ connection.commit( )
537
+ return int( cursor.rowcount or 0 )
538
+ except Exception:
539
+ return 0
540
+
541
+ def log_error( error: Exception, heading: str = None, cause: str = None,
542
+ method: str = None, module: str = None ) -> Error:
543
+ """Wrap and log an exception using the configured Fiddy error database.
544
+
545
+ Purpose:
546
+ Create an ``Error`` object and persist it in one step. The wrapped error object is returned
547
+ so callers can continue to inspect or display structured metadata after logging.
548
+
549
+ Args:
550
+ error (Exception): Source exception being wrapped and logged.
551
+ heading (str): Optional user-facing heading.
552
+ cause (str): Optional component, class, or module purpose that caused the error.
553
+ method (str): Optional stable method or function signature where the error occurred.
554
+ module (str): Optional module where the error occurred.
555
+
556
+ Returns:
557
+ Error: Wrapped and sanitized error object.
558
+ """
559
+ exception = Error( error=error, heading=heading, cause=cause,
560
+ method=method, module=module )
561
+
562
+ Logger( ).write( exception )
563
+ return exception
boogr/default_icon.ico ADDED
Binary file