github-monitor 2.0__tar.gz → 2.2__tar.gz
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.
Potentially problematic release.
This version of github-monitor might be problematic. Click here for more details.
- {github_monitor-2.0 → github_monitor-2.2}/PKG-INFO +35 -6
- {github_monitor-2.0 → github_monitor-2.2}/README.md +34 -5
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.egg-info/PKG-INFO +35 -6
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.py +436 -48
- {github_monitor-2.0 → github_monitor-2.2}/pyproject.toml +1 -1
- {github_monitor-2.0 → github_monitor-2.2}/LICENSE +0 -0
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.egg-info/SOURCES.txt +0 -0
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.egg-info/dependency_links.txt +0 -0
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.egg-info/entry_points.txt +0 -0
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.egg-info/requires.txt +0 -0
- {github_monitor-2.0 → github_monitor-2.2}/github_monitor.egg-info/top_level.txt +0 -0
- {github_monitor-2.0 → github_monitor-2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github_monitor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: Tool implementing real-time tracking of Github users activities including profile and repositories changes
|
|
5
5
|
Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -26,7 +26,7 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
# github_monitor
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
OSINT tool for real-time monitoring of GitHub users' activities, including profile and repository changes.
|
|
30
30
|
|
|
31
31
|
<a id="features"></a>
|
|
32
32
|
## Features
|
|
@@ -39,6 +39,7 @@ github_monitor is a tool for real-time monitoring of GitHub users' activities, i
|
|
|
39
39
|
- added/removed public repositories
|
|
40
40
|
- changes in user name, email, location, company, bio and blog URL
|
|
41
41
|
- changes in profile visibility (public to private and vice versa)
|
|
42
|
+
- changes in user's daily contributions
|
|
42
43
|
- detection when a user blocks or unblocks you
|
|
43
44
|
- detection of account metadata changes (such as account update date)
|
|
44
45
|
- Email notifications for different events (new GitHub events, changed followings, followers, repositories, user name, email, location, company, bio, blog URL etc.)
|
|
@@ -86,8 +87,8 @@ github_monitor is a tool for real-time monitoring of GitHub users' activities, i
|
|
|
86
87
|
|
|
87
88
|
Tested on:
|
|
88
89
|
|
|
89
|
-
* **macOS**: Ventura, Sonoma, Sequoia
|
|
90
|
-
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm), Ubuntu 24, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
90
|
+
* **macOS**: Ventura, Sonoma, Sequoia, Tahoe
|
|
91
|
+
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm, Trixie), Ubuntu 24/25, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
91
92
|
* **Windows**: 10, 11
|
|
92
93
|
|
|
93
94
|
It should work on other versions of macOS, Linux, Unix and Windows as well.
|
|
@@ -292,6 +293,18 @@ If you want to monitor changes to user's public repositories (e.g. new stargazer
|
|
|
292
293
|
github_monitor github_username -j
|
|
293
294
|
```
|
|
294
295
|
|
|
296
|
+
By default, only user-owned repos are tracked. To include forks and collaborations, set `GET_ALL_REPOS` to `True` or use the `-a` flag:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
github_monitor github_username -j -a
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
If you want to track user's daily contributions then use the `-m` flag:
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
github_monitor github_username -m
|
|
306
|
+
```
|
|
307
|
+
|
|
295
308
|
If for any reason you do not want to monitor GitHub events for the user (e.g. new pushes, PRs, issues, forks, releases etc.), then use the `-k` flag:
|
|
296
309
|
|
|
297
310
|
```sh
|
|
@@ -319,6 +332,12 @@ github_monitor github_username -r
|
|
|
319
332
|
<img src="https://raw.githubusercontent.com/misiektoja/github_monitor/refs/heads/main/assets/github_list_of_repos.png" alt="github_list_of_repos" width="90%"/>
|
|
320
333
|
</p>
|
|
321
334
|
|
|
335
|
+
By default, only user-owned repos are listed. To include forks and collaborations, set `GET_ALL_REPOS` to `True` or use the `-a` flag:
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
github_monitor github_username -r -a
|
|
339
|
+
```
|
|
340
|
+
|
|
322
341
|
If you want to display a list of repositories starred by the user then use the `-g` flag:
|
|
323
342
|
|
|
324
343
|
```sh
|
|
@@ -367,7 +386,7 @@ To get email notifications when changes in user repositories are detected (e.g.
|
|
|
367
386
|
- or use the `-q` flag
|
|
368
387
|
|
|
369
388
|
```sh
|
|
370
|
-
github_monitor github_username -q
|
|
389
|
+
github_monitor github_username -j -q
|
|
371
390
|
```
|
|
372
391
|
|
|
373
392
|
To be informed whenever changes in the update date of user repositories are detected:
|
|
@@ -375,11 +394,21 @@ To be informed whenever changes in the update date of user repositories are dete
|
|
|
375
394
|
- or use the `-u` flag
|
|
376
395
|
|
|
377
396
|
```sh
|
|
378
|
-
github_monitor github_username -u
|
|
397
|
+
github_monitor github_username -j -u
|
|
379
398
|
```
|
|
380
399
|
|
|
381
400
|
The last two options (`-q` and `-u`) only work if tracking of repositories changes is enabled (`-j`).
|
|
382
401
|
|
|
402
|
+
To be informed about user's daily contributions:
|
|
403
|
+
- set `CONTRIB_NOTIFICATION` to `True`
|
|
404
|
+
- or use the `-y` flag
|
|
405
|
+
|
|
406
|
+
```sh
|
|
407
|
+
github_monitor github_username -m -y
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The `-y` only works if tracking of daily contributions is enabled (`-m`).
|
|
411
|
+
|
|
383
412
|
To disable sending an email on errors (enabled by default):
|
|
384
413
|
- set `ERROR_NOTIFICATION` to `False`
|
|
385
414
|
- or use the `-e` flag
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# github_monitor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OSINT tool for real-time monitoring of GitHub users' activities, including profile and repository changes.
|
|
4
4
|
|
|
5
5
|
<a id="features"></a>
|
|
6
6
|
## Features
|
|
@@ -13,6 +13,7 @@ github_monitor is a tool for real-time monitoring of GitHub users' activities, i
|
|
|
13
13
|
- added/removed public repositories
|
|
14
14
|
- changes in user name, email, location, company, bio and blog URL
|
|
15
15
|
- changes in profile visibility (public to private and vice versa)
|
|
16
|
+
- changes in user's daily contributions
|
|
16
17
|
- detection when a user blocks or unblocks you
|
|
17
18
|
- detection of account metadata changes (such as account update date)
|
|
18
19
|
- Email notifications for different events (new GitHub events, changed followings, followers, repositories, user name, email, location, company, bio, blog URL etc.)
|
|
@@ -60,8 +61,8 @@ github_monitor is a tool for real-time monitoring of GitHub users' activities, i
|
|
|
60
61
|
|
|
61
62
|
Tested on:
|
|
62
63
|
|
|
63
|
-
* **macOS**: Ventura, Sonoma, Sequoia
|
|
64
|
-
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm), Ubuntu 24, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
64
|
+
* **macOS**: Ventura, Sonoma, Sequoia, Tahoe
|
|
65
|
+
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm, Trixie), Ubuntu 24/25, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
65
66
|
* **Windows**: 10, 11
|
|
66
67
|
|
|
67
68
|
It should work on other versions of macOS, Linux, Unix and Windows as well.
|
|
@@ -266,6 +267,18 @@ If you want to monitor changes to user's public repositories (e.g. new stargazer
|
|
|
266
267
|
github_monitor github_username -j
|
|
267
268
|
```
|
|
268
269
|
|
|
270
|
+
By default, only user-owned repos are tracked. To include forks and collaborations, set `GET_ALL_REPOS` to `True` or use the `-a` flag:
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
github_monitor github_username -j -a
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
If you want to track user's daily contributions then use the `-m` flag:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
github_monitor github_username -m
|
|
280
|
+
```
|
|
281
|
+
|
|
269
282
|
If for any reason you do not want to monitor GitHub events for the user (e.g. new pushes, PRs, issues, forks, releases etc.), then use the `-k` flag:
|
|
270
283
|
|
|
271
284
|
```sh
|
|
@@ -293,6 +306,12 @@ github_monitor github_username -r
|
|
|
293
306
|
<img src="https://raw.githubusercontent.com/misiektoja/github_monitor/refs/heads/main/assets/github_list_of_repos.png" alt="github_list_of_repos" width="90%"/>
|
|
294
307
|
</p>
|
|
295
308
|
|
|
309
|
+
By default, only user-owned repos are listed. To include forks and collaborations, set `GET_ALL_REPOS` to `True` or use the `-a` flag:
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
github_monitor github_username -r -a
|
|
313
|
+
```
|
|
314
|
+
|
|
296
315
|
If you want to display a list of repositories starred by the user then use the `-g` flag:
|
|
297
316
|
|
|
298
317
|
```sh
|
|
@@ -341,7 +360,7 @@ To get email notifications when changes in user repositories are detected (e.g.
|
|
|
341
360
|
- or use the `-q` flag
|
|
342
361
|
|
|
343
362
|
```sh
|
|
344
|
-
github_monitor github_username -q
|
|
363
|
+
github_monitor github_username -j -q
|
|
345
364
|
```
|
|
346
365
|
|
|
347
366
|
To be informed whenever changes in the update date of user repositories are detected:
|
|
@@ -349,11 +368,21 @@ To be informed whenever changes in the update date of user repositories are dete
|
|
|
349
368
|
- or use the `-u` flag
|
|
350
369
|
|
|
351
370
|
```sh
|
|
352
|
-
github_monitor github_username -u
|
|
371
|
+
github_monitor github_username -j -u
|
|
353
372
|
```
|
|
354
373
|
|
|
355
374
|
The last two options (`-q` and `-u`) only work if tracking of repositories changes is enabled (`-j`).
|
|
356
375
|
|
|
376
|
+
To be informed about user's daily contributions:
|
|
377
|
+
- set `CONTRIB_NOTIFICATION` to `True`
|
|
378
|
+
- or use the `-y` flag
|
|
379
|
+
|
|
380
|
+
```sh
|
|
381
|
+
github_monitor github_username -m -y
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The `-y` only works if tracking of daily contributions is enabled (`-m`).
|
|
385
|
+
|
|
357
386
|
To disable sending an email on errors (enabled by default):
|
|
358
387
|
- set `ERROR_NOTIFICATION` to `False`
|
|
359
388
|
- or use the `-e` flag
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github_monitor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: Tool implementing real-time tracking of Github users activities including profile and repositories changes
|
|
5
5
|
Author-email: Michal Szymanski <misiektoja-pypi@rm-rf.ninja>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -26,7 +26,7 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
# github_monitor
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
OSINT tool for real-time monitoring of GitHub users' activities, including profile and repository changes.
|
|
30
30
|
|
|
31
31
|
<a id="features"></a>
|
|
32
32
|
## Features
|
|
@@ -39,6 +39,7 @@ github_monitor is a tool for real-time monitoring of GitHub users' activities, i
|
|
|
39
39
|
- added/removed public repositories
|
|
40
40
|
- changes in user name, email, location, company, bio and blog URL
|
|
41
41
|
- changes in profile visibility (public to private and vice versa)
|
|
42
|
+
- changes in user's daily contributions
|
|
42
43
|
- detection when a user blocks or unblocks you
|
|
43
44
|
- detection of account metadata changes (such as account update date)
|
|
44
45
|
- Email notifications for different events (new GitHub events, changed followings, followers, repositories, user name, email, location, company, bio, blog URL etc.)
|
|
@@ -86,8 +87,8 @@ github_monitor is a tool for real-time monitoring of GitHub users' activities, i
|
|
|
86
87
|
|
|
87
88
|
Tested on:
|
|
88
89
|
|
|
89
|
-
* **macOS**: Ventura, Sonoma, Sequoia
|
|
90
|
-
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm), Ubuntu 24, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
90
|
+
* **macOS**: Ventura, Sonoma, Sequoia, Tahoe
|
|
91
|
+
* **Linux**: Raspberry Pi OS (Bullseye, Bookworm, Trixie), Ubuntu 24/25, Rocky Linux 8.x/9.x, Kali Linux 2024/2025
|
|
91
92
|
* **Windows**: 10, 11
|
|
92
93
|
|
|
93
94
|
It should work on other versions of macOS, Linux, Unix and Windows as well.
|
|
@@ -292,6 +293,18 @@ If you want to monitor changes to user's public repositories (e.g. new stargazer
|
|
|
292
293
|
github_monitor github_username -j
|
|
293
294
|
```
|
|
294
295
|
|
|
296
|
+
By default, only user-owned repos are tracked. To include forks and collaborations, set `GET_ALL_REPOS` to `True` or use the `-a` flag:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
github_monitor github_username -j -a
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
If you want to track user's daily contributions then use the `-m` flag:
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
github_monitor github_username -m
|
|
306
|
+
```
|
|
307
|
+
|
|
295
308
|
If for any reason you do not want to monitor GitHub events for the user (e.g. new pushes, PRs, issues, forks, releases etc.), then use the `-k` flag:
|
|
296
309
|
|
|
297
310
|
```sh
|
|
@@ -319,6 +332,12 @@ github_monitor github_username -r
|
|
|
319
332
|
<img src="https://raw.githubusercontent.com/misiektoja/github_monitor/refs/heads/main/assets/github_list_of_repos.png" alt="github_list_of_repos" width="90%"/>
|
|
320
333
|
</p>
|
|
321
334
|
|
|
335
|
+
By default, only user-owned repos are listed. To include forks and collaborations, set `GET_ALL_REPOS` to `True` or use the `-a` flag:
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
github_monitor github_username -r -a
|
|
339
|
+
```
|
|
340
|
+
|
|
322
341
|
If you want to display a list of repositories starred by the user then use the `-g` flag:
|
|
323
342
|
|
|
324
343
|
```sh
|
|
@@ -367,7 +386,7 @@ To get email notifications when changes in user repositories are detected (e.g.
|
|
|
367
386
|
- or use the `-q` flag
|
|
368
387
|
|
|
369
388
|
```sh
|
|
370
|
-
github_monitor github_username -q
|
|
389
|
+
github_monitor github_username -j -q
|
|
371
390
|
```
|
|
372
391
|
|
|
373
392
|
To be informed whenever changes in the update date of user repositories are detected:
|
|
@@ -375,11 +394,21 @@ To be informed whenever changes in the update date of user repositories are dete
|
|
|
375
394
|
- or use the `-u` flag
|
|
376
395
|
|
|
377
396
|
```sh
|
|
378
|
-
github_monitor github_username -u
|
|
397
|
+
github_monitor github_username -j -u
|
|
379
398
|
```
|
|
380
399
|
|
|
381
400
|
The last two options (`-q` and `-u`) only work if tracking of repositories changes is enabled (`-j`).
|
|
382
401
|
|
|
402
|
+
To be informed about user's daily contributions:
|
|
403
|
+
- set `CONTRIB_NOTIFICATION` to `True`
|
|
404
|
+
- or use the `-y` flag
|
|
405
|
+
|
|
406
|
+
```sh
|
|
407
|
+
github_monitor github_username -m -y
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The `-y` only works if tracking of daily contributions is enabled (`-m`).
|
|
411
|
+
|
|
383
412
|
To disable sending an email on errors (enabled by default):
|
|
384
413
|
- set `ERROR_NOTIFICATION` to `False`
|
|
385
414
|
- or use the `-e` flag
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Author: Michal Szymanski <misiektoja-github@rm-rf.ninja>
|
|
4
|
-
v2.
|
|
4
|
+
v2.2
|
|
5
5
|
|
|
6
6
|
OSINT tool implementing real-time tracking of GitHub users activities including profile and repositories changes:
|
|
7
7
|
https://github.com/misiektoja/github_monitor/
|
|
@@ -16,7 +16,7 @@ tzlocal (optional)
|
|
|
16
16
|
python-dotenv (optional)
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
VERSION = "2.
|
|
19
|
+
VERSION = "2.2"
|
|
20
20
|
|
|
21
21
|
# ---------------------------
|
|
22
22
|
# CONFIGURATION SECTION START
|
|
@@ -32,8 +32,7 @@ CONFIG_BLOCK = """
|
|
|
32
32
|
# - Pass it at runtime with -t / --github-token
|
|
33
33
|
# - Set it as an environment variable (e.g. export GITHUB_TOKEN=...)
|
|
34
34
|
# - Add it to ".env" file (GITHUB_TOKEN=...) for persistent use
|
|
35
|
-
# Fallback:
|
|
36
|
-
# - Hard-code it in the code or config file
|
|
35
|
+
# - Fallback: hard-code it in the code or config file
|
|
37
36
|
GITHUB_TOKEN = "your_github_classic_personal_access_token"
|
|
38
37
|
|
|
39
38
|
# The URL of the GitHub API
|
|
@@ -77,6 +76,7 @@ EVENT_NOTIFICATION = False
|
|
|
77
76
|
|
|
78
77
|
# Whether to send an email when user's repositories change (stargazers, watchers, forks, issues,
|
|
79
78
|
# PRs, description etc., except for update date)
|
|
79
|
+
# Requires TRACK_REPOS_CHANGES to be enabled
|
|
80
80
|
# Can also be enabled via the -q flag
|
|
81
81
|
REPO_NOTIFICATION = False
|
|
82
82
|
|
|
@@ -84,6 +84,11 @@ REPO_NOTIFICATION = False
|
|
|
84
84
|
# Can also be enabled via the -u flag
|
|
85
85
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
86
86
|
|
|
87
|
+
# Whether to send an email when user's daily contributions count changes
|
|
88
|
+
# Requires TRACK_CONTRIB_CHANGES to be enabled
|
|
89
|
+
# Can also be enabled via the -y flag
|
|
90
|
+
CONTRIB_NOTIFICATION = False
|
|
91
|
+
|
|
87
92
|
# Whether to send an email on errors
|
|
88
93
|
# Can also be disabled via the -e flag
|
|
89
94
|
ERROR_NOTIFICATION = True
|
|
@@ -127,6 +132,25 @@ EVENTS_TO_MONITOR = [
|
|
|
127
132
|
# any events older than the most recent EVENTS_NUMBER will be missed
|
|
128
133
|
EVENTS_NUMBER = 30 # 1 page
|
|
129
134
|
|
|
135
|
+
# If True, track user's repository changes (changed stargazers, watchers, forks, description, update date etc.)
|
|
136
|
+
# Can also be enabled using the -j flag
|
|
137
|
+
TRACK_REPOS_CHANGES = False
|
|
138
|
+
|
|
139
|
+
# If True, disable event monitoring
|
|
140
|
+
# Can also be disabled using the -k flag
|
|
141
|
+
DO_NOT_MONITOR_GITHUB_EVENTS = False
|
|
142
|
+
|
|
143
|
+
# If True, fetch all user repos (owned, forks, collaborations); otherwise, fetch only owned repos
|
|
144
|
+
GET_ALL_REPOS = False
|
|
145
|
+
|
|
146
|
+
# Alert about blocked (403 - TOS violation and 451 - DMCA block) repos in the console output (in monitoring mode)
|
|
147
|
+
# In listing mode (-r), blocked repos are always shown
|
|
148
|
+
BLOCKED_REPOS = False
|
|
149
|
+
|
|
150
|
+
# If True, track and log user's daily contributions count changes
|
|
151
|
+
# Can also be enabled using the -m flag
|
|
152
|
+
TRACK_CONTRIB_CHANGES = False
|
|
153
|
+
|
|
130
154
|
# How often to print a "liveness check" message to the output; in seconds
|
|
131
155
|
# Set to 0 to disable
|
|
132
156
|
LIVENESS_CHECK_INTERVAL = 43200 # 12 hours
|
|
@@ -194,11 +218,17 @@ PROFILE_NOTIFICATION = False
|
|
|
194
218
|
EVENT_NOTIFICATION = False
|
|
195
219
|
REPO_NOTIFICATION = False
|
|
196
220
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
221
|
+
CONTRIB_NOTIFICATION = False
|
|
197
222
|
ERROR_NOTIFICATION = False
|
|
198
223
|
GITHUB_CHECK_INTERVAL = 0
|
|
199
224
|
LOCAL_TIMEZONE = ""
|
|
200
225
|
EVENTS_TO_MONITOR = []
|
|
201
226
|
EVENTS_NUMBER = 0
|
|
227
|
+
TRACK_REPOS_CHANGES = False
|
|
228
|
+
DO_NOT_MONITOR_GITHUB_EVENTS = False
|
|
229
|
+
GET_ALL_REPOS = False
|
|
230
|
+
BLOCKED_REPOS = False
|
|
231
|
+
TRACK_CONTRIB_CHANGES = False
|
|
202
232
|
LIVENESS_CHECK_INTERVAL = 0
|
|
203
233
|
CHECK_INTERNET_URL = ""
|
|
204
234
|
CHECK_INTERNET_TIMEOUT = 0
|
|
@@ -226,9 +256,6 @@ LIVENESS_CHECK_COUNTER = LIVENESS_CHECK_INTERVAL / GITHUB_CHECK_INTERVAL
|
|
|
226
256
|
stdout_bck = None
|
|
227
257
|
csvfieldnames = ['Date', 'Type', 'Name', 'Old', 'New']
|
|
228
258
|
|
|
229
|
-
TRACK_REPOS_CHANGES = False
|
|
230
|
-
DO_NOT_MONITOR_GITHUB_EVENTS = False
|
|
231
|
-
|
|
232
259
|
CLI_CONFIG_PATH = None
|
|
233
260
|
|
|
234
261
|
# to solve the issue: 'SyntaxError: f-string expression part cannot include a backslash'
|
|
@@ -244,7 +271,7 @@ if sys.version_info < (3, 10):
|
|
|
244
271
|
import time
|
|
245
272
|
import string
|
|
246
273
|
import os
|
|
247
|
-
from datetime import datetime, timezone
|
|
274
|
+
from datetime import datetime, timezone, date
|
|
248
275
|
from dateutil import relativedelta
|
|
249
276
|
from dateutil.parser import isoparse
|
|
250
277
|
import calendar
|
|
@@ -270,6 +297,7 @@ import re
|
|
|
270
297
|
import ipaddress
|
|
271
298
|
try:
|
|
272
299
|
from github import Github, Auth, GithubException, UnknownObjectException
|
|
300
|
+
from github.GithubException import RateLimitExceededException
|
|
273
301
|
from github.GithubException import BadCredentialsException
|
|
274
302
|
except ModuleNotFoundError:
|
|
275
303
|
raise SystemExit("Error: Couldn't find the PyGitHub library !\n\nTo install it, run:\n pip3 install PyGithub\n\nOnce installed, re-run this tool. For more help, visit:\nhttps://github.com/PyGithub/PyGithub")
|
|
@@ -280,7 +308,9 @@ import socket
|
|
|
280
308
|
from typing import Any, Callable
|
|
281
309
|
import shutil
|
|
282
310
|
from pathlib import Path
|
|
283
|
-
|
|
311
|
+
from typing import Optional
|
|
312
|
+
import datetime as dt
|
|
313
|
+
import requests
|
|
284
314
|
|
|
285
315
|
NET_ERRORS = (
|
|
286
316
|
req.exceptions.RequestException,
|
|
@@ -558,6 +588,11 @@ def now_local_naive():
|
|
|
558
588
|
return datetime.now(pytz.timezone(LOCAL_TIMEZONE)).replace(microsecond=0, tzinfo=None)
|
|
559
589
|
|
|
560
590
|
|
|
591
|
+
# Returns today's date in LOCAL_TIMEZONE (naive date)
|
|
592
|
+
def today_local() -> dt.date:
|
|
593
|
+
return now_local_naive().date()
|
|
594
|
+
|
|
595
|
+
|
|
561
596
|
# Returns the current date/time in human readable format; eg. Sun 21 Apr 2024, 15:08:45
|
|
562
597
|
def get_cur_ts(ts_str=""):
|
|
563
598
|
return (f'{ts_str}{calendar.day_abbr[(now_local_naive()).weekday()]}, {now_local_naive().strftime("%d %b %Y, %H:%M:%S")}')
|
|
@@ -627,6 +662,11 @@ def get_short_date_from_ts(ts, show_year=False, show_hour=True, show_weekday=Tru
|
|
|
627
662
|
ts_rounded = int(round(ts))
|
|
628
663
|
ts_new = datetime.fromtimestamp(ts_rounded, tz)
|
|
629
664
|
|
|
665
|
+
elif isinstance(ts, date):
|
|
666
|
+
ts = datetime.combine(ts, datetime.min.time())
|
|
667
|
+
ts = pytz.utc.localize(ts)
|
|
668
|
+
ts_new = ts.astimezone(tz)
|
|
669
|
+
|
|
630
670
|
else:
|
|
631
671
|
return ""
|
|
632
672
|
|
|
@@ -832,6 +872,34 @@ def gh_call(fn: Callable[..., Any], retries=NET_MAX_RETRIES, backoff=NET_BASE_BA
|
|
|
832
872
|
for i in range(1, retries + 1):
|
|
833
873
|
try:
|
|
834
874
|
return fn(*args, **kwargs)
|
|
875
|
+
except RateLimitExceededException as e:
|
|
876
|
+
headers = getattr(e, "headers", None)
|
|
877
|
+
|
|
878
|
+
reset_str = None
|
|
879
|
+
if headers:
|
|
880
|
+
val = headers.get("X-RateLimit-Reset")
|
|
881
|
+
if isinstance(val, str):
|
|
882
|
+
reset_str = val
|
|
883
|
+
|
|
884
|
+
sleep_for: int
|
|
885
|
+
if reset_str is not None and reset_str.isdigit():
|
|
886
|
+
reset_epoch = int(reset_str)
|
|
887
|
+
sleep_for = max(0, reset_epoch - int(time.time()) + 1)
|
|
888
|
+
else:
|
|
889
|
+
retry_after_str = None
|
|
890
|
+
if headers:
|
|
891
|
+
ra = headers.get("Retry-After")
|
|
892
|
+
if isinstance(ra, str):
|
|
893
|
+
retry_after_str = ra
|
|
894
|
+
if retry_after_str is not None and retry_after_str.isdigit():
|
|
895
|
+
sleep_for = int(retry_after_str)
|
|
896
|
+
else:
|
|
897
|
+
sleep_for = int(backoff * i)
|
|
898
|
+
|
|
899
|
+
print(f"* {fn.__name__} rate limited, sleeping {sleep_for}s (retry {i}/{retries})")
|
|
900
|
+
time.sleep(sleep_for)
|
|
901
|
+
continue
|
|
902
|
+
|
|
835
903
|
except NET_ERRORS as e:
|
|
836
904
|
print(f"* {fn.__name__} error: {e} (retry {i}/{retries})")
|
|
837
905
|
time.sleep(backoff * i)
|
|
@@ -911,6 +979,7 @@ def github_print_followers_and_followings(user):
|
|
|
911
979
|
|
|
912
980
|
# Processes items from all passed repositories and returns a list of dictionaries
|
|
913
981
|
def github_process_repos(repos_list):
|
|
982
|
+
import logging
|
|
914
983
|
list_of_repos = []
|
|
915
984
|
stargazers_list = []
|
|
916
985
|
subscribers_list = []
|
|
@@ -921,9 +990,24 @@ def github_process_repos(repos_list):
|
|
|
921
990
|
try:
|
|
922
991
|
repo_created_date = repo.created_at
|
|
923
992
|
repo_updated_date = repo.updated_at
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
993
|
+
|
|
994
|
+
github_logger = logging.getLogger('github')
|
|
995
|
+
original_level = github_logger.level
|
|
996
|
+
github_logger.setLevel(logging.ERROR)
|
|
997
|
+
|
|
998
|
+
try:
|
|
999
|
+
stargazers_list = [star.login for star in repo.get_stargazers()]
|
|
1000
|
+
subscribers_list = [subscriber.login for subscriber in repo.get_subscribers()]
|
|
1001
|
+
forked_repos = [fork.full_name for fork in repo.get_forks()]
|
|
1002
|
+
except GithubException as e:
|
|
1003
|
+
if e.status in [403, 451]:
|
|
1004
|
+
if BLOCKED_REPOS:
|
|
1005
|
+
print(f"* Repo '{repo.name}' is blocked, skipping for now: {e}")
|
|
1006
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1007
|
+
continue
|
|
1008
|
+
raise
|
|
1009
|
+
finally:
|
|
1010
|
+
github_logger.setLevel(original_level)
|
|
927
1011
|
|
|
928
1012
|
issues = list(repo.get_issues(state='open'))
|
|
929
1013
|
pulls = list(repo.get_pulls(state='open'))
|
|
@@ -936,8 +1020,20 @@ def github_process_repos(repos_list):
|
|
|
936
1020
|
pr_list = [f"#{pr.number} {pr.title} ({pr.user.login}) [ {pr.html_url} ]" for pr in pulls]
|
|
937
1021
|
|
|
938
1022
|
list_of_repos.append({"name": repo.name, "descr": repo.description, "is_fork": repo.fork, "forks": repo.forks_count, "stars": repo.stargazers_count, "subscribers": repo.subscribers_count, "url": repo.html_url, "language": repo.language, "date": repo_created_date, "update_date": repo_updated_date, "stargazers_list": stargazers_list, "forked_repos": forked_repos, "subscribers_list": subscribers_list, "issues": issue_count, "pulls": pr_count, "issues_list": issues_list, "pulls_list": pr_list})
|
|
1023
|
+
|
|
1024
|
+
except GithubException as e:
|
|
1025
|
+
# Skip TOS-blocked (403) and legally blocked (451) repositories
|
|
1026
|
+
if e.status in [403, 451]:
|
|
1027
|
+
if BLOCKED_REPOS:
|
|
1028
|
+
print(f"* Repo '{repo.name}' is blocked, skipping for now: {e}")
|
|
1029
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1030
|
+
continue
|
|
1031
|
+
else:
|
|
1032
|
+
print(f"* Cannot process repo '{repo.name}', skipping for now: {e}")
|
|
1033
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1034
|
+
continue
|
|
939
1035
|
except Exception as e:
|
|
940
|
-
print(f"*
|
|
1036
|
+
print(f"* Cannot process repo '{repo.name}', skipping for now: {e}")
|
|
941
1037
|
print_cur_ts("Timestamp:\t\t\t")
|
|
942
1038
|
continue
|
|
943
1039
|
|
|
@@ -946,6 +1042,7 @@ def github_process_repos(repos_list):
|
|
|
946
1042
|
|
|
947
1043
|
# Prints a list of public repositories for a GitHub user (-r)
|
|
948
1044
|
def github_print_repos(user):
|
|
1045
|
+
import logging
|
|
949
1046
|
user_name_str = user
|
|
950
1047
|
user_url = "-"
|
|
951
1048
|
repos_count = 0
|
|
@@ -962,8 +1059,12 @@ def github_print_repos(user):
|
|
|
962
1059
|
user_name = g_user.name
|
|
963
1060
|
user_url = g_user.html_url
|
|
964
1061
|
|
|
965
|
-
|
|
966
|
-
|
|
1062
|
+
if GET_ALL_REPOS:
|
|
1063
|
+
repos_list = g_user.get_repos()
|
|
1064
|
+
repos_count = g_user.public_repos
|
|
1065
|
+
else:
|
|
1066
|
+
repos_list = [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login]
|
|
1067
|
+
repos_count = len(repos_list)
|
|
967
1068
|
|
|
968
1069
|
user_name_str = user_login
|
|
969
1070
|
if user_name:
|
|
@@ -974,6 +1075,7 @@ def github_print_repos(user):
|
|
|
974
1075
|
print(f"\nUsername:\t\t{user_name_str}")
|
|
975
1076
|
print(f"User URL:\t\t{user_url}/")
|
|
976
1077
|
print(f"GitHub API URL:\t\t{GITHUB_API_URL}")
|
|
1078
|
+
print(f"Owned repos only:\t{not GET_ALL_REPOS}")
|
|
977
1079
|
print(f"Local timezone:\t\t{LOCAL_TIMEZONE}")
|
|
978
1080
|
|
|
979
1081
|
print(f"\nRepositories:\t\t{repos_count}\n")
|
|
@@ -984,6 +1086,10 @@ def github_print_repos(user):
|
|
|
984
1086
|
for repo in repos_list:
|
|
985
1087
|
print(f"🔸 {repo.name} {'(fork)' if repo.fork else ''} \n")
|
|
986
1088
|
|
|
1089
|
+
github_logger = logging.getLogger('github')
|
|
1090
|
+
original_level = github_logger.level
|
|
1091
|
+
github_logger.setLevel(logging.ERROR)
|
|
1092
|
+
|
|
987
1093
|
try:
|
|
988
1094
|
pr_count = repo.get_pulls(state='open').totalCount
|
|
989
1095
|
issue_count = repo.open_issues_count - pr_count
|
|
@@ -991,26 +1097,36 @@ def github_print_repos(user):
|
|
|
991
1097
|
pr_count = "?"
|
|
992
1098
|
issue_count = "?"
|
|
993
1099
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1100
|
+
try:
|
|
1101
|
+
print(f" - 🌐 URL:\t\t{repo.html_url}")
|
|
1102
|
+
print(f" - 💻 Language:\t\t{repo.language}")
|
|
1103
|
+
|
|
1104
|
+
print(f"\n - ⭐ Stars:\t\t{repo.stargazers_count}")
|
|
1105
|
+
print(f" - 🍴 Forks:\t\t{repo.forks_count}")
|
|
1106
|
+
print(f" - 👓 Watchers:\t\t{repo.subscribers_count}")
|
|
1107
|
+
|
|
1108
|
+
# print(f" - 🐞 Issues+PRs:\t{repo.open_issues_count}")
|
|
1109
|
+
print(f" - 🐞 Issues:\t\t{issue_count}")
|
|
1110
|
+
print(f" - 📬 PRs:\t\t{pr_count}")
|
|
1111
|
+
|
|
1112
|
+
print(f"\n - 📝 License:\t\t{repo.license.name if repo.license else 'None'}")
|
|
1113
|
+
print(f" - 🌿 Branch (default):\t{repo.default_branch}")
|
|
1114
|
+
|
|
1115
|
+
print(f"\n - 📅 Created:\t\t{get_date_from_ts(repo.created_at)} ({calculate_timespan(int(time.time()), repo.created_at, granularity=2)} ago)")
|
|
1116
|
+
print(f" - 🔄 Updated:\t\t{get_date_from_ts(repo.updated_at)} ({calculate_timespan(int(time.time()), repo.updated_at, granularity=2)} ago)")
|
|
1117
|
+
print(f" - 🔃 Last push:\t{get_date_from_ts(repo.pushed_at)} ({calculate_timespan(int(time.time()), repo.pushed_at, granularity=2)} ago)")
|
|
1118
|
+
|
|
1119
|
+
if repo.description:
|
|
1120
|
+
print(f"\n - 📝 Desc:\t\t{repo.description}")
|
|
1121
|
+
except GithubException as e:
|
|
1122
|
+
# Inform about TOS-blocked (403) and legally blocked (451) repositories
|
|
1123
|
+
if e.status in [403, 451]:
|
|
1124
|
+
print(f"\n* Repo '{repo.name}' is blocked: {e}")
|
|
1125
|
+
print("─" * HORIZONTAL_LINE2)
|
|
1126
|
+
continue
|
|
1127
|
+
finally:
|
|
1128
|
+
github_logger.setLevel(original_level)
|
|
1011
1129
|
|
|
1012
|
-
if repo.description:
|
|
1013
|
-
print(f"\n - 📝 Desc:\t\t{repo.description}")
|
|
1014
1130
|
print("─" * HORIZONTAL_LINE2)
|
|
1015
1131
|
except Exception as e:
|
|
1016
1132
|
raise RuntimeError(f"Cannot fetch user's repositories list: {e}")
|
|
@@ -1102,19 +1218,31 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1102
1218
|
st += print_v(f"Event type:\t\t\t{event.type}")
|
|
1103
1219
|
|
|
1104
1220
|
if event.repo.id:
|
|
1105
|
-
repo_name = event.repo.name
|
|
1106
|
-
repo_url = event.repo.url.replace("https://api.github.com/repos/", "https://github.com/")
|
|
1107
|
-
st += print_v(f"\nRepo name:\t\t\t{repo_name}")
|
|
1108
|
-
st += print_v(f"Repo URL:\t\t\t{repo_url}")
|
|
1109
|
-
|
|
1110
1221
|
try:
|
|
1111
1222
|
desc_len = 80
|
|
1112
1223
|
repo = g.get_repo(event.repo.name)
|
|
1113
|
-
|
|
1224
|
+
|
|
1225
|
+
# For ForkEvent, prefer the source repo if available
|
|
1226
|
+
if event.type == "ForkEvent" and repo is not None:
|
|
1227
|
+
try:
|
|
1228
|
+
parent = gh_call(lambda: getattr(repo, "parent", None))()
|
|
1229
|
+
if parent:
|
|
1230
|
+
repo = parent
|
|
1231
|
+
except Exception:
|
|
1232
|
+
pass
|
|
1233
|
+
|
|
1234
|
+
repo_name = getattr(repo, "full_name", event.repo.name)
|
|
1235
|
+
repo_url = getattr(repo, "html_url", event.repo.url.replace("https://api.github.com/repos/", "https://github.com/"))
|
|
1236
|
+
|
|
1237
|
+
st += print_v(f"\nRepo name:\t\t\t{repo_name}")
|
|
1238
|
+
st += print_v(f"Repo URL:\t\t\t{repo_url}")
|
|
1239
|
+
|
|
1240
|
+
desc = (repo.description or "") if repo else ""
|
|
1114
1241
|
cleaned = desc.replace('\n', ' ')
|
|
1115
1242
|
short_desc = cleaned[:desc_len] + '...' if len(cleaned) > desc_len else cleaned
|
|
1116
1243
|
if short_desc:
|
|
1117
1244
|
st += print_v(f"Repo description:\t\t{short_desc}")
|
|
1245
|
+
|
|
1118
1246
|
except UnknownObjectException:
|
|
1119
1247
|
repo = None
|
|
1120
1248
|
st += print_v("\nRepository not found or has been removed")
|
|
@@ -1142,6 +1270,7 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1142
1270
|
if event.payload.get("action"):
|
|
1143
1271
|
st += print_v(f"\nAction:\t\t\t\t{event.payload.get('action')}")
|
|
1144
1272
|
|
|
1273
|
+
# Prefer commits from payload when present (older API behavior)
|
|
1145
1274
|
if event.payload.get("commits"):
|
|
1146
1275
|
commits = event.payload["commits"]
|
|
1147
1276
|
commits_total = len(commits)
|
|
@@ -1152,7 +1281,7 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1152
1281
|
|
|
1153
1282
|
commit_details = None
|
|
1154
1283
|
if repo:
|
|
1155
|
-
commit_details = repo.get_commit(commit["sha"])
|
|
1284
|
+
commit_details = gh_call(lambda: repo.get_commit(commit["sha"]))()
|
|
1156
1285
|
|
|
1157
1286
|
if commit_details:
|
|
1158
1287
|
commit_date = commit_details.commit.author.date
|
|
@@ -1188,6 +1317,81 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1188
1317
|
st += print_v(f"\n - Commit message:\t\t'{commit['message']}'")
|
|
1189
1318
|
st += print_v("." * HORIZONTAL_LINE1)
|
|
1190
1319
|
|
|
1320
|
+
# Fallback for new Events API where PushEvent no longer includes commit summaries
|
|
1321
|
+
elif event.type == "PushEvent" and repo:
|
|
1322
|
+
before_sha = event.payload.get("before")
|
|
1323
|
+
head_sha = event.payload.get("head") or event.payload.get("after")
|
|
1324
|
+
size_hint = event.payload.get("size")
|
|
1325
|
+
|
|
1326
|
+
# Debug when payload has no commits
|
|
1327
|
+
# st += print_v("\n[debug] PushEvent payload has no 'commits' array; using compare API")
|
|
1328
|
+
# st += print_v(f"[debug] before:\t\t\t{before_sha}")
|
|
1329
|
+
# st += print_v(f"[debug] head/after:\t\t{head_sha}")
|
|
1330
|
+
if size_hint is not None:
|
|
1331
|
+
st += print_v(f"[debug] size (hint):\t\t{size_hint}")
|
|
1332
|
+
|
|
1333
|
+
if before_sha and head_sha and before_sha != head_sha:
|
|
1334
|
+
try:
|
|
1335
|
+
compare = gh_call(lambda: repo.compare(before_sha, head_sha))()
|
|
1336
|
+
except Exception as e:
|
|
1337
|
+
compare = None
|
|
1338
|
+
st += print_v(f"* Error using compare({before_sha[:12]}...{head_sha[:12]}): {e}")
|
|
1339
|
+
|
|
1340
|
+
if compare:
|
|
1341
|
+
commits = list(compare.commits)
|
|
1342
|
+
commits_total = len(commits)
|
|
1343
|
+
short_repo = getattr(repo, "full_name", repo_name)
|
|
1344
|
+
compare_url = f"https://github.com/{short_repo}/compare/{before_sha[:12]}...{head_sha[:12]}"
|
|
1345
|
+
st += print_v(f"\nNumber of commits:\t\t{commits_total}")
|
|
1346
|
+
st += print_v(f"Compare URL:\t\t\t{compare_url}")
|
|
1347
|
+
|
|
1348
|
+
for commit_count, c in enumerate(commits, start=1):
|
|
1349
|
+
st += print_v(f"\n=== Commit {commit_count}/{commits_total} ===")
|
|
1350
|
+
st += print_v("." * HORIZONTAL_LINE1)
|
|
1351
|
+
|
|
1352
|
+
commit_sha = getattr(c, "sha", None) or getattr(c, "id", None)
|
|
1353
|
+
commit_details = gh_call(lambda: repo.get_commit(commit_sha))() if (repo and commit_sha) else None
|
|
1354
|
+
|
|
1355
|
+
if commit_details:
|
|
1356
|
+
commit_date = commit_details.commit.author.date
|
|
1357
|
+
st += print_v(f" - Commit date:\t\t\t{get_date_from_ts(commit_date)}")
|
|
1358
|
+
|
|
1359
|
+
if commit_sha:
|
|
1360
|
+
st += print_v(f" - Commit SHA:\t\t\t{commit_sha}")
|
|
1361
|
+
|
|
1362
|
+
author_name = None
|
|
1363
|
+
if commit_details and commit_details.commit and commit_details.commit.author:
|
|
1364
|
+
author_name = commit_details.commit.author.name
|
|
1365
|
+
st += print_v(f" - Commit author:\t\t{author_name or 'N/A'}")
|
|
1366
|
+
|
|
1367
|
+
if commit_details and commit_details.author:
|
|
1368
|
+
st += print_v(f" - Commit author URL:\t\t{commit_details.author.html_url}")
|
|
1369
|
+
|
|
1370
|
+
if commit_details:
|
|
1371
|
+
st += print_v(f" - Commit URL:\t\t\t{commit_details.html_url}")
|
|
1372
|
+
st += print_v(f" - Commit raw patch URL:\t{commit_details.html_url}.patch")
|
|
1373
|
+
|
|
1374
|
+
stats = getattr(commit_details, "stats", None)
|
|
1375
|
+
additions = stats.additions if stats else 0
|
|
1376
|
+
deletions = stats.deletions if stats else 0
|
|
1377
|
+
stats_total = stats.total if stats else 0
|
|
1378
|
+
st += print_v(f"\n - Additions/Deletions:\t\t+{additions} / -{deletions} ({stats_total})")
|
|
1379
|
+
|
|
1380
|
+
try:
|
|
1381
|
+
file_count = sum(1 for _ in commit_details.files)
|
|
1382
|
+
except Exception:
|
|
1383
|
+
file_count = "N/A"
|
|
1384
|
+
st += print_v(f" - Files changed:\t\t{file_count}")
|
|
1385
|
+
if file_count and file_count != "N/A":
|
|
1386
|
+
st += print_v(" - Changed files list:")
|
|
1387
|
+
for f in commit_details.files:
|
|
1388
|
+
st += print_v(f" • '{f.filename}' - {f.status} (+{f.additions} / -{f.deletions})")
|
|
1389
|
+
|
|
1390
|
+
st += print_v(f"\n - Commit message:\t\t'{commit_details.commit.message}'")
|
|
1391
|
+
st += print_v("." * HORIZONTAL_LINE1)
|
|
1392
|
+
else:
|
|
1393
|
+
st += print_v("\nNo compare range available (forced push, tag push, or identical before/after)")
|
|
1394
|
+
|
|
1191
1395
|
if event.payload.get("commits") == []:
|
|
1192
1396
|
st += print_v("\nNo new commits (forced push, tag push, branch reset or other ref update)")
|
|
1193
1397
|
|
|
@@ -1927,6 +2131,97 @@ def is_profile_public(g: Github, user, new_account_days=30):
|
|
|
1927
2131
|
|
|
1928
2132
|
return False
|
|
1929
2133
|
|
|
2134
|
+
|
|
2135
|
+
# Returns a dict mapping 'YYYY-MM-DD' -> int contribution count for the range
|
|
2136
|
+
def get_daily_contributions(username: str, start: Optional[dt.date] = None, end: Optional[dt.date] = None, token: Optional[str] = None) -> dict:
|
|
2137
|
+
if token is None:
|
|
2138
|
+
raise ValueError("GitHub token is required")
|
|
2139
|
+
|
|
2140
|
+
today = dt.date.today()
|
|
2141
|
+
if start is None:
|
|
2142
|
+
start = today
|
|
2143
|
+
if end is None:
|
|
2144
|
+
end = today
|
|
2145
|
+
|
|
2146
|
+
url = GITHUB_API_URL.rstrip("/") + "/graphql"
|
|
2147
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
2148
|
+
|
|
2149
|
+
start_iso = dt.datetime.combine(start, dt.time.min).isoformat()
|
|
2150
|
+
to_exclusive = end + dt.timedelta(days=1)
|
|
2151
|
+
end_iso = dt.datetime.combine(to_exclusive, dt.time.min).isoformat()
|
|
2152
|
+
|
|
2153
|
+
query = """
|
|
2154
|
+
query($login: String!, $from: DateTime!, $to: DateTime!) {
|
|
2155
|
+
user(login: $login) {
|
|
2156
|
+
contributionsCollection(from: $from, to: $to) {
|
|
2157
|
+
contributionCalendar {
|
|
2158
|
+
weeks {
|
|
2159
|
+
contributionDays {
|
|
2160
|
+
date
|
|
2161
|
+
contributionCount
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}"""
|
|
2168
|
+
|
|
2169
|
+
variables = {"login": username, "from": start_iso, "to": end_iso}
|
|
2170
|
+
r = requests.post(url, json={"query": query, "variables": variables}, headers=headers, timeout=30)
|
|
2171
|
+
r.raise_for_status()
|
|
2172
|
+
data = r.json()
|
|
2173
|
+
|
|
2174
|
+
days = data["data"]["user"]["contributionsCollection"]["contributionCalendar"]["weeks"]
|
|
2175
|
+
out = {}
|
|
2176
|
+
for w in days:
|
|
2177
|
+
for d in w["contributionDays"]:
|
|
2178
|
+
date = d["date"]
|
|
2179
|
+
if start <= dt.date.fromisoformat(date) <= end:
|
|
2180
|
+
out[date] = d["contributionCount"]
|
|
2181
|
+
return out
|
|
2182
|
+
|
|
2183
|
+
|
|
2184
|
+
# Return contribution count for a single day
|
|
2185
|
+
def get_daily_contributions_count(username: str, day: dt.date, token: str) -> int:
|
|
2186
|
+
data = get_daily_contributions(username, day, day, token)
|
|
2187
|
+
return next(iter(data.values()), 0)
|
|
2188
|
+
|
|
2189
|
+
|
|
2190
|
+
# Checks count for today and decides whether to notify based on stored state.
|
|
2191
|
+
def check_daily_contribs(username: str, token: str, state: dict, min_delta: int = 1, fail_threshold: int = 3) -> tuple[bool, int, bool]:
|
|
2192
|
+
day = today_local()
|
|
2193
|
+
|
|
2194
|
+
try:
|
|
2195
|
+
curr = get_daily_contributions_count(username, day, token=token)
|
|
2196
|
+
state["consecutive_failures"] = 0
|
|
2197
|
+
state["last_error"] = None
|
|
2198
|
+
except Exception as e:
|
|
2199
|
+
state["consecutive_failures"] = state.get("consecutive_failures", 0) + 1
|
|
2200
|
+
state["last_error"] = f"{type(e).__name__}: {e}"
|
|
2201
|
+
error_notify = state["consecutive_failures"] >= fail_threshold
|
|
2202
|
+
return False, state.get("count", 0), error_notify
|
|
2203
|
+
|
|
2204
|
+
prev_day = state.get("day")
|
|
2205
|
+
prev_cnt = state.get("count")
|
|
2206
|
+
|
|
2207
|
+
# New day -> reset baseline silently
|
|
2208
|
+
if prev_day != day:
|
|
2209
|
+
state["day"] = day
|
|
2210
|
+
state["count"] = curr
|
|
2211
|
+
state["prev_count"] = curr
|
|
2212
|
+
return False, curr, False # no notify on rollover
|
|
2213
|
+
|
|
2214
|
+
# Same day -> notify if change >= threshold
|
|
2215
|
+
if prev_cnt is not None and abs(curr - prev_cnt) >= min_delta:
|
|
2216
|
+
state["prev_count"] = prev_cnt
|
|
2217
|
+
state["count"] = curr
|
|
2218
|
+
return True, curr, False
|
|
2219
|
+
|
|
2220
|
+
# No change
|
|
2221
|
+
state["count"] = curr
|
|
2222
|
+
return False, curr, False
|
|
2223
|
+
|
|
2224
|
+
|
|
1930
2225
|
# Monitors activity of the specified GitHub user
|
|
1931
2226
|
def github_monitor_user(user, csv_file_name):
|
|
1932
2227
|
|
|
@@ -1946,6 +2241,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1946
2241
|
event_date: datetime | None = None
|
|
1947
2242
|
blocked = None
|
|
1948
2243
|
public = False
|
|
2244
|
+
contrib_state = {}
|
|
2245
|
+
contrib_curr = 0
|
|
1949
2246
|
|
|
1950
2247
|
print("Sneaking into GitHub like a ninja ...")
|
|
1951
2248
|
|
|
@@ -1971,11 +2268,16 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1971
2268
|
|
|
1972
2269
|
followers_count = g_user.followers
|
|
1973
2270
|
followings_count = g_user.following
|
|
1974
|
-
repos_count = g_user.public_repos
|
|
1975
2271
|
|
|
1976
2272
|
followers_list = g_user.get_followers()
|
|
1977
2273
|
followings_list = g_user.get_following()
|
|
1978
|
-
|
|
2274
|
+
|
|
2275
|
+
if GET_ALL_REPOS:
|
|
2276
|
+
repos_list = g_user.get_repos()
|
|
2277
|
+
repos_count = g_user.public_repos
|
|
2278
|
+
else:
|
|
2279
|
+
repos_list = [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login]
|
|
2280
|
+
repos_count = len(repos_list)
|
|
1979
2281
|
|
|
1980
2282
|
starred_list = g_user.get_starred()
|
|
1981
2283
|
starred_count = starred_list.totalCount
|
|
@@ -1983,6 +2285,14 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1983
2285
|
public = is_profile_public(g, user)
|
|
1984
2286
|
blocked = is_blocked_by(user) if public else None
|
|
1985
2287
|
|
|
2288
|
+
if TRACK_CONTRIB_CHANGES:
|
|
2289
|
+
contrib_curr = get_daily_contributions_count(user, today_local(), token=GITHUB_TOKEN)
|
|
2290
|
+
contrib_state = {
|
|
2291
|
+
"day": today_local(),
|
|
2292
|
+
"count": contrib_curr,
|
|
2293
|
+
"prev_count": contrib_curr
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1986
2296
|
if not DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
1987
2297
|
events = list(islice(g_user.get_events(), EVENTS_NUMBER))
|
|
1988
2298
|
available_events = len(events)
|
|
@@ -2063,6 +2373,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2063
2373
|
print(f"Followings:\t\t\t{followings_count}")
|
|
2064
2374
|
print(f"Repositories:\t\t\t{repos_count}")
|
|
2065
2375
|
print(f"Starred repos:\t\t\t{starred_count}")
|
|
2376
|
+
if TRACK_CONTRIB_CHANGES:
|
|
2377
|
+
print(f"Today's contributions:\t\t{contrib_curr}")
|
|
2066
2378
|
|
|
2067
2379
|
if not DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
2068
2380
|
print(f"Available events:\t\t{available_events}{'+' if available_events == EVENTS_NUMBER else ''}")
|
|
@@ -2164,8 +2476,13 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2164
2476
|
followers_old, followers_old_count = handle_profile_change("Followers", followers_old_count, followers_count, followers_old, followers_raw, user, csv_file_name, field="login")
|
|
2165
2477
|
|
|
2166
2478
|
# Changed public repositories
|
|
2167
|
-
|
|
2168
|
-
|
|
2479
|
+
if GET_ALL_REPOS:
|
|
2480
|
+
repos_raw = list(gh_call(g_user.get_repos)())
|
|
2481
|
+
repos_count = gh_call(lambda: g_user.public_repos)()
|
|
2482
|
+
else:
|
|
2483
|
+
repos_raw = list(gh_call(lambda: [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login])())
|
|
2484
|
+
repos_count = len(repos_raw)
|
|
2485
|
+
|
|
2169
2486
|
if repos_raw is not None and repos_count is not None:
|
|
2170
2487
|
repos_old, repos_old_count = handle_profile_change("Repos", repos_old_count, repos_count, repos_old, repos_raw, user, csv_file_name, field="name")
|
|
2171
2488
|
|
|
@@ -2176,6 +2493,36 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2176
2493
|
starred_count = starred_raw.totalCount
|
|
2177
2494
|
starred_old, starred_old_count = handle_profile_change("Starred Repos", starred_old_count, starred_count, starred_old, starred_list, user, csv_file_name, field="full_name")
|
|
2178
2495
|
|
|
2496
|
+
# Changed contributions in a day
|
|
2497
|
+
if TRACK_CONTRIB_CHANGES:
|
|
2498
|
+
contrib_notify, contrib_curr, contrib_error_notify = check_daily_contribs(user, GITHUB_TOKEN, contrib_state, min_delta=1, fail_threshold=3)
|
|
2499
|
+
if contrib_error_notify and ERROR_NOTIFICATION:
|
|
2500
|
+
failures = contrib_state.get("consecutive_failures", 0)
|
|
2501
|
+
last_err = contrib_state.get("last_error", "Unknown error")
|
|
2502
|
+
err_msg = f"Error: GitHub daily contributions check failed {failures} times. Last error: {last_err}\n"
|
|
2503
|
+
print(err_msg)
|
|
2504
|
+
send_email(f"GitHub monitor errors for {user}", err_msg + get_cur_ts(nl_ch + "Timestamp: "), "", SMTP_SSL)
|
|
2505
|
+
|
|
2506
|
+
if contrib_notify:
|
|
2507
|
+
contrib_old = contrib_state.get("prev_count")
|
|
2508
|
+
print(f"* Daily contributions changed for user {user} on {get_short_date_from_ts(contrib_state['day'], show_hour=False)} from {contrib_old} to {contrib_curr}!\n")
|
|
2509
|
+
|
|
2510
|
+
try:
|
|
2511
|
+
if csv_file_name:
|
|
2512
|
+
write_csv_entry(csv_file_name, now_local_naive(), "Daily Contribs", user, contrib_old, contrib_curr)
|
|
2513
|
+
except Exception as e:
|
|
2514
|
+
print(f"* Error: {e}")
|
|
2515
|
+
|
|
2516
|
+
m_subject = f"GitHub user {user} daily contributions changed from {contrib_old} to {contrib_curr}!"
|
|
2517
|
+
m_body = (f"GitHub user {user} daily contributions changed on {get_short_date_from_ts(contrib_state['day'], show_hour=False)} from {contrib_old} to {contrib_curr}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}")
|
|
2518
|
+
|
|
2519
|
+
if CONTRIB_NOTIFICATION:
|
|
2520
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2521
|
+
send_email(m_subject, m_body, "", SMTP_SSL)
|
|
2522
|
+
|
|
2523
|
+
print(f"Check interval:\t\t\t{display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)})")
|
|
2524
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
2525
|
+
|
|
2179
2526
|
# Changed bio
|
|
2180
2527
|
bio = gh_call(lambda: g_user.bio)()
|
|
2181
2528
|
if bio is not None and bio != bio_old:
|
|
@@ -2404,7 +2751,12 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2404
2751
|
|
|
2405
2752
|
# Changed repos details
|
|
2406
2753
|
if TRACK_REPOS_CHANGES:
|
|
2407
|
-
|
|
2754
|
+
|
|
2755
|
+
if GET_ALL_REPOS:
|
|
2756
|
+
repos_list = gh_call(g_user.get_repos)()
|
|
2757
|
+
else:
|
|
2758
|
+
repos_list = gh_call(lambda: [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login])()
|
|
2759
|
+
|
|
2408
2760
|
if repos_list is not None:
|
|
2409
2761
|
try:
|
|
2410
2762
|
list_of_repos = github_process_repos(repos_list)
|
|
@@ -2581,7 +2933,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2581
2933
|
|
|
2582
2934
|
|
|
2583
2935
|
def main():
|
|
2584
|
-
global CLI_CONFIG_PATH, DOTENV_FILE, LOCAL_TIMEZONE, LIVENESS_CHECK_COUNTER, GITHUB_TOKEN, GITHUB_API_URL, CSV_FILE, DISABLE_LOGGING, GITHUB_LOGFILE, PROFILE_NOTIFICATION, EVENT_NOTIFICATION, REPO_NOTIFICATION, REPO_UPDATE_DATE_NOTIFICATION, ERROR_NOTIFICATION, GITHUB_CHECK_INTERVAL, SMTP_PASSWORD, stdout_bck, DO_NOT_MONITOR_GITHUB_EVENTS, TRACK_REPOS_CHANGES
|
|
2936
|
+
global CLI_CONFIG_PATH, DOTENV_FILE, LOCAL_TIMEZONE, LIVENESS_CHECK_COUNTER, GITHUB_TOKEN, GITHUB_API_URL, CSV_FILE, DISABLE_LOGGING, GITHUB_LOGFILE, PROFILE_NOTIFICATION, EVENT_NOTIFICATION, REPO_NOTIFICATION, REPO_UPDATE_DATE_NOTIFICATION, ERROR_NOTIFICATION, GITHUB_CHECK_INTERVAL, SMTP_PASSWORD, stdout_bck, DO_NOT_MONITOR_GITHUB_EVENTS, TRACK_REPOS_CHANGES, GET_ALL_REPOS, CONTRIB_NOTIFICATION, TRACK_CONTRIB_CHANGES
|
|
2585
2937
|
|
|
2586
2938
|
if "--generate-config" in sys.argv:
|
|
2587
2939
|
print(CONFIG_BLOCK.strip("\n"))
|
|
@@ -2688,6 +3040,13 @@ def main():
|
|
|
2688
3040
|
default=None,
|
|
2689
3041
|
help="Email when user's repositories update date changes"
|
|
2690
3042
|
)
|
|
3043
|
+
notify.add_argument(
|
|
3044
|
+
"-y", "--notify-daily-contribs",
|
|
3045
|
+
dest="notify_daily_contribs",
|
|
3046
|
+
action="store_true",
|
|
3047
|
+
default=None,
|
|
3048
|
+
help="Email when user's daily contributions count changes"
|
|
3049
|
+
)
|
|
2691
3050
|
notify.add_argument(
|
|
2692
3051
|
"-e", "--no-error-notify",
|
|
2693
3052
|
dest="notify_errors",
|
|
@@ -2766,6 +3125,13 @@ def main():
|
|
|
2766
3125
|
default=None,
|
|
2767
3126
|
help="Disable event monitoring"
|
|
2768
3127
|
)
|
|
3128
|
+
opts.add_argument(
|
|
3129
|
+
"-a", "--get-all-repos",
|
|
3130
|
+
dest="get_all_repos",
|
|
3131
|
+
action="store_true",
|
|
3132
|
+
default=None,
|
|
3133
|
+
help="Fetch all user repos (owned, forks, collaborations)"
|
|
3134
|
+
)
|
|
2769
3135
|
opts.add_argument(
|
|
2770
3136
|
"-b", "--csv-file",
|
|
2771
3137
|
dest="csv_file",
|
|
@@ -2780,6 +3146,13 @@ def main():
|
|
|
2780
3146
|
default=None,
|
|
2781
3147
|
help="Disable logging to github_monitor_<username>.log"
|
|
2782
3148
|
)
|
|
3149
|
+
opts.add_argument(
|
|
3150
|
+
"-m", "--track-contribs-changes",
|
|
3151
|
+
dest="track_contribs_changes",
|
|
3152
|
+
action="store_true",
|
|
3153
|
+
default=None,
|
|
3154
|
+
help="Track user's daily contributions count and log changes"
|
|
3155
|
+
)
|
|
2783
3156
|
|
|
2784
3157
|
args = parser.parse_args()
|
|
2785
3158
|
|
|
@@ -2883,6 +3256,9 @@ def main():
|
|
|
2883
3256
|
print("* Error: GITHUB_API_URL (-x / --github_url) value is empty")
|
|
2884
3257
|
sys.exit(1)
|
|
2885
3258
|
|
|
3259
|
+
if args.get_all_repos is True:
|
|
3260
|
+
GET_ALL_REPOS = True
|
|
3261
|
+
|
|
2886
3262
|
if args.list_followers_and_followings:
|
|
2887
3263
|
try:
|
|
2888
3264
|
github_print_followers_and_followings(args.username)
|
|
@@ -2966,12 +3342,18 @@ def main():
|
|
|
2966
3342
|
if args.notify_repo_update_date is True:
|
|
2967
3343
|
REPO_UPDATE_DATE_NOTIFICATION = True
|
|
2968
3344
|
|
|
3345
|
+
if args.notify_daily_contribs is True:
|
|
3346
|
+
CONTRIB_NOTIFICATION = True
|
|
3347
|
+
|
|
2969
3348
|
if args.notify_errors is False:
|
|
2970
3349
|
ERROR_NOTIFICATION = False
|
|
2971
3350
|
|
|
2972
3351
|
if args.track_repos_changes is True:
|
|
2973
3352
|
TRACK_REPOS_CHANGES = True
|
|
2974
3353
|
|
|
3354
|
+
if args.track_contribs_changes is True:
|
|
3355
|
+
TRACK_CONTRIB_CHANGES = True
|
|
3356
|
+
|
|
2975
3357
|
if args.no_monitor_events is True:
|
|
2976
3358
|
DO_NOT_MONITOR_GITHUB_EVENTS = True
|
|
2977
3359
|
|
|
@@ -2979,6 +3361,9 @@ def main():
|
|
|
2979
3361
|
REPO_NOTIFICATION = False
|
|
2980
3362
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
2981
3363
|
|
|
3364
|
+
if not TRACK_CONTRIB_CHANGES:
|
|
3365
|
+
CONTRIB_NOTIFICATION = False
|
|
3366
|
+
|
|
2982
3367
|
if DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
2983
3368
|
EVENT_NOTIFICATION = False
|
|
2984
3369
|
|
|
@@ -2987,13 +3372,16 @@ def main():
|
|
|
2987
3372
|
PROFILE_NOTIFICATION = False
|
|
2988
3373
|
REPO_NOTIFICATION = False
|
|
2989
3374
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
3375
|
+
CONTRIB_NOTIFICATION = False
|
|
2990
3376
|
ERROR_NOTIFICATION = False
|
|
2991
3377
|
|
|
2992
3378
|
print(f"* GitHub polling interval:\t[ {display_time(GITHUB_CHECK_INTERVAL)} ]")
|
|
2993
|
-
print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}] [new events = {EVENT_NOTIFICATION}]\n*\t\t\t\t[repos changes = {REPO_NOTIFICATION}] [repos update date = {REPO_UPDATE_DATE_NOTIFICATION}]\n*\t\t\t\t[errors = {ERROR_NOTIFICATION}]")
|
|
3379
|
+
print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}] [new events = {EVENT_NOTIFICATION}]\n*\t\t\t\t[repos changes = {REPO_NOTIFICATION}] [repos update date = {REPO_UPDATE_DATE_NOTIFICATION}]\n*\t\t\t\t[contrib changes = {CONTRIB_NOTIFICATION}] [errors = {ERROR_NOTIFICATION}]")
|
|
2994
3380
|
print(f"* GitHub API URL:\t\t{GITHUB_API_URL}")
|
|
2995
3381
|
print(f"* Track repos changes:\t\t{TRACK_REPOS_CHANGES}")
|
|
3382
|
+
print(f"* Track contrib changes:\t{TRACK_CONTRIB_CHANGES}")
|
|
2996
3383
|
print(f"* Monitor GitHub events:\t{not DO_NOT_MONITOR_GITHUB_EVENTS}")
|
|
3384
|
+
print(f"* Get owned repos only:\t\t{not GET_ALL_REPOS}")
|
|
2997
3385
|
print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
|
|
2998
3386
|
print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
|
|
2999
3387
|
print(f"* Output logging enabled:\t{not DISABLE_LOGGING}" + (f" ({FINAL_LOG_PATH})" if not DISABLE_LOGGING else ""))
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "github_monitor"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2"
|
|
8
8
|
description = "Tool implementing real-time tracking of Github users activities including profile and repositories changes"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|