From c325db8cd077943ecab539909d46aa5767329880 Mon Sep 17 00:00:00 2001 From: Lukas Winkler Date: Sat, 2 Feb 2019 22:12:08 +0100 Subject: [PATCH] first kind of working version --- .gitignore | 127 +++++++++++++++++++++++++++++++++++ generator/__init__.py | 0 generator/api.py | 53 +++++++++++++++ generator/cli.py | 5 ++ generator/config.py | 46 +++++++++++++ generator/defaultconfig.yaml | 27 ++++++++ generator/generator.py | 32 +++++++++ generator/issue.py | 40 +++++++++++ generator/main.py | 0 requirements.txt | 6 ++ setup.py | 14 ++++ 11 files changed, 350 insertions(+) create mode 100644 .gitignore create mode 100644 generator/__init__.py create mode 100644 generator/api.py create mode 100644 generator/cli.py create mode 100644 generator/config.py create mode 100644 generator/defaultconfig.yaml create mode 100644 generator/generator.py create mode 100644 generator/issue.py create mode 100644 generator/main.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfa487b --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Python Patch ### +.venv/ + +# End of https://www.gitignore.io/api/python +github-changelog-generator.yaml +.idea/ +*.js \ No newline at end of file diff --git a/generator/__init__.py b/generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/generator/api.py b/generator/api.py new file mode 100644 index 0000000..c3abe92 --- /dev/null +++ b/generator/api.py @@ -0,0 +1,53 @@ +from datetime import date +from urllib.parse import urlencode +from warnings import warn + +import requests + +from generator.issue import Issue + + +class GithubAPI: + BASE_URL = "https://api.github.com" + + def __init__(self, token=None): + self.s = requests.Session() + if token: + self.s.headers.update({'Authorization': 'token {}'.format(token)}) + else: + warn("use Token!", stacklevel=2) # TODO + + def call(self, url, parameters=None): + if "//" not in url: + url = self.BASE_URL + url + print(url + "?" + urlencode(parameters)) + r = self.s.get(url, params=parameters) + if isinstance(r.json(), dict): + yield r.json() + else: + yield from r.json() + while "next" in r.links: + print("fetching next page") + r = self.s.get(r.links["next"]["url"]) + yield from r.json() + + def fetch_issues_since(self, repo, since: date): + assert "/" in repo # e.g. "matomo-org/matomo" + path = "/repos/{}/issues".format(repo) + params = { + "state": "closed", + "direction": "asc", + "since": since.isoformat() + } + responses = self.call(path, params) + for response in responses: + yield Issue(response) + + def fetch_pr_details(self, pr: Issue): + print(pr.pr_url) + data = list(self.call(pr.pr_url))[ + 0] # self.call is a generator even if there is only one result + return data + + def fetch_events(self): + pass diff --git a/generator/cli.py b/generator/cli.py new file mode 100644 index 0000000..e83673b --- /dev/null +++ b/generator/cli.py @@ -0,0 +1,5 @@ +from generator.generator import generate_changelog + + +def main(): + generate_changelog() diff --git a/generator/config.py b/generator/config.py new file mode 100644 index 0000000..71fb031 --- /dev/null +++ b/generator/config.py @@ -0,0 +1,46 @@ +import os.path + +import pkg_resources +import yaml + + +class Config: + config_paths = ["./github-changelog-generator.yaml", "~/.config/github-changelog-generator.yaml"] + + def __init__(self): + with open(self.get_config_path(), 'r') as stream: + config = yaml.safe_load(stream) + self.api_token = config["api_token"] # type:str + self.labels_to_ignore = set(config["labels_to_ignore"]) + self.sort_by_labels = config["sort_by_labels"] + self.is_matomo = config["is_matomo"] # type:bool + if self.is_matomo: + self.compare_config() + + def get_config_path(self): + for path in self.config_paths: + if os.path.isfile(path): + return path + + def compare_config(self): + used_config = self.__dict__ + default_config_file = pkg_resources.resource_filename('generator', 'defaultconfig.yaml') + + with open(default_config_file, 'r') as stream: + default_config = yaml.safe_load(stream) + default_config["labels_to_ignore"] = set(default_config["labels_to_ignore"]) + + for key, value in default_config.items(): + if key in ["api_token"]: + continue + elif key not in used_config: + print("Key {} is missing from user config".format(key)) + elif value != used_config[key]: + print("{} differs from recommended config".format(key)) + print("default config:") + print(value) + print("own config:") + print(used_config[key]) + + +config = Config() diff --git a/generator/defaultconfig.yaml b/generator/defaultconfig.yaml new file mode 100644 index 0000000..2a13087 --- /dev/null +++ b/generator/defaultconfig.yaml @@ -0,0 +1,27 @@ +api_token: null +labels_to_ignore: + - wontfix + - not-in-changelog + - invalid + - duplicate + - answered + - worksforme +sort_by_labels: + - Critical + - Major + - 'c: New plugin' + - 'c: Security' + - Enhancement + - 'c: Performance' + - Regression + - 'c: Design / UI' + - 'c: Usability' + - 'c: Accessibility' + - Task + - 'c: Platform' + - Bug + - 'c: Website matomo.org' + - 'c: i18n' + - RFC + +is_matomo: True \ No newline at end of file diff --git a/generator/generator.py b/generator/generator.py new file mode 100644 index 0000000..6073026 --- /dev/null +++ b/generator/generator.py @@ -0,0 +1,32 @@ +from datetime import timedelta, datetime + +from generator.api import GithubAPI +from generator.config import config +from generator.issue import Issue + +api = GithubAPI(token=config.api_token) + + +def getissueorder(issue: Issue): + order = 99 + for label in issue.labels: + if label in config.sort_by_labels: + order = config.sort_by_labels.index(label) + return order, issue.number + + +def generate_changelog(): + since = datetime.today() - timedelta(3) + issues = api.fetch_issues_since("matomo-org/matomo", since) + issues = list(issues) # enumerate iterable + for issue in issues: + if issue.pull_request: + print("PR") + issue.add_pr_data(api.fetch_pr_details(issue)) + issue.compare_close_date(since) + issues = [i for i in issues if i.should_be_included] # remove all filtered issues + for issue in issues: + issue.add_events_data(api.fetch_events()) + issues.sort(key=getissueorder) + for i in issues: + print("#{}: {} ({})".format(i.number, i.title, ", ".join(i.labels))) diff --git a/generator/issue.py b/generator/issue.py new file mode 100644 index 0000000..939ef0b --- /dev/null +++ b/generator/issue.py @@ -0,0 +1,40 @@ +from datetime import date, datetime +from typing import List + +from generator.config import config + + +class Issue(): + closed_before_since = None + + def __init__(self, api): + self.number = api["number"] # type:int + self.title = api["title"] # type:str + self.url = api["url"] # type:str + self.labels = [l["name"] for l in api["labels"]] # type:List[str] + self.closed_at = datetime.strptime(api["closed_at"], "%Y-%m-%dT%H:%M:%SZ") + self.pull_request = "pull_request" in api + if self.pull_request: + self.pr_url = api["pull_request"]["url"] + + def add_pr_data(self, api): + print(api) + pass + + def add_events_data(self, api): + pass + + def compare_close_date(self, since: date): + self.closed_before_since = self.closed_at < since + + @property + def has_ignored_label(self) -> bool: + return not config.labels_to_ignore.isdisjoint(self.labels) + + @property + def should_be_included(self) -> bool: + return not self.has_ignored_label and not self.closed_before_since + + @property + def pull_request_closed(self) -> bool: + return self.pull_request and False # TODO: add merged status from details diff --git a/generator/main.py b/generator/main.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..036e579 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +certifi==2018.11.29 +chardet==3.0.4 +idna==2.8 +pkg-resources==0.0.0 +requests==2.21.0 +urllib3==1.24.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c13a85d --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +from distutils.core import setup + +import setuptools + +setup( + name='github-changelog-generator', + version='0.0.1', + packages=setuptools.find_packages(), + entry_points = { + 'console_scripts': ['github-changelog-generator=generator.cli:main'], + } + # license='Creative Commons Attribution-Noncommercial-Share Alike license', + # long_description=open('README.md').read(), +)