snooper-pkg 0.0.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.
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
snooper_pkg/_modidx.py ADDED
@@ -0,0 +1,102 @@
1
+ # Autogenerated by nbdev
2
+
3
+ d = { 'settings': { 'branch': 'main',
4
+ 'doc_baseurl': '/snooper_pkg',
5
+ 'doc_host': 'https://sayanbanerjee32.github.io',
6
+ 'git_url': 'https://github.com/sayanbanerjee32/snooper_pkg',
7
+ 'lib_path': 'snooper_pkg'},
8
+ 'syms': { 'snooper_pkg.config': {},
9
+ 'snooper_pkg.db': { 'snooper_pkg.db.ForegroundEvent': ('db.html#foregroundevent', 'snooper_pkg/db.py'),
10
+ 'snooper_pkg.db.ProcessEvent': ('db.html#processevent', 'snooper_pkg/db.py'),
11
+ 'snooper_pkg.db.Session': ('db.html#session', 'snooper_pkg/db.py'),
12
+ 'snooper_pkg.db._db_timestamp': ('db.html#_db_timestamp', 'snooper_pkg/db.py'),
13
+ 'snooper_pkg.db.close_stale_open_sessions': ('db.html#close_stale_open_sessions', 'snooper_pkg/db.py'),
14
+ 'snooper_pkg.db.delete_old_data': ('db.html#delete_old_data', 'snooper_pkg/db.py'),
15
+ 'snooper_pkg.db.end_session': ('db.html#end_session', 'snooper_pkg/db.py'),
16
+ 'snooper_pkg.db.get_foreground_events': ('db.html#get_foreground_events', 'snooper_pkg/db.py'),
17
+ 'snooper_pkg.db.get_last_completed_session': ('db.html#get_last_completed_session', 'snooper_pkg/db.py'),
18
+ 'snooper_pkg.db.get_processes': ('db.html#get_processes', 'snooper_pkg/db.py'),
19
+ 'snooper_pkg.db.get_session_by_session_id': ('db.html#get_session_by_session_id', 'snooper_pkg/db.py'),
20
+ 'snooper_pkg.db.init_db': ('db.html#init_db', 'snooper_pkg/db.py'),
21
+ 'snooper_pkg.db.log_fg_app_events': ('db.html#log_fg_app_events', 'snooper_pkg/db.py'),
22
+ 'snooper_pkg.db.log_process_event': ('db.html#log_process_event', 'snooper_pkg/db.py'),
23
+ 'snooper_pkg.db.start_session': ('db.html#start_session', 'snooper_pkg/db.py')},
24
+ 'snooper_pkg.foreground_tracker': { 'snooper_pkg.foreground_tracker._require_windows': ( 'foreground_tracker.html#_require_windows',
25
+ 'snooper_pkg/foreground_tracker.py'),
26
+ 'snooper_pkg.foreground_tracker.get_foreground_window_info': ( 'foreground_tracker.html#get_foreground_window_info',
27
+ 'snooper_pkg/foreground_tracker.py'),
28
+ 'snooper_pkg.foreground_tracker.is_streaming_app': ( 'foreground_tracker.html#is_streaming_app',
29
+ 'snooper_pkg/foreground_tracker.py'),
30
+ 'snooper_pkg.foreground_tracker.start_foreground_app_monitoring': ( 'foreground_tracker.html#start_foreground_app_monitoring',
31
+ 'snooper_pkg/foreground_tracker.py')},
32
+ 'snooper_pkg.idle_detector': { 'snooper_pkg.idle_detector.LASTINPUTINFO': ( 'idle_detector.html#lastinputinfo',
33
+ 'snooper_pkg/idle_detector.py'),
34
+ 'snooper_pkg.idle_detector._require_windows': ( 'idle_detector.html#_require_windows',
35
+ 'snooper_pkg/idle_detector.py'),
36
+ 'snooper_pkg.idle_detector.get_idle_seconds': ( 'idle_detector.html#get_idle_seconds',
37
+ 'snooper_pkg/idle_detector.py'),
38
+ 'snooper_pkg.idle_detector.get_last_input_tick': ( 'idle_detector.html#get_last_input_tick',
39
+ 'snooper_pkg/idle_detector.py')},
40
+ 'snooper_pkg.main': { 'snooper_pkg.main._acquire_single_instance': ( 'main.html#_acquire_single_instance',
41
+ 'snooper_pkg/main.py'),
42
+ 'snooper_pkg.main.configure_logging': ('main.html#configure_logging', 'snooper_pkg/main.py'),
43
+ 'snooper_pkg.main.main': ('main.html#main', 'snooper_pkg/main.py')},
44
+ 'snooper_pkg.monitoring_controller': { 'snooper_pkg.monitoring_controller.MonitorController': ( 'monitoring_controller.html#monitorcontroller',
45
+ 'snooper_pkg/monitoring_controller.py'),
46
+ 'snooper_pkg.monitoring_controller.MonitorController.__init__': ( 'monitoring_controller.html#monitorcontroller.__init__',
47
+ 'snooper_pkg/monitoring_controller.py'),
48
+ 'snooper_pkg.monitoring_controller.MonitorController._auto_resume': ( 'monitoring_controller.html#monitorcontroller._auto_resume',
49
+ 'snooper_pkg/monitoring_controller.py'),
50
+ 'snooper_pkg.monitoring_controller.MonitorController._midnight_rollover': ( 'monitoring_controller.html#monitorcontroller._midnight_rollover',
51
+ 'snooper_pkg/monitoring_controller.py'),
52
+ 'snooper_pkg.monitoring_controller.MonitorController.generate_current_session_stats': ( 'monitoring_controller.html#monitorcontroller.generate_current_session_stats',
53
+ 'snooper_pkg/monitoring_controller.py'),
54
+ 'snooper_pkg.monitoring_controller.MonitorController.generate_last_report_data': ( 'monitoring_controller.html#monitorcontroller.generate_last_report_data',
55
+ 'snooper_pkg/monitoring_controller.py'),
56
+ 'snooper_pkg.monitoring_controller.MonitorController.is_paused': ( 'monitoring_controller.html#monitorcontroller.is_paused',
57
+ 'snooper_pkg/monitoring_controller.py'),
58
+ 'snooper_pkg.monitoring_controller.MonitorController.is_running': ( 'monitoring_controller.html#monitorcontroller.is_running',
59
+ 'snooper_pkg/monitoring_controller.py'),
60
+ 'snooper_pkg.monitoring_controller.MonitorController.pause_for': ( 'monitoring_controller.html#monitorcontroller.pause_for',
61
+ 'snooper_pkg/monitoring_controller.py'),
62
+ 'snooper_pkg.monitoring_controller.MonitorController.resume_now': ( 'monitoring_controller.html#monitorcontroller.resume_now',
63
+ 'snooper_pkg/monitoring_controller.py'),
64
+ 'snooper_pkg.monitoring_controller.MonitorController.run_startup_housekeeping': ( 'monitoring_controller.html#monitorcontroller.run_startup_housekeeping',
65
+ 'snooper_pkg/monitoring_controller.py'),
66
+ 'snooper_pkg.monitoring_controller.MonitorController.start_monitoring': ( 'monitoring_controller.html#monitorcontroller.start_monitoring',
67
+ 'snooper_pkg/monitoring_controller.py'),
68
+ 'snooper_pkg.monitoring_controller.MonitorController.stop_monitoring': ( 'monitoring_controller.html#monitorcontroller.stop_monitoring',
69
+ 'snooper_pkg/monitoring_controller.py'),
70
+ 'snooper_pkg.monitoring_controller.seconds_until_midnight': ( 'monitoring_controller.html#seconds_until_midnight',
71
+ 'snooper_pkg/monitoring_controller.py')},
72
+ 'snooper_pkg.process_monitor': { 'snooper_pkg.process_monitor.get_current_processes': ( 'process_monitor.html#get_current_processes',
73
+ 'snooper_pkg/process_monitor.py'),
74
+ 'snooper_pkg.process_monitor.start_process_monitoring': ( 'process_monitor.html#start_process_monitoring',
75
+ 'snooper_pkg/process_monitor.py')},
76
+ 'snooper_pkg.report_window': { 'snooper_pkg.report_window.fmt_duration': ( 'report_window.html#fmt_duration',
77
+ 'snooper_pkg/report_window.py'),
78
+ 'snooper_pkg.report_window.show_apps': ( 'report_window.html#show_apps',
79
+ 'snooper_pkg/report_window.py'),
80
+ 'snooper_pkg.report_window.show_report': ( 'report_window.html#show_report',
81
+ 'snooper_pkg/report_window.py'),
82
+ 'snooper_pkg.report_window.show_summary': ( 'report_window.html#show_summary',
83
+ 'snooper_pkg/report_window.py'),
84
+ 'snooper_pkg.report_window.show_titles': ( 'report_window.html#show_titles',
85
+ 'snooper_pkg/report_window.py'),
86
+ 'snooper_pkg.report_window.update_titles': ( 'report_window.html#update_titles',
87
+ 'snooper_pkg/report_window.py')},
88
+ 'snooper_pkg.reporter': { 'snooper_pkg.reporter._clean_events': ('reporter.html#_clean_events', 'snooper_pkg/reporter.py'),
89
+ 'snooper_pkg.reporter._events_data': ('reporter.html#_events_data', 'snooper_pkg/reporter.py'),
90
+ 'snooper_pkg.reporter._session_data': ('reporter.html#_session_data', 'snooper_pkg/reporter.py'),
91
+ 'snooper_pkg.reporter._summary_data': ('reporter.html#_summary_data', 'snooper_pkg/reporter.py'),
92
+ 'snooper_pkg.reporter.build_session_report_data': ( 'reporter.html#build_session_report_data',
93
+ 'snooper_pkg/reporter.py')},
94
+ 'snooper_pkg.tray': { 'snooper_pkg.tray.UiDispatcher': ('tray.html#uidispatcher', 'snooper_pkg/tray.py'),
95
+ 'snooper_pkg.tray.UiDispatcher.__init__': ('tray.html#uidispatcher.__init__', 'snooper_pkg/tray.py'),
96
+ 'snooper_pkg.tray.UiDispatcher._process': ('tray.html#uidispatcher._process', 'snooper_pkg/tray.py'),
97
+ 'snooper_pkg.tray.UiDispatcher.submit': ('tray.html#uidispatcher.submit', 'snooper_pkg/tray.py'),
98
+ 'snooper_pkg.tray.open_current_session_report': ( 'tray.html#open_current_session_report',
99
+ 'snooper_pkg/tray.py'),
100
+ 'snooper_pkg.tray.open_last_session_report': ( 'tray.html#open_last_session_report',
101
+ 'snooper_pkg/tray.py'),
102
+ 'snooper_pkg.tray.run_tray': ('tray.html#run_tray', 'snooper_pkg/tray.py')}}}
snooper_pkg/config.py ADDED
@@ -0,0 +1,54 @@
1
+ """Central configuration values for Snooper's monitoring, data retention, reporting, privacy, and pause behaviour.
2
+
3
+ Docs: https://sayanbanerjee32.github.io/snooper_pkg/config.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_config.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['APP_NAME', 'LOG_FILENAME', 'LOG_LEVEL', 'PROCESS_MONITORING_INTERVAL_SECONDS', 'EXCLUDED_PROCESSES',
9
+ 'DATABASE_FILENAME', 'DATA_RETENTION_DAYS', 'FG_APP_MONITORING_INTERVAL_SECONDS',
10
+ 'FG_APP_MONITORING_MIN_IDLE_SECONDS', 'STREAMING_APP_MIN_IDLE_SECONDS', 'NUM_TOP_APP_IN_REPORT',
11
+ 'NUM_TOP_WINDOW_IN_REPORT', 'SHOW_WINDOW_TITLES', 'DEFAULT_PAUSE_MINUTES']
12
+
13
+ # %% ../nbs/00_config.ipynb #f2a11914
14
+ APP_NAME = "Snooper"
15
+
16
+
17
+ # %% ../nbs/00_config.ipynb #2c47f2da
18
+ LOG_FILENAME = "snooper.log"
19
+
20
+ # %% ../nbs/00_config.ipynb #e0c32039
21
+ LOG_LEVEL = "WARNING" # DEBUG, INFO, WARNING, ERROR
22
+
23
+ # %% ../nbs/00_config.ipynb #f99ee53a
24
+ PROCESS_MONITORING_INTERVAL_SECONDS = 5
25
+
26
+ # %% ../nbs/00_config.ipynb #f8d4d24d
27
+ EXCLUDED_PROCESSES = {'sudo','python'}
28
+
29
+ # %% ../nbs/00_config.ipynb #13babdaf
30
+ DATABASE_FILENAME = "snooper.db"
31
+
32
+ # %% ../nbs/00_config.ipynb #941e072f
33
+ DATA_RETENTION_DAYS = 7
34
+
35
+ # %% ../nbs/00_config.ipynb #d380659a
36
+ FG_APP_MONITORING_INTERVAL_SECONDS = 2
37
+
38
+ # %% ../nbs/00_config.ipynb #97baa2a4
39
+ FG_APP_MONITORING_MIN_IDLE_SECONDS = 60
40
+
41
+ # %% ../nbs/00_config.ipynb #0a8552af
42
+ STREAMING_APP_MIN_IDLE_SECONDS = {'youtube': 600, 'netflix': 600, 'twitch': 600}
43
+
44
+ # %% ../nbs/00_config.ipynb #199d6147
45
+ NUM_TOP_APP_IN_REPORT = 10
46
+
47
+ # %% ../nbs/00_config.ipynb #c58e4eb3
48
+ NUM_TOP_WINDOW_IN_REPORT = 5
49
+
50
+ # %% ../nbs/00_config.ipynb #84bab75b
51
+ SHOW_WINDOW_TITLES = True
52
+
53
+ # %% ../nbs/00_config.ipynb #e2cf94c1
54
+ DEFAULT_PAUSE_MINUTES = 30
snooper_pkg/db.py ADDED
@@ -0,0 +1,304 @@
1
+ """SQLite persistence APIs for monitoring sessions, foreground activity, and process events.
2
+
3
+ Docs: https://sayanbanerjee32.github.io/snooper_pkg/db.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_db.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['ProcessEvent', 'Session', 'ForegroundEvent', 'init_db', 'log_process_event', 'get_processes', 'start_session',
9
+ 'end_session', 'get_last_completed_session', 'close_stale_open_sessions', 'get_session_by_session_id',
10
+ 'log_fg_app_events', 'get_foreground_events', 'delete_old_data']
11
+
12
+ # %% ../nbs/01_db.ipynb #f9f7bca0
13
+ import logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # %% ../nbs/01_db.ipynb #6421ef53
18
+ from datetime import datetime, timedelta
19
+ from fastlite import *
20
+ from platformdirs import user_data_path
21
+
22
+ # %% ../nbs/01_db.ipynb #e1ae96a1
23
+ import snooper_pkg.config as cf
24
+
25
+ # %% ../nbs/01_db.ipynb #de4ffec3
26
+ class ProcessEvent:
27
+ "A recorded process start or stop event."
28
+
29
+ session_id: str # Monitoring session containing the event
30
+ pid: int # Operating-system process identifier
31
+ app: str # Process or application name
32
+ event_type: str # Event type: `start` or `stop`
33
+ timestamp: str # ISO-formatted event timestamp
34
+ id: int | None = None # Database-generated primary key
35
+
36
+ # %% ../nbs/01_db.ipynb #9ed8b625
37
+ class Session:
38
+ "A single continuous monitoring session."
39
+
40
+ session_id: str # Unique session identifier
41
+ start_time: str # ISO-formatted session start time
42
+ end_time: str | None # ISO-formatted end time; `None` while open
43
+ status: str # Session state: `open` or `closed`
44
+ end_reason: str | None # Reason the session ended
45
+
46
+ # %% ../nbs/01_db.ipynb #038a14fd
47
+ class ForegroundEvent:
48
+ "An interval during which an application or idle state was in the foreground."
49
+
50
+ session_id: str # Monitoring session containing the interval
51
+ app: str # Foreground application name
52
+ pid: int # Foreground process identifier
53
+ window_title: str # Foreground window title
54
+ start_time: str # ISO-formatted interval start time
55
+ end_time: str # ISO-formatted interval end time
56
+ hwnd: int # Windows window handle
57
+ is_idle: bool # Whether the interval represents user idle time
58
+ id: int | None = None # Database-generated primary key
59
+
60
+ # %% ../nbs/01_db.ipynb #3d9026f4
61
+ def _db_timestamp(value: str | datetime) -> str:
62
+ "Convert a timestamp into the string format stored in SQLite."
63
+ return value.isoformat(sep=" ", timespec="seconds") if isinstance(value, datetime) else value
64
+
65
+ # %% ../nbs/01_db.ipynb #fe74016d
66
+ DATA_DIR = user_data_path(cf.APP_NAME, appauthor=False)
67
+ DATABASE_PATH = DATA_DIR / cf.DATABASE_FILENAME
68
+
69
+ db = None
70
+ process_events = sessions = fore_ground_app_events = None
71
+
72
+ # %% ../nbs/01_db.ipynb #75286d2b
73
+ def init_db(
74
+ path=None # database path for the application
75
+ ):
76
+ "Open the database connection and create tables. Uses `DATABASE_PATH` if `path` is None."
77
+ global db, process_events, sessions, fore_ground_app_events
78
+ if path is None: path = DATABASE_PATH
79
+ path.parent.mkdir(parents=True, exist_ok=True)
80
+ db = database(path)
81
+ process_events = db.create(ProcessEvent, if_not_exists=True, transform=True)
82
+ sessions = db.create(Session, pk="session_id", if_not_exists=True, transform=True)
83
+ fore_ground_app_events = db.create(ForegroundEvent, if_not_exists=True, transform=True)
84
+ return db
85
+
86
+ # %% ../nbs/01_db.ipynb #35a9d475
87
+ def log_process_event(
88
+ session_id: str, # Session receiving the event
89
+ pid: int, # Operating-system process identifier
90
+ app: str, # Process or application name
91
+ event_type: str, # Event type: `start` or `stop`
92
+ timestamp: str, # ISO-formatted event timestamp
93
+ ):
94
+ "Record a process start or stop event."
95
+ return process_events.insert(
96
+ session_id=session_id,
97
+ pid=pid,
98
+ app=app,
99
+ event_type=event_type,
100
+ timestamp=timestamp,
101
+ )
102
+
103
+ # %% ../nbs/01_db.ipynb #70e04342
104
+ def get_processes(
105
+ session_id: str, # Session whose completed process intervals are requested
106
+ ) -> list[dict]:
107
+ "Return paired process start/stop intervals for a session."
108
+ return db.q(
109
+ """
110
+ SELECT
111
+ s.session_id,
112
+ s.pid,
113
+ s.app,
114
+ s.timestamp AS start_time,
115
+ e.timestamp AS stop_time,
116
+ (julianday(e.timestamp) - julianday(s.timestamp)) * 86400
117
+ AS duration_seconds
118
+ FROM process_event s
119
+ JOIN process_event e
120
+ ON s.pid = e.pid
121
+ AND s.app = e.app
122
+ AND s.session_id = e.session_id
123
+ WHERE s.event_type = 'start'
124
+ AND e.event_type = 'stop'
125
+ AND s.session_id = ?
126
+ """,
127
+ [session_id],
128
+ )
129
+
130
+ # %% ../nbs/01_db.ipynb #9e0b447c
131
+ def start_session(
132
+ session_id: str, # Unique identifier for the new session
133
+ start_time: str, # ISO-formatted session start time
134
+ ):
135
+ "Create and return a new open monitoring session."
136
+ start_time = _db_timestamp(start_time)
137
+ return sessions.insert(
138
+ session_id=session_id,
139
+ start_time=start_time,
140
+ status="open",
141
+ )
142
+
143
+ # %% ../nbs/01_db.ipynb #e72f7a86
144
+ def end_session(
145
+ session_id: str, # Session to close
146
+ end_time: str, # ISO-formatted session end time
147
+ end_reason: str = "NA", # Reason the session ended
148
+ ):
149
+ "Close a monitoring session and record why it ended."
150
+ end_time = _db_timestamp(end_time)
151
+ return sessions.update(
152
+ session_id=session_id,
153
+ end_time=end_time,
154
+ status="closed",
155
+ end_reason=end_reason,
156
+ )
157
+
158
+ # %% ../nbs/01_db.ipynb #0c1f5f71
159
+ def get_last_completed_session() -> dict:
160
+ "Return the most recently completed session, or an empty dictionary."
161
+ rows = db.q(
162
+ """
163
+ SELECT session_id, start_time, end_time
164
+ FROM session
165
+ WHERE status = 'closed'
166
+ ORDER BY julianday(end_time) DESC
167
+ LIMIT 1
168
+ """
169
+ )
170
+ return rows[0] if rows else {}
171
+
172
+ # %% ../nbs/01_db.ipynb #babf2013
173
+ def close_stale_open_sessions(
174
+ end_time: str, # Timestamp used to close stale sessions
175
+ ) -> list[str]:
176
+ "Close sessions left open before startup and return their identifiers."
177
+ end_time = _db_timestamp(end_time)
178
+ open_sessions_dict_list = db.q("""
179
+ SELECT
180
+ session_id
181
+ FROM session
182
+ WHERE status = 'open'
183
+ AND julianday(start_time) < julianday(?)
184
+ """, [end_time])
185
+ closed_session_list = []
186
+ if open_sessions_dict_list:
187
+ for open_sessions_dict in open_sessions_dict_list:
188
+ end_session(session_id = open_sessions_dict['session_id'], end_time = end_time, end_reason = 'interrupted')
189
+ closed_session_list.append(open_sessions_dict['session_id'])
190
+ return closed_session_list
191
+
192
+ # %% ../nbs/01_db.ipynb #eacbf030
193
+ def get_session_by_session_id(
194
+ session_id: str, # Identifier of the requested session
195
+ ) -> dict:
196
+ "Return a session by ID, using the current time for an open session's end."
197
+ rows = db.q(
198
+ """
199
+ SELECT
200
+ session_id,
201
+ start_time,
202
+ COALESCE(end_time, ?) AS end_time
203
+ FROM session
204
+ WHERE session_id = ?
205
+ ORDER BY julianday(start_time) DESC
206
+ LIMIT 1
207
+ """,
208
+ [
209
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
210
+ session_id,
211
+ ],
212
+ )
213
+ return rows[0] if rows else {}
214
+
215
+ # %% ../nbs/01_db.ipynb #1602e56e
216
+ def log_fg_app_events(
217
+ session_id: str, # Session receiving the interval
218
+ app: str, # Foreground application name
219
+ pid: int, # Foreground process identifier
220
+ window_title: str, # Foreground window title
221
+ start_time: str, # ISO-formatted interval start time
222
+ end_time: str, # ISO-formatted interval end time
223
+ hwnd: int, # Windows window handle
224
+ is_idle: bool, # Whether this was an idle interval
225
+ ):
226
+ "Record a completed foreground-window or idle interval."
227
+ return fore_ground_app_events.insert(
228
+ session_id=session_id,
229
+ app=app,
230
+ pid=pid,
231
+ window_title=window_title,
232
+ start_time=start_time,
233
+ end_time=end_time,
234
+ hwnd=hwnd,
235
+ is_idle=is_idle,
236
+ )
237
+
238
+ # %% ../nbs/01_db.ipynb #680919a5
239
+ def get_foreground_events(
240
+ session_id: str, # Session whose foreground intervals are requested
241
+ ) -> list[dict]:
242
+ "Return a session's foreground intervals in chronological order."
243
+ rows = db.q(
244
+ """
245
+ SELECT *
246
+ FROM foreground_event
247
+ WHERE session_id = ?
248
+ ORDER BY julianday(start_time)
249
+ """,
250
+ [session_id],
251
+ )
252
+ return rows or []
253
+
254
+ # %% ../nbs/01_db.ipynb #61154a49
255
+ def delete_old_data(
256
+ now_ts: str | datetime | None = None, # Retention reference time; defaults to now
257
+ ) -> str:
258
+ "Delete data belonging to closed sessions older than the retention period."
259
+ if isinstance(now_ts, str):
260
+ now_dt = datetime.fromisoformat(now_ts)
261
+ else:
262
+ now_dt = now_ts or datetime.now()
263
+
264
+ retention_days = getattr(cf, "DATA_RETENTION_DAYS", 365)
265
+ cutoff_ts = (
266
+ now_dt - timedelta(days=retention_days)
267
+ ).isoformat(timespec="seconds")
268
+
269
+ db.q(
270
+ """
271
+ DELETE FROM process_event
272
+ WHERE session_id IN (
273
+ SELECT session_id
274
+ FROM session
275
+ WHERE status = 'closed'
276
+ AND julianday(end_time) < julianday(?)
277
+ )
278
+ """,
279
+ [cutoff_ts],
280
+ )
281
+
282
+ db.q(
283
+ """
284
+ DELETE FROM foreground_event
285
+ WHERE session_id IN (
286
+ SELECT session_id
287
+ FROM session
288
+ WHERE status = 'closed'
289
+ AND julianday(end_time) < julianday(?)
290
+ )
291
+ """,
292
+ [cutoff_ts],
293
+ )
294
+
295
+ db.q(
296
+ """
297
+ DELETE FROM session
298
+ WHERE status = 'closed'
299
+ AND julianday(end_time) < julianday(?)
300
+ """,
301
+ [cutoff_ts],
302
+ )
303
+
304
+ return cutoff_ts
@@ -0,0 +1,98 @@
1
+ """Track the active Windows foreground application and persist completed foreground and idle intervals.
2
+
3
+ Docs: https://sayanbanerjee32.github.io/snooper_pkg/foreground_tracker.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_foreground_tracker.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['get_foreground_window_info', 'is_streaming_app', 'start_foreground_app_monitoring']
9
+
10
+ # %% ../nbs/03_foreground_tracker.ipynb #0f83e8bf
11
+ import logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ # %% ../nbs/03_foreground_tracker.ipynb #3fc06c45
16
+ import psutil
17
+ from datetime import datetime
18
+ from time import sleep
19
+
20
+ # %% ../nbs/03_foreground_tracker.ipynb #4cbec422
21
+ import snooper_pkg.config as cf
22
+ from snooper_pkg.db import *
23
+ from snooper_pkg.idle_detector import *
24
+
25
+ # %% ../nbs/03_foreground_tracker.ipynb #4939c6b1
26
+ import sys
27
+ def _require_windows():
28
+ if sys.platform != "win32":
29
+ raise RuntimeError("Windows idle detection is only available on Windows")
30
+
31
+
32
+ # %% ../nbs/03_foreground_tracker.ipynb #9672487f
33
+ def get_foreground_window_info() -> tuple[str, int, int, str]:
34
+ "Return the foreground window title, handle, process ID, and process name."
35
+ _require_windows()
36
+ import win32gui, win32process
37
+ hwnd = win32gui.GetForegroundWindow()
38
+ title = win32gui.GetWindowText(hwnd) or "Unknown"
39
+ tid, pid = win32process.GetWindowThreadProcessId(hwnd)
40
+ if not isinstance(pid, int) or pid <= 0: return title, hwnd, -1, "UNKNOWN"
41
+ try: process_name = psutil.Process(pid).name()
42
+ except (psutil.NoSuchProcess, psutil.AccessDenied, ValueError, psutil.Error): return title, hwnd, -1, "UNKNOWN"
43
+ return title, hwnd, pid, process_name
44
+
45
+ # %% ../nbs/03_foreground_tracker.ipynb #9f7f5ced
46
+ def is_streaming_app(
47
+ title: str, # Foreground window title to inspect
48
+ streaming_app_min_idle_time: dict[str, int | float], # Title substrings mapped to idle thresholds in seconds
49
+ ) -> int | float | None:
50
+ "Return the idle threshold for the first configured title match, or `None`."
51
+ for s in streaming_app_min_idle_time.keys():
52
+ if s.lower() in title.lower():
53
+ return streaming_app_min_idle_time[s]
54
+ return None
55
+
56
+ # %% ../nbs/03_foreground_tracker.ipynb #f101b187
57
+ def start_foreground_app_monitoring(
58
+ session_id: str, # Monitoring session receiving foreground intervals
59
+ stop_event: object, # Stop signal providing an `is_set()` method
60
+ time_interval: int | float = cf.FG_APP_MONITORING_INTERVAL_SECONDS, # Seconds between foreground samples
61
+ min_idle_time_gap: int | float = cf.FG_APP_MONITORING_MIN_IDLE_SECONDS, # General idle threshold in seconds
62
+ streaming_app_min_idle_time: dict[str, int | float] = cf.STREAMING_APP_MIN_IDLE_SECONDS, # Title-specific idle thresholds
63
+ ) -> None:
64
+ "Sample foreground state until stopped and record each completed interval."
65
+ fg_dict = {}
66
+ try:
67
+ while not stop_event.is_set():
68
+ title, hwnd, pid, process_name = get_foreground_window_info()
69
+ if is_streaming_app(title, streaming_app_min_idle_time) is not None:
70
+ min_idle_time = is_streaming_app(title, streaming_app_min_idle_time)
71
+ is_idle = get_idle_seconds() > min_idle_time
72
+ else: is_idle = get_idle_seconds() > min_idle_time_gap
73
+ if is_idle:
74
+ title, hwnd, pid, process_name = "Idle", -1, -1, "IDLE"
75
+ if fg_dict.get("pid") != pid or fg_dict.get("hwnd") != hwnd or fg_dict.get("window_title") != title:
76
+ if fg_dict:
77
+ fg_dict["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
78
+ log_fg_app_events(**fg_dict)
79
+ fg_dict = {
80
+ "session_id":session_id,
81
+ "window_title": title,
82
+ "pid": pid,
83
+ "hwnd": hwnd,
84
+ "app": process_name,
85
+ "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
86
+ "is_idle":is_idle,
87
+ "end_time": None
88
+ }
89
+ else:
90
+ fg_dict["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
91
+ sleep(time_interval)
92
+ except KeyboardInterrupt:
93
+ logger.exception("Stopped by user.")
94
+ finally:
95
+ if fg_dict:
96
+ fg_dict["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
97
+ log_fg_app_events(**fg_dict)
98
+
@@ -0,0 +1,54 @@
1
+ """Windows idle-time measurement based on system tick counts for the most recent user input.
2
+
3
+ Docs: https://sayanbanerjee32.github.io/snooper_pkg/idle_detector.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_idle_detector.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['LASTINPUTINFO', 'get_last_input_tick', 'get_idle_seconds']
9
+
10
+ # %% ../nbs/02_idle_detector.ipynb #4cab5d99
11
+ import logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ # %% ../nbs/02_idle_detector.ipynb #c6affeda
16
+ import ctypes
17
+ from ctypes import wintypes
18
+
19
+ # %% ../nbs/02_idle_detector.ipynb #56597405
20
+ import sys
21
+ def _require_windows():
22
+ if sys.platform != "win32":
23
+ raise RuntimeError("Windows idle detection is only available on Windows")
24
+
25
+ # %% ../nbs/02_idle_detector.ipynb #e6a637cf
26
+ class LASTINPUTINFO(ctypes.Structure):
27
+ "ctypes representation of the Windows `LASTINPUTINFO` structure."
28
+
29
+ _fields_ = [
30
+ ("cbSize", wintypes.UINT), # Size of this structure in bytes
31
+ ("dwTime", wintypes.DWORD), # Tick count when the last input occurred
32
+ ]
33
+
34
+
35
+ # %% ../nbs/02_idle_detector.ipynb #3f90a87c
36
+ def get_last_input_tick():
37
+ "Call `GetLastInputInfo` and return its last-input tick count in milliseconds."
38
+ _require_windows()
39
+
40
+ info = LASTINPUTINFO()
41
+ info.cbSize = ctypes.sizeof(info)
42
+
43
+ ok = ctypes.windll.user32.GetLastInputInfo(ctypes.byref(info))
44
+ if not ok:
45
+ raise ctypes.WinError()
46
+ return info.dwTime
47
+
48
+ # %% ../nbs/02_idle_detector.ipynb #2083d992
49
+ def get_idle_seconds():
50
+ "Return the elapsed seconds between system uptime and the last input tick."
51
+ _require_windows()
52
+ current_tick = ctypes.windll.kernel32.GetTickCount64()
53
+ last_input_tick = get_last_input_tick()
54
+ return (current_tick - last_input_tick)/1000