update-development-status.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. """
  2. This script updates games development status
  3. To run, install from pip:
  4. - pygithub
  5. - python-gitlab
  6. Add environment variables:
  7. - GH_TOKEN
  8. - https://github.com/settings/tokens?type=beta
  9. - (see https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token#creating-a-token)
  10. - GL_TOKEN
  11. - https://gitlab.com/-/user_settings/personal_access_tokens
  12. - (see https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token)
  13. - With read_api scope
  14. """
  15. import os
  16. import re
  17. from typing import Optional
  18. import yaml
  19. import feedparser
  20. from pathlib import Path
  21. from datetime import datetime, timedelta
  22. from github import Github, GithubException
  23. from gitlab import Gitlab
  24. from time import mktime
  25. GL_HOST = 'https://gitlab.com'
  26. SF_HOST = 'https://sourceforge.net'
  27. GH_REGEX = re.compile(r'https://github.com/([^/]+)/([^/]+)')
  28. GL_REGEX = re.compile(GL_HOST + r'/([^/]+/[^/]+)')
  29. SF_REGEX = re.compile(SF_HOST + r"/projects/([^/]+)")
  30. GH_DT_FMT = "%a, %d %b %Y %H:%M:%S %Z"
  31. def main():
  32. gh = Github(os.environ["GH_TOKEN"])
  33. gl = Gitlab(GL_HOST, private_token=os.environ["GL_TOKEN"])
  34. for filename in Path('games').iterdir():
  35. if not filename.is_file() or filename.suffix != '.yaml':
  36. continue
  37. games = yaml.safe_load(open(filename, encoding='utf-8'))
  38. for game in games:
  39. if 'added' not in game:
  40. print(f"{game['name']} has no added field")
  41. continue
  42. repo_url = game.get('repo', '')
  43. if len(repo_url) == 0 or game.get('development', '') == 'complete':
  44. continue
  45. if (latest_commit_date := get_latest_commit_date(repo_url, gh, gl)) is None:
  46. continue
  47. diff = datetime.now() - latest_commit_date
  48. status_original = game.get("development", "unknown")
  49. if diff < timedelta(weeks=1):
  50. game['development'] = 'very active'
  51. elif diff < timedelta(weeks=4):
  52. game['development'] = 'active'
  53. elif diff < timedelta(days=365):
  54. game['development'] = 'sporadic'
  55. else:
  56. game['development'] = 'halted'
  57. if status_original != game["development"]:
  58. print(f"{game['name']} status should be {game['development']} ({status_original=})")
  59. # yaml.dump(games, open(filename, 'w', encoding='utf-8'), sort_keys=False)
  60. # print(filename, 'has been updated')
  61. def is_github_repo(repo):
  62. return repo.startswith('https://github.')
  63. def is_gitlab_repo(repo):
  64. return repo.startswith(GL_HOST)
  65. def is_sourceforge_repo(repo):
  66. return repo.startswith(SF_HOST)
  67. def get_latest_commit_date(repo_url, gh, gl):
  68. if is_github_repo(repo_url):
  69. return get_latest_commit_date_for_github(gh, repo_url)
  70. elif is_gitlab_repo(repo_url):
  71. return get_latest_commit_date_for_gitlab(gl, repo_url)
  72. elif is_sourceforge_repo(repo_url):
  73. return get_latest_commit_date_for_sourceforge(repo_url)
  74. print('The', repo_url, 'repository could not be updated')
  75. def get_latest_commit_date_for_github(gh, repo_url):
  76. match = re.match(GH_REGEX, repo_url)
  77. if not match:
  78. return
  79. owner, repo = match.groups()
  80. try:
  81. gh_repo = gh.get_repo(f"{owner}/{repo}")
  82. branches = list(gh_repo.get_branches())
  83. commit_dates = {datetime.strptime(branch.commit.last_modified, GH_DT_FMT) for branch in branches if branch.commit.last_modified}
  84. except GithubException as e:
  85. print(f'Error getting repo info for {owner}/{repo}: {e}')
  86. return
  87. return max(commit_dates) if commit_dates else None
  88. def get_latest_commit_date_for_gitlab(gl, repo_url):
  89. match = re.match(GL_REGEX, repo_url)
  90. if not match:
  91. return
  92. project_namespace = match.groups()[0]
  93. project = gl.projects.get(project_namespace)
  94. branches = project.branches.list(get_all=True)
  95. created_dates = {branch.commit["created_at"] for branch in branches}
  96. last_commit = max(created_dates)
  97. return datetime.strptime(
  98. ''.join(last_commit.rsplit(':', 1)),
  99. '%Y-%m-%dT%H:%M:%S.%f%z'
  100. ).replace(tzinfo=None)
  101. def get_latest_commit_date_for_sourceforge(repo_url: str) -> Optional[datetime]:
  102. if not (match := re.match(SF_REGEX, repo_url)):
  103. return
  104. project_name = match.groups()[0]
  105. feed = feedparser.parse(f"https://sourceforge.net/p/{project_name}/activity/feed.rss")
  106. for entry in feed.entries:
  107. # Only look for commits
  108. if " committed " not in entry["title"]:
  109. continue
  110. return datetime.fromtimestamp(mktime(entry["published_parsed"]))
  111. return None
  112. if __name__ == "__main__":
  113. main()