From 9582b9e11c60b8cf95d02b70e43302c6bfc52a64 Mon Sep 17 00:00:00 2001 From: Wei Kang Date: Sun, 26 Aug 2018 16:51:05 -0700 Subject: [PATCH] add changelog for v2.0.0 --- CHANGELOG.txt => CHANGELOG.md | 41 ++ tools/gitcount.ipynb | 773 ++++++++++++++++++++++++++++++++++ tools/github_stats.py | 198 --------- 3 files changed, 814 insertions(+), 198 deletions(-) rename CHANGELOG.txt => CHANGELOG.md (72%) create mode 100644 tools/gitcount.ipynb delete mode 100644 tools/github_stats.py diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 72% rename from CHANGELOG.txt rename to CHANGELOG.md index fa2952c..5c6be56 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +Version 2.0.0 (2018-08-26) + +This release does not add any new functionality to `giddy`, but +instead features api changes in `giddy` and its +pysal submodule dependencies `mapclassify`, `libpysal`, and `esda`. More +specifically, the `giddy.api` module which was originally designed to + facilitate a smoother transition from the old metapackage pysal to the + refactored submodule structure (see [here](http://pysal.org/about.html#migrating-to-pysal-2-0) + for details) was removed as we are moving away from the +refactoring stage and looking at the future development. + +We closed a total of 20 issues (enhancements and bug fixes) through 9 pull requests, since our last release on 2018-07-27. + +## Issues Closed + - remove giddy.api in README.rst (#66) + - chore: update for libpysal lower case module name changes (#65) + - remove api.py (#62) + - set up travis dual testing against mapclassify and esda (#63) + - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61) + - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64) + - version giddy only in giddy/__ini__.py (#60) + - remove duplicate makefile for sphinx build (#59) + - add zenodo doi badge to README (#58) + - add changelog for the release 1.2.0 (#57) + - prepare for release 1.2.0 (#56) + +## Pull Requests + - remove giddy.api in README.rst (#66) + - chore: update for libpysal lower case module name changes (#65) + - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61) + - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64) + - version giddy only in giddy/__ini__.py (#60) + - remove duplicate makefile for sphinx build (#59) + - add zenodo doi badge to README (#58) + - add changelog for the release 1.2.0 (#57) + - prepare for release 1.2.0 (#56) + +The following individuals contributed to this release: + + - Wei Kang + - Stefanie Lumnitz v<1.2.0>, 2018-07-27 diff --git a/tools/gitcount.ipynb b/tools/gitcount.ipynb new file mode 100644 index 0000000..9a54329 --- /dev/null +++ b/tools/gitcount.ipynb @@ -0,0 +1,773 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PySAL Change Log Statistics\n", + "\n", + "This notebook generates the summary statistics for a package. \n", + "\n", + "It assumes you are running this under the `tools` directory at the toplevel of the package\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Change the values only in the next cell" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "package_name = 'giddy'\n", + "release_date = '2018-08-26'\n", + "start_date = '2018-07-27'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook will generate a file in the current directory with the name \"changelog_VERSION.md\". You can edit and append this on front of the CHANGELOG file for the package release." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import print_function\n", + "import os\n", + "import json\n", + "import re\n", + "import sys\n", + "import pandas\n", + "\n", + "from datetime import datetime, timedelta\n", + "from time import sleep\n", + "from subprocess import check_output\n", + "try:\n", + " from urllib import urlopen\n", + "except:\n", + " from urllib.request import urlopen\n", + "\n", + "import ssl\n", + "import yaml\n", + "\n", + "context = ssl._create_unverified_context()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "CWD = os.path.abspath(os.path.curdir)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'/Users/weikang/Google Drive (weikang@ucr.edu)/python_repos/pysal-refactor/giddy/tools'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "CWD" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime.datetime(2018, 7, 27, 0, 0)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "since_date = '--since=\"{start}\"'.format(start=start_date)\n", + "since_date\n", + "since = datetime.strptime(start_date+\" 0:0:0\", \"%Y-%m-%d %H:%M:%S\")\n", + "since" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# get __version__\n", + "f = \"../{package}/__init__.py\".format(package=package_name)\n", + "\n", + "with open(f, 'r') as initfile:\n", + " exec(initfile.readline())\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Total commits by subpackage" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "cmd = ['git', 'log', '--oneline', since_date]\n", + "ncommits = len(check_output(cmd).splitlines())" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "14" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ncommits" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List Contributors" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some of our contributors have many aliases for the same identity. So, we've added a mapping to make sure that individuals are listed once (and only once). " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "identities = {'Levi John Wolf': ('ljwolf', 'Levi John Wolf'),\n", + " 'Serge Rey': ('Serge Rey', 'Sergio Rey', 'sjsrey', 'serge'),\n", + " 'Wei Kang': ('Wei Kang', 'weikang9009'),\n", + " 'Dani Arribas-Bel': ('Dani Arribas-Bel', 'darribas')\n", + "}\n", + "\n", + "def regularize_identity(string):\n", + " string = string.decode()\n", + " for name, aliases in identities.items():\n", + " for alias in aliases:\n", + " if alias in string:\n", + " string = string.replace(alias, name)\n", + " if len(string.split(' '))>1:\n", + " string = string.title()\n", + " return string.lstrip('* ')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "author_cmd = ['git', 'log', '--format=* %aN', since_date]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import Counter" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "ncommits = len(check_output(cmd).splitlines())\n", + "all_authors = check_output(author_cmd).splitlines()\n", + "counter = Counter([regularize_identity(author) for author in all_authors])\n", + "# global_counter += counter\n", + "# counters.update({'.'.join((package,subpackage)): counter})\n", + "unique_authors = sorted(set(all_authors))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "unique_authors = counter.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['Wei Kang', 'Stefanie Lumnitz'])" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unique_authors" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Disaggregate by PR, Issue" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "ISO8601 = \"%Y-%m-%dT%H:%M:%SZ\"\n", + "PER_PAGE = 100\n", + "element_pat = re.compile(r'<(.+?)>')\n", + "rel_pat = re.compile(r'rel=[\\'\"](\\w+)[\\'\"]')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def parse_link_header(headers):\n", + " link_s = headers.get('link', '')\n", + " urls = element_pat.findall(link_s)\n", + " rels = rel_pat.findall(link_s)\n", + " d = {}\n", + " for rel,url in zip(rels, urls):\n", + " d[rel] = url\n", + " return d\n", + "\n", + "def get_paged_request(url):\n", + " \"\"\"get a full list, handling APIv3's paging\"\"\"\n", + " results = []\n", + " while url:\n", + " #print(\"fetching %s\" % url, file=sys.stderr)\n", + " f = urlopen(url)\n", + " results.extend(json.load(f))\n", + " links = parse_link_header(f.headers)\n", + " url = links.get('next')\n", + " return results\n", + "\n", + "def get_issues(project=\"pysal/giddy\", state=\"closed\", pulls=False):\n", + " \"\"\"Get a list of the issues from the Github API.\"\"\"\n", + " which = 'pulls' if pulls else 'issues'\n", + " url = \"https://api.github.com/repos/%s/%s?state=%s&per_page=%i\" % (project, which, state, PER_PAGE)\n", + " return get_paged_request(url)\n", + "\n", + "\n", + "def _parse_datetime(s):\n", + " \"\"\"Parse dates in the format returned by the Github API.\"\"\"\n", + " if s:\n", + " return datetime.strptime(s, ISO8601)\n", + " else:\n", + " return datetime.fromtimestamp(0)\n", + "\n", + "\n", + "def issues2dict(issues):\n", + " \"\"\"Convert a list of issues to a dict, keyed by issue number.\"\"\"\n", + " idict = {}\n", + " for i in issues:\n", + " idict[i['number']] = i\n", + " return idict\n", + "\n", + "\n", + "def is_pull_request(issue):\n", + " \"\"\"Return True if the given issue is a pull request.\"\"\"\n", + " return 'pull_request_url' in issue\n", + "\n", + "\n", + "def issues_closed_since(period=timedelta(days=365), project=\"pysal/pysal\", pulls=False):\n", + " \"\"\"Get all issues closed since a particular point in time. period\n", + "can either be a datetime object, or a timedelta object. In the\n", + "latter case, it is used as a time before the present.\"\"\"\n", + "\n", + " which = 'pulls' if pulls else 'issues'\n", + "\n", + " if isinstance(period, timedelta):\n", + " period = datetime.now() - period\n", + " url = \"https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i\" % (project, which, period.strftime(ISO8601), PER_PAGE)\n", + " allclosed = get_paged_request(url)\n", + " # allclosed = get_issues(project=project, state='closed', pulls=pulls, since=period)\n", + " filtered = [i for i in allclosed if _parse_datetime(i['closed_at']) > period]\n", + "\n", + " # exclude rejected PRs\n", + " if pulls:\n", + " filtered = [ pr for pr in filtered if pr['merged_at'] ]\n", + "\n", + " return filtered\n", + "\n", + "\n", + "def sorted_by_field(issues, field='closed_at', reverse=False):\n", + " \"\"\"Return a list of issues sorted by closing date date.\"\"\"\n", + " return sorted(issues, key = lambda i:i[field], reverse=reverse)\n", + "\n", + "\n", + "def report(issues, show_urls=False):\n", + " \"\"\"Summary report about a list of issues, printing number and title.\n", + " \"\"\"\n", + " # titles may have unicode in them, so we must encode everything below\n", + " if show_urls:\n", + " for i in issues:\n", + " role = 'ghpull' if 'merged_at' in i else 'ghissue'\n", + " print('* :%s:`%d`: %s' % (role, i['number'],\n", + " i['title'].encode('utf-8')))\n", + " else:\n", + " for i in issues:\n", + " print('* %d: %s' % (i['number'], i['title'].encode('utf-8')))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "all_issues = {}\n", + "all_pulls = {}\n", + "total_commits = 0\n", + "#prj='pysal/libpysal'\n", + "prj = 'pysal/{package}'.format(package=package_name)\n", + "issues = issues_closed_since(since, project=prj,pulls=False)\n", + "pulls = issues_closed_since(since, project=prj,pulls=True)\n", + "issues = sorted_by_field(issues, reverse=True)\n", + "pulls = sorted_by_field(pulls, reverse=True)\n", + "n_issues, n_pulls = map(len, (issues, pulls))\n", + "n_total = n_issues + n_pulls\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "issue_listing = []\n", + "for issue in issues:\n", + " entry = \"{title} (#{number})\".format(title=issue['title'],number=issue['number'])\n", + " issue_listing.append(entry)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "pull_listing = []\n", + "for pull in pulls:\n", + " entry = \"{title} (#{number})\".format(title=pull['title'],number=pull['number'])\n", + " pull_listing.append(entry)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['remove giddy.api in README.rst (#66)',\n", + " ' chore: update for libpysal lower case module name changes (#65)',\n", + " 'replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)',\n", + " 'Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)',\n", + " 'version giddy only in giddy/__ini__.py (#60)',\n", + " 'remove duplicate makefile for sphinx build (#59)',\n", + " 'add zenodo doi badge to README (#58)',\n", + " 'add changelog for the release 1.2.0 (#57)',\n", + " 'prepare for release 1.2.0 (#56)']" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pull_listing" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "message = \"We closed a total of {total} issues (enhancements and bug fixes) through {pr} pull requests\".format(total=n_total, pr=n_pulls)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "message = \"{msg}, since our last release on {previous}.\".format(msg=message, previous=str(start_date))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'We closed a total of 20 issues (enhancements and bug fixes) through 9 pull requests, since our last release on 2018-07-27.'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "message" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "message += \"\\n\\n## Issues Closed\\n\"" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We closed a total of 20 issues (enhancements and bug fixes) through 9 pull requests, since our last release on 2018-07-27.\n", + "\n", + "## Issues Closed\n", + "\n" + ] + } + ], + "source": [ + "print(message)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "issues = \"\\n\".join([\" - \"+issue for issue in issue_listing])\n", + "message += issues\n", + "message += \"\\n\\n## Pull Requests\\n\"\n", + "pulls = \"\\n\".join([\" - \"+pull for pull in pull_listing])\n", + "message += pulls" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We closed a total of 20 issues (enhancements and bug fixes) through 9 pull requests, since our last release on 2018-07-27.\n", + "\n", + "## Issues Closed\n", + " - remove giddy.api in README.rst (#66)\n", + " - chore: update for libpysal lower case module name changes (#65)\n", + " - remove api.py (#62)\n", + " - set up travis dual testing against mapclassify and esda (#63)\n", + " - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)\n", + " - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)\n", + " - version giddy only in giddy/__ini__.py (#60)\n", + " - remove duplicate makefile for sphinx build (#59)\n", + " - add zenodo doi badge to README (#58)\n", + " - add changelog for the release 1.2.0 (#57)\n", + " - prepare for release 1.2.0 (#56)\n", + "\n", + "## Pull Requests\n", + " - remove giddy.api in README.rst (#66)\n", + " - chore: update for libpysal lower case module name changes (#65)\n", + " - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)\n", + " - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)\n", + " - version giddy only in giddy/__ini__.py (#60)\n", + " - remove duplicate makefile for sphinx build (#59)\n", + " - add zenodo doi badge to README (#58)\n", + " - add changelog for the release 1.2.0 (#57)\n", + " - prepare for release 1.2.0 (#56)\n" + ] + } + ], + "source": [ + "print(message)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "people = \"\\n\".join([\" - \"+person for person in unique_authors])" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " - Wei Kang\n", + " - Stefanie Lumnitz\n" + ] + } + ], + "source": [ + "print(people)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "message +=\"\\n\\nThe following individuals contributed to this release:\\n\\n{people}\".format(people=people)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We closed a total of 20 issues (enhancements and bug fixes) through 9 pull requests, since our last release on 2018-07-27.\n", + "\n", + "## Issues Closed\n", + " - remove giddy.api in README.rst (#66)\n", + " - chore: update for libpysal lower case module name changes (#65)\n", + " - remove api.py (#62)\n", + " - set up travis dual testing against mapclassify and esda (#63)\n", + " - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)\n", + " - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)\n", + " - version giddy only in giddy/__ini__.py (#60)\n", + " - remove duplicate makefile for sphinx build (#59)\n", + " - add zenodo doi badge to README (#58)\n", + " - add changelog for the release 1.2.0 (#57)\n", + " - prepare for release 1.2.0 (#56)\n", + "\n", + "## Pull Requests\n", + " - remove giddy.api in README.rst (#66)\n", + " - chore: update for libpysal lower case module name changes (#65)\n", + " - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)\n", + " - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)\n", + " - version giddy only in giddy/__ini__.py (#60)\n", + " - remove duplicate makefile for sphinx build (#59)\n", + " - add zenodo doi badge to README (#58)\n", + " - add changelog for the release 1.2.0 (#57)\n", + " - prepare for release 1.2.0 (#56)\n", + "\n", + "The following individuals contributed to this release:\n", + "\n", + " - Wei Kang\n", + " - Stefanie Lumnitz\n" + ] + } + ], + "source": [ + "print(message)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "head = \"# Changes\\n\\nVersion {version} ({release_date})\\n\\n\".format(version=__version__, release_date=release_date)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Changes\n", + "\n", + "Version 2.0.0 (2018-08-26)\n", + "\n", + "We closed a total of 20 issues (enhancements and bug fixes) through 9 pull requests, since our last release on 2018-07-27.\n", + "\n", + "## Issues Closed\n", + " - remove giddy.api in README.rst (#66)\n", + " - chore: update for libpysal lower case module name changes (#65)\n", + " - remove api.py (#62)\n", + " - set up travis dual testing against mapclassify and esda (#63)\n", + " - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)\n", + " - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)\n", + " - version giddy only in giddy/__ini__.py (#60)\n", + " - remove duplicate makefile for sphinx build (#59)\n", + " - add zenodo doi badge to README (#58)\n", + " - add changelog for the release 1.2.0 (#57)\n", + " - prepare for release 1.2.0 (#56)\n", + "\n", + "## Pull Requests\n", + " - remove giddy.api in README.rst (#66)\n", + " - chore: update for libpysal lower case module name changes (#65)\n", + " - replace `libpysal.api` imports with new imports in `markov.py` and `d… (#61)\n", + " - Remove api.py and account for changes in (incoming) API of mapclassify, esda, and libpysal (#64)\n", + " - version giddy only in giddy/__ini__.py (#60)\n", + " - remove duplicate makefile for sphinx build (#59)\n", + " - add zenodo doi badge to README (#58)\n", + " - add changelog for the release 1.2.0 (#57)\n", + " - prepare for release 1.2.0 (#56)\n", + "\n", + "The following individuals contributed to this release:\n", + "\n", + " - Wei Kang\n", + " - Stefanie Lumnitz\n" + ] + } + ], + "source": [ + "print(head+message)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "outfile = 'changelog_{version}.md'.format(version=__version__)\n", + "with open(outfile, 'w') as of:\n", + " of.write(head+message)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/github_stats.py b/tools/github_stats.py deleted file mode 100644 index b57bd24..0000000 --- a/tools/github_stats.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python -"""Simple tools to query github.com and gather stats about issues. - -Adapted from: https://github.com/ipython/ipython/blob/master/tools/github_stats.py -""" -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - -from __future__ import print_function - -import json -import re -import sys - -from datetime import datetime, timedelta -from subprocess import check_output -from urllib.request import urlopen -import ssl - -context = ssl._create_unverified_context() - -#----------------------------------------------------------------------------- -# Globals -#----------------------------------------------------------------------------- - -ISO8601 = "%Y-%m-%dT%H:%M:%SZ" -PER_PAGE = 100 - -element_pat = re.compile(r'<(.+?)>') -rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]') - -#----------------------------------------------------------------------------- -# Functions -#----------------------------------------------------------------------------- - -def parse_link_header(headers): - link_s = headers.get('link', '') - urls = element_pat.findall(link_s) - rels = rel_pat.findall(link_s) - d = {} - for rel,url in zip(rels, urls): - d[rel] = url - return d - -def get_paged_request(url): - """get a full list, handling APIv3's paging""" - results = [] - while url: - print("fetching %s" % url, file=sys.stderr) - f = urlopen(url, context=context) - results.extend(json.load(f)) - links = parse_link_header(f.headers) - url = links.get('next') - return results - -def get_issues(project="pysal/giddy", state="closed", pulls=False): - """Get a list of the issues from the Github API.""" - which = 'pulls' if pulls else 'issues' - url = "https://api.github.com/repos/%s/%s?state=%s&per_page=%i" % (project, which, state, PER_PAGE) - return get_paged_request(url) - - -def _parse_datetime(s): - """Parse dates in the format returned by the Github API.""" - if s: - return datetime.strptime(s, ISO8601) - else: - return datetime.fromtimestamp(0) - - -def issues2dict(issues): - """Convert a list of issues to a dict, keyed by issue number.""" - idict = {} - for i in issues: - idict[i['number']] = i - return idict - - -def is_pull_request(issue): - """Return True if the given issue is a pull request.""" - return 'pull_request_url' in issue - - -def issues_closed_since(period=timedelta(days=365), project="pysal/giddy", pulls=False): - """Get all issues closed since a particular point in time. period -can either be a datetime object, or a timedelta object. In the -latter case, it is used as a time before the present.""" - - which = 'pulls' if pulls else 'issues' - - if isinstance(period, timedelta): - period = datetime.now() - period - url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, period.strftime(ISO8601), PER_PAGE) - allclosed = get_paged_request(url) - # allclosed = get_issues(project=project, state='closed', pulls=pulls, since=period) - filtered = [i for i in allclosed if _parse_datetime(i['closed_at']) > period] - - # exclude rejected PRs - if pulls: - filtered = [ pr for pr in filtered if pr['merged_at'] ] - - return filtered - - -def sorted_by_field(issues, field='closed_at', reverse=False): - """Return a list of issues sorted by closing date date.""" - return sorted(issues, key = lambda i:i[field], reverse=reverse) - - -def report(issues, show_urls=False): - """Summary report about a list of issues, printing number and title. - """ - # titles may have unicode in them, so we must encode everything below - if show_urls: - for i in issues: - role = 'ghpull' if 'merged_at' in i else 'ghissue' - print('* :%s:`%d`: %s' % (role, i['number'], - i['title'].encode('utf-8'))) - else: - for i in issues: - print('* %d: %s' % (i['number'], i['title'].encode('utf-8'))) - -#----------------------------------------------------------------------------- -# Main script -#----------------------------------------------------------------------------- - -if __name__ == "__main__": - # Whether to add reST urls for all issues in printout. - show_urls = True - - # By default, search one month back - tag = None - if len(sys.argv) > 1: - try: - days = int(sys.argv[1]) - except: - tag = sys.argv[1] - else: - tag = check_output(['git', 'describe', '--abbrev=0']).strip() - - if tag: - cmd = ['git', 'log', '-1', '--format=%ai', tag] - tagday, tz = check_output(cmd).strip().rsplit(' ', 1) - since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S") - else: - since = datetime.now() - timedelta(days=days) - - print("fetching GitHub stats since %s (tag: %s)" % (since, tag), file=sys.stderr) - # turn off to play interactively without redownloading, use %run -i - if 1: - issues = issues_closed_since(since, pulls=False) - pulls = issues_closed_since(since, pulls=True) - - # For regular reports, it's nice to show them in reverse chronological order - issues = sorted_by_field(issues, reverse=True) - pulls = sorted_by_field(pulls, reverse=True) - - n_issues, n_pulls = map(len, (issues, pulls)) - n_total = n_issues + n_pulls - - # Print summary report we can directly include into release notes. - print() - since_day = since.strftime("%Y/%m/%d") - today = datetime.today().strftime("%Y/%m/%d") - print(".. _github-stats:") - print('Github stats') - print('============') - print() - print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag)) - print() - print("These lists are automatically generated, and may be incomplete or contain duplicates.") - print() - if tag: - # print git info, in addition to GitHub info: - since_tag = tag+'..' - cmd = ['git', 'log', '--oneline', since_tag] - ncommits = len(check_output(cmd).splitlines()) - - author_cmd = ['git', 'log', '--format=* %aN', since_tag] - all_authors = check_output(author_cmd).splitlines() - unique_authors = sorted(set(all_authors)) - - print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits)) - print() - print('\n'.join(unique_authors)) - print() - - print() - print("We closed a total of %d issues, %d pull requests and %d regular issues;\n" - "this is the full list (generated with the script \n" - ":file:`tools/github_stats.py`):" % (n_total, n_pulls, n_issues)) - print() - print('Pull Requests (%d):\n' % n_pulls) - report(pulls, show_urls) - print() - print('Issues (%d):\n' % n_issues) - report(issues, show_urls)