├── .gitignore ├── COPYING ├── Makefile ├── README.md ├── github_social_graph.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /.venv/ 3 | /github_social_graph.egg-info/ 4 | /dist/ 5 | 6 | /*.json 7 | /*.dot 8 | /*.png 9 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VENV=.venv 2 | PIP=$(VENV)/bin/pip2 3 | PIP3=$(VENV)/bin/pip3 4 | GSG=$(VENV)/bin/python2 $(VENV)/bin/gsg 5 | GSG3=$(VENV)/bin/python3 $(VENV)/bin/gsg 6 | 7 | # Just to be sure that `make' command won't do anything unexpectable. 8 | all: 9 | 10 | env: 11 | virtualenv -p python2 $(VENV) 12 | virtualenv -p python3 $(VENV) 13 | 14 | clean-env: 15 | rm -rf $(VENV) 16 | 17 | re-env: clean-env env 18 | 19 | install-deps: env 20 | $(PIP) install -e . 21 | $(PIP3) install -e . 22 | 23 | clean c: 24 | rm -rf github_social_graph.egg-info 25 | 26 | mrproper: clean clean-env 27 | find -name '*.pyc' -delete 28 | 29 | t: 30 | $(GSG) -i 1.json -o 1.png 31 | 32 | t3: 33 | $(GSG3) -i 1.json -o 1.png 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-social-graph 2 | 3 | This program helps to build simple social graphs for GitHub with the 4 | help of GitHub API v3 and [graphviz](http://www.graphviz.org/). 5 | 6 | ## Prerequisites 7 | 8 | To build `Pillow` and `pygraphviz` dependencies you will need some dev 9 | packages. Example for Debian-like system: 10 | ``` 11 | $ [sudo] apt-get install python-dev libjpeg-dev pkg-config graphviz-dev 12 | ``` 13 | 14 | ## Install 15 | 16 | ```bash 17 | $ [sudo] pip install github-social-graph 18 | ``` 19 | 20 | ## Usage 21 | 22 | Show help: 23 | ```bash 24 | $ github-social-graph -h # or gsg -h 25 | ``` 26 | 27 | Usage examples: 28 | 29 | ```bash 30 | # Draw graph for vim-jp organization members (without authorization): 31 | $ gsg --orgs vim-jp -o 1.png 32 | 33 | # Draw graph for organization and users (with authorization by password): 34 | $ gsg -u Kagami -p --orgs vim-jp --users Shougo -o 1.png 35 | 36 | # Only fetch data for future use and analysis: 37 | $ gsg --orgs vim-jp -o jp.json 38 | 39 | # Use pre-fetched data to draw graph: 40 | $ gsg -i jp.json -o jp.png 41 | ``` 42 | 43 | ## Examples 44 | 45 | This graph demonstrates some curious relationships between 46 | Japanese/Vim/Haskell users (click for fullsize image): 47 | 48 | [![](http://dump.bitcheese.net/files/ysilytu/jp-shrink-min.png)](http://dump.bitcheese.net/files/izunyjy/jp-min.png) 49 | 50 | (Produced by `github-social-graph --token --orgs vim-jp akechi golang-samples vimjolts neovim --users bos tibbe donsbot kazu-yamamoto spl MarcWeber ZyX-I -o jp.png`) 51 | 52 | Much simpler graph: 53 | 54 | ![](http://dump.bitcheese.net/files/itanida/kagami.png) 55 | 56 | (Produced by `github-social-graph --full-graph --users Kagami -o kagami.png`) 57 | 58 | ## TODO 59 | 60 | * Proper error handling 61 | * Info about current rate limits 62 | * Deep crawling 63 | * Merge several JSON inputs 64 | * Additional graph modes and options 65 | 66 | ## License 67 | 68 | github-social-graph - Build simple social graphs for GitHub 69 | 70 | Written in 2014-2015 by Kagami Hiiragi 71 | 72 | To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. 73 | 74 | You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . 75 | -------------------------------------------------------------------------------- /github_social_graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Build simple social graphs for GitHub. 3 | 4 | Examples: 5 | 6 | # Draw graph for vim-jp organization members (without authorization): 7 | $ github-social-graph --orgs vim-jp -o 1.png 8 | 9 | # Draw graph for organization and users (with authorization by password): 10 | $ github-social-graph -u Kagami -p --orgs vim-jp --users Shougo -o 1.png 11 | 12 | # Only fetch data for future use and analysis: 13 | $ github-social-graph --orgs vim-jp -o jp.json 14 | 15 | # Use pre-fetched data to draw graph: 16 | $ github-social-graph -i jp.json -o jp.png 17 | """ 18 | 19 | from __future__ import division 20 | import six 21 | 22 | import os 23 | import sys 24 | import errno 25 | import tempfile 26 | import os.path as path 27 | import json 28 | import argparse 29 | from copy import deepcopy 30 | from threading import Thread 31 | from io import BytesIO 32 | from six.moves import queue 33 | from six.moves.urllib.request import urlopen 34 | 35 | from pygithub3 import Github 36 | from pygraphviz import AGraph 37 | from PIL import Image, ImageDraw 38 | 39 | 40 | SUPPORTED_INPUT_FORMATS = ['json', 'dot'] 41 | AVATAR_DOWNLOADING_PARALLEL_LEVEL = 10 42 | AVATAR_SIZE = 60 43 | DPI = 96 44 | # blues4 45 | BACKGROUND_COLOR = '#eff3ff' 46 | ARROWS_BORDER_COLOR = '#bdd7e7' 47 | CIRCLES_BORDER_COLOR = '#6baed6' 48 | TEXT_COLOR = '#2171b5' 49 | 50 | 51 | def log(text, *args, **kwargs): 52 | out = text.format(*args, **kwargs) 53 | out += '\n' 54 | sys.stderr.write(out) 55 | 56 | 57 | def fetcher(options): 58 | def get_or_set(username): 59 | try: 60 | info = graph_data[username] 61 | except KeyError: 62 | graph_data[username] = info = {} 63 | return info 64 | 65 | github = Github( 66 | login=options.username, password=options.password, 67 | token=options.token) 68 | 69 | graph_data = {} 70 | usernames = set(options.users) 71 | 72 | log('Start fetching GitHub data. It may take some time, be patient.') 73 | for org_name in set(options.orgs): 74 | log('Fetching {}\'s members...', org_name) 75 | users = github.orgs.members.list_public(org_name).all() 76 | for user in users: 77 | usernames.add(user.login) 78 | for username in usernames: 79 | log('Fetching {}\'s followers and following...', username) 80 | followers = github.users.followers.list(username).all() 81 | following = github.users.followers.list_following(username).all() 82 | info = get_or_set(username) 83 | info['followers'] = [f.login for f in followers] 84 | info['following'] = [f.login for f in following] 85 | if not options.full_graph: 86 | graph_data = process_graph_data(graph_data) 87 | if options.avatars: 88 | for username, info in list(six.iteritems(graph_data)): 89 | log('Fetching {}\'s info...', username) 90 | info['avatar_url'] = github.users.get(username).avatar_url 91 | if options.full_graph: 92 | for f in set(info['followers'] + info['following']): 93 | f_info = get_or_set(f) 94 | if 'avatar_url' in f_info: 95 | continue 96 | log('Fetching {}\'s info...', f) 97 | f_info['avatar_url'] = github.users.get(f).avatar_url 98 | log('Fetching is complete.') 99 | return graph_data 100 | 101 | 102 | def process_graph_data(graph_data): 103 | """ 104 | Fix graph data to make it easier to create big graphs. 105 | """ 106 | graph_data = deepcopy(graph_data) 107 | for username, info in six.iteritems(graph_data): 108 | # Leave only users with followers info (do not draw huge amount 109 | # of isolated nodes). 110 | info['followers'] = [ 111 | f 112 | for f in info.get('followers', []) 113 | if 'followers' in graph_data.get(f, {}) 114 | ] 115 | info['following'] = [ 116 | f 117 | for f in info.get('following', []) 118 | if 'followers' in graph_data.get(f, {}) 119 | ] 120 | return graph_data 121 | 122 | 123 | def process_options(): 124 | class _NoPassword: pass 125 | class _NoToken: pass 126 | 127 | parser = argparse.ArgumentParser( 128 | description=__doc__, 129 | formatter_class=argparse.RawDescriptionHelpFormatter) 130 | parser.add_argument( 131 | '-u', '--username', 132 | help='GitHub username for authenticated requests') 133 | parser.add_argument( 134 | '-p', '--password', nargs='?', 135 | default=_NoPassword, 136 | help='GitHub password for authenticated requests; ' 137 | 'omit value if you wish to enter it by hand') 138 | parser.add_argument( 139 | '-t', '--token', metavar='TOKEN', nargs='?', 140 | default=_NoToken, 141 | help='GitHub token for authenticated requests; ' 142 | 'omit value if you wish to enter it by hand') 143 | parser.add_argument( 144 | '-i', '--input', type=argparse.FileType('r'), 145 | help='pre-fetched data filename or "-" for stdin') 146 | parser.add_argument( 147 | '-if', '--input-format', choices=SUPPORTED_INPUT_FORMATS, 148 | help='format of the input data; ' 149 | 'if not specified will be guessed from the filename') 150 | parser.add_argument( 151 | '-o', '--output', type=argparse.FileType('wb'), required=True, 152 | help='output filename or "-" for stdout') 153 | parser.add_argument( 154 | '-of', '--output-format', 155 | help='format of the output data (json, dot, png, etc.); ' 156 | 'if not specified will be guessed from the filename') 157 | parser.add_argument( 158 | '--orgs', metavar='ORGANIZATION', nargs='*', default=[], 159 | help='organizations to start fetching data with') 160 | parser.add_argument( 161 | '--users', metavar='USERNAME', nargs='*', default=[], 162 | help='users to start fetching data with') 163 | parser.add_argument( 164 | '-na', '--no-avatars', action='store_false', dest='avatars', 165 | help='do not show avatars in graphs') 166 | parser.add_argument( 167 | '-fg', '--full-graph', action='store_true', 168 | help='fetch and draw full graph ' 169 | '(this may take a long time and lot of API requests)') 170 | 171 | options = parser.parse_args() 172 | 173 | # Post-validate. 174 | if options.username and options.password is _NoPassword: 175 | parser.error('password should be specified') 176 | if options.password is not _NoPassword and options.username is None: 177 | parser.error('username should be specified') 178 | if options.password is not _NoPassword and options.token is not _NoToken: 179 | parser.error('password and token could not be used together') 180 | if options.input is sys.stdin and not options.input_format: 181 | parser.error('input format should be specified') 182 | if options.output is sys.stdout and not options.output_format: 183 | parser.error('output format should be specified') 184 | if options.input and not options.input_format: 185 | options.input_format = path.splitext(options.input.name)[1][1:] 186 | if options.input_format and \ 187 | options.input_format not in SUPPORTED_INPUT_FORMATS: 188 | parser.error('specified input format do not supported') 189 | if options.output and not options.output_format: 190 | options.output_format = path.splitext(options.output.name)[1][1:] 191 | if options.input_format == 'dot' and options.output_format == 'json': 192 | parser.error('could not convert dot to json') 193 | if not options.input and not options.orgs and not options.users: 194 | parser.error('no input data and no users/organizations is provided') 195 | 196 | # Fill additional values. 197 | if options.password is _NoPassword: 198 | # Clear hackish value for later options uses. 199 | options.password = None 200 | else: 201 | if options.password is None: 202 | options.password = raw_input('Enter password: ') 203 | if options.token is _NoToken: 204 | options.token = None 205 | else: 206 | if options.token is None: 207 | options.token = raw_input('Enter token: ') 208 | 209 | return options 210 | 211 | 212 | def get_avatars_cache_dir(): 213 | return path.join(tempfile.gettempdir(), 'github-social-graph') 214 | 215 | 216 | def get_avatar_path(node): 217 | return path.join(get_avatars_cache_dir(), '{}.png'.format(node)) 218 | 219 | 220 | def create_graph(graph_data, input_format, avatars): 221 | def add_node(node): 222 | attrs = { 223 | 'label': node, 224 | 'shape': 'circle', 225 | 'margin': 0, 226 | 'color': CIRCLES_BORDER_COLOR, 227 | 'fontcolor': TEXT_COLOR, 228 | 'fontsize': 10, 229 | } 230 | if avatars: 231 | # TODO: Draw some placeholder image for users without avatars? 232 | if 'avatar_url' in graph_data.get(node, {}): 233 | attrs['image'] = get_avatar_path(node) 234 | attrs['label'] = '' 235 | attrs['xlabel'] = node 236 | attrs['width'] = AVATAR_SIZE / DPI 237 | attrs['height'] = AVATAR_SIZE / DPI 238 | attrs['fixedsize'] = 'true' 239 | graph.add_node(node, **attrs) 240 | 241 | graph_attrs = { 242 | 'directed': True, 243 | 'dpi': DPI, 244 | 'background': BACKGROUND_COLOR, 245 | } 246 | if input_format == 'dot': 247 | graph = AGraph(graph_data, **graph_attrs) 248 | else: 249 | graph = AGraph(**graph_attrs) 250 | for username, info in six.iteritems(graph_data): 251 | add_node(username) 252 | for f in info.get('followers', []): 253 | add_node(f) 254 | graph.add_edge(f, username, color=ARROWS_BORDER_COLOR) 255 | for f in info.get('following', []): 256 | add_node(f) 257 | graph.add_edge(username, f, color=ARROWS_BORDER_COLOR) 258 | return graph 259 | 260 | 261 | def download_avatars(graph_data): 262 | def is_cached(username): 263 | avatar_path = get_avatar_path(username) 264 | if path.exists(avatar_path): 265 | return True 266 | 267 | def mkdirp(dirname): 268 | # See for details. 269 | try: 270 | os.makedirs(dirname) 271 | except OSError as exc: 272 | if exc.errno == errno.EEXIST and path.isdir(dirname): 273 | pass 274 | else: 275 | raise 276 | 277 | def download(urlsq, res): 278 | # TODO(Kagami): Error handling, timeouts... 279 | while True: 280 | try: 281 | url, username = urlsq.get_nowait() 282 | except queue.Empty: 283 | return 284 | try: 285 | data = urlopen(url).read() 286 | res.append((data, username)) 287 | finally: 288 | urlsq.task_done() 289 | 290 | mkdirp(get_avatars_cache_dir()) 291 | urls_usernames = [ 292 | (info['avatar_url'], username) 293 | for username, info in six.iteritems(graph_data) 294 | if 'avatar_url' in info and not is_cached(username)] 295 | if not urls_usernames: 296 | return 297 | log('Downloading {} avatars...', len(urls_usernames)) 298 | 299 | urlsq = queue.Queue() 300 | for u in urls_usernames: 301 | urlsq.put(u) 302 | res = [] 303 | threads_num = min(AVATAR_DOWNLOADING_PARALLEL_LEVEL, len(urls_usernames)) 304 | for _ in six.moves.range(threads_num): 305 | Thread(target=download, args=(urlsq, res)).start() 306 | urlsq.join() 307 | for data, username in res: 308 | with open(get_avatar_path(username), 'wb') as fh: 309 | fh.write(process_avatar(data)) 310 | 311 | 312 | def process_avatar(data): 313 | """ 314 | Shrink avatar image and do some post-processing. 315 | """ 316 | image = Image.open(BytesIO(data)) 317 | 318 | width, height = image.size 319 | mask = Image.new('RGBA', (width, height), (255, 255, 255, 0)) 320 | draw = ImageDraw.Draw(mask) 321 | draw.ellipse([0, 0, width, height], fill=(255,255,255,255)) 322 | mask.paste(image, mask=mask) 323 | image = mask 324 | image.thumbnail((AVATAR_SIZE, AVATAR_SIZE), Image.ANTIALIAS) 325 | 326 | output = BytesIO() 327 | image.save(output, 'PNG') 328 | return output.getvalue() 329 | 330 | 331 | def draw_graph(graph, output, format): 332 | graph.draw(output, format=format, prog='dot', args='-q') 333 | 334 | 335 | def main(): 336 | options = process_options() 337 | 338 | if options.input: 339 | if options.input_format == 'json': 340 | graph_data = json.load(options.input) 341 | elif options.input_format == 'dot': 342 | graph_data = options.input.read() 343 | else: 344 | raise NotImplementedError 345 | if options.input is not sys.stdin: 346 | options.input.close() 347 | else: 348 | graph_data = fetcher(options) 349 | 350 | if options.output_format == 'json': 351 | options.output.write(json.dumps(graph_data).encode('utf-8')) 352 | else: 353 | graph = create_graph(graph_data, options.input_format, options.avatars) 354 | if options.output_format == 'dot': 355 | graph.write(options.output) 356 | else: 357 | if options.avatars and \ 358 | (options.input_format == 'json' or not options.input): 359 | download_avatars(graph_data) 360 | draw_graph(graph, options.output, options.output_format) 361 | if options.output is not sys.stdout: 362 | options.output.close() 363 | 364 | 365 | if __name__ == '__main__': 366 | main() 367 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | # Reference: http://pythonhosted.org/distribute/setuptools.html 6 | name='github-social-graph', 7 | version='0.1.1', 8 | author='Kagami Hiiragi', 9 | author_email='kagami@genshiken.org', 10 | url='https://github.com/Kagami/github-social-graph', 11 | description='Build simple social graphs for GitHub', 12 | license='CC0', 13 | install_requires=[ 14 | 'pygithub33>=0.6.2', 15 | 'pygraphviz>=1.3rc2', 16 | 'Pillow>=2.4.0', 17 | 'six', 18 | ], 19 | py_modules=['github_social_graph'], 20 | entry_points={ 21 | 'console_scripts': [ 22 | 'github-social-graph = github_social_graph:main', 23 | 'gsg = github_social_graph:main', 24 | ], 25 | }, 26 | classifiers=[ 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.6', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.2', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 36 | 'Operating System :: OS Independent', 37 | 'Development Status :: 3 - Alpha', 38 | 'Intended Audience :: Information Technology', 39 | ], 40 | ) 41 | --------------------------------------------------------------------------------