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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github_monitor
3
- Version: 2.0
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
- github_monitor is a tool for real-time monitoring of GitHub users' activities, including profile and repository changes.
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
- github_monitor is a tool for real-time monitoring of GitHub users' activities, including profile and repository changes.
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.0
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
- github_monitor is a tool for real-time monitoring of GitHub users' activities, including profile and repository changes.
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.0
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.0"
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
- stargazers_list = [star.login for star in repo.get_stargazers()]
925
- subscribers_list = [subscriber.login for subscriber in repo.get_subscribers()]
926
- forked_repos = [fork.full_name for fork in repo.get_forks()]
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"* Error while processing info for repo '{repo.name}', skipping for now: {e}")
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
- repos_count = g_user.public_repos
966
- repos_list = g_user.get_repos()
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
- print(f" - 🌐 URL:\t\t{repo.html_url}")
995
- print(f" - 💻 Language:\t\t{repo.language}")
996
-
997
- print(f"\n - ⭐ Stars:\t\t{repo.stargazers_count}")
998
- print(f" - 🍴 Forks:\t\t{repo.forks_count}")
999
- print(f" - 👓 Watchers:\t\t{repo.subscribers_count}")
1000
-
1001
- # print(f" - 🐞 Issues+PRs:\t{repo.open_issues_count}")
1002
- print(f" - 🐞 Issues:\t\t{issue_count}")
1003
- print(f" - 📬 PRs:\t\t{pr_count}")
1004
-
1005
- print(f"\n - 📝 License:\t\t{repo.license.name if repo.license else 'None'}")
1006
- print(f" - 🌿 Branch (default):\t{repo.default_branch}")
1007
-
1008
- print(f"\n - 📅 Created:\t\t{get_date_from_ts(repo.created_at)} ({calculate_timespan(int(time.time()), repo.created_at, granularity=2)} ago)")
1009
- print(f" - 🔄 Updated:\t\t{get_date_from_ts(repo.updated_at)} ({calculate_timespan(int(time.time()), repo.updated_at, granularity=2)} ago)")
1010
- print(f" - 🔃 Last push:\t{get_date_from_ts(repo.pushed_at)} ({calculate_timespan(int(time.time()), repo.pushed_at, granularity=2)} ago)")
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
- desc = repo.description or ''
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
- repos_list = g_user.get_repos()
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
- repos_raw = list(gh_call(g_user.get_repos)())
2168
- repos_count = gh_call(lambda: g_user.public_repos)()
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
- repos_list = gh_call(g_user.get_repos)()
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.0"
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