├── .bumpversion.cfg ├── .github ├── dependabot.yml └── weekly-digest.yml ├── .gitignore ├── .pylintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo.gif ├── git_stalk ├── __init__.py └── stalk.py ├── pytest.ini ├── requirements.txt ├── setup.py ├── tests ├── __init__.py └── test_user_existence.py ├── tox.ini └── travis_requirements.txt /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.6.0 3 | commit = False 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:git_stalk/__init__.py] 9 | 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "23:30" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/weekly-digest.yml: -------------------------------------------------------------------------------- 1 | # Configuration for weekly-digest - 2 | https://github.com/apps/weekly-digest 3 | publishDay: sun 4 | canPublishIssues: true 5 | canPublishPullRequests: true 6 | canPublishContributors: true 7 | canPublishStargazers: true 8 | canPublishCommits: true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.html 4 | __pycache__/ 5 | *.egg-info/ 6 | dist/ 7 | build/ 8 | .vscode/ 9 | .tox/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CSV 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=4 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | #enable=similarities,classes 59 | enable=all 60 | 61 | # Disable the message, report, category or checker with the given id(s). You 62 | # can either give multiple identifiers separated by comma (,) or put this 63 | # option multiple times (only on the command line, not in the configuration 64 | # file where it should appear only once).You can also use "--disable=all" to 65 | # disable everything first and then reenable specific checks. For example, if 66 | # you want to run only the similarities checker, you can use "--disable=all 67 | # --enable=similarities". If you want to run only the classes checker, but have 68 | # no Warning level messages displayed, use"--disable=all --enable=classes 69 | # --disable=W" 70 | #disable=all 71 | #disable=map-builtin-not-iterating,reduce-builtin,parameter-unpacking,buffer-builtin,raw_input-builtin,execfile-builtin,raising-string,cmp-method,old-octal-literal,suppressed-message,print-statement,import-star-module-level,indexing-exception,long-builtin,apply-builtin,dict-view-method,old-division,unicode-builtin,setslice-method,unichr-builtin,round-builtin,oct-method,next-method-called,unpacking-in-except,metaclass-assignment,using-cmp-argument,old-raise-syntax,cmp-builtin,file-builtin,old-ne-operator,basestring-builtin,xrange-builtin,useless-suppression,nonzero-method,standarderror-builtin,range-builtin-not-iterating,delslice-method,no-absolute-import,coerce-method,input-builtin,backtick,long-suffix,intern-builtin,coerce-builtin,zip-builtin-not-iterating,filter-builtin-not-iterating,reload-builtin,getslice-method,hex-method,dict-iter-method 72 | disable=missing-docstring,wildcard-import,no-method,too-few-public-methods,invalid-name,no-member,bad-continuation,redefined-variable-type,too-many-ancestors,len-as-condition,no-else-return,abstract-method,too-many-instance-attributes 73 | 74 | [REPORTS] 75 | 76 | # Set the output format. Available formats are text, parseable, colorized, msvs 77 | # (visual studio) and html. You can also give a reporter class, eg 78 | # mypackage.mymodule.MyReporterClass. 79 | output-format=text 80 | 81 | # Put messages in a separate file for each module / package specified on the 82 | # command line instead of printing them on stdout. Reports (if any) will be 83 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 84 | # and it will be removed in Pylint 2.0. 85 | files-output=no 86 | 87 | # Tells whether to display a full report or only the messages 88 | reports=yes 89 | 90 | # Python expression which should return a note less than 10 (10 is the highest 91 | # note). You have access to the variables errors warning, statement which 92 | # respectively contain the number of errors / warnings messages and the total 93 | # number of statements analyzed. This is used by the global evaluation report 94 | # (RP0004). 95 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 96 | 97 | # Template used to display messages. This is a python new-style format string 98 | # used to format the message information. See doc for all details 99 | #msg-template= 100 | 101 | 102 | [SPELLING] 103 | 104 | # Spelling dictionary name. Available dictionaries: none. To make it working 105 | # install python-enchant package. 106 | spelling-dict= 107 | 108 | # List of comma separated words that should not be checked. 109 | spelling-ignore-words= 110 | 111 | # A path to a file that contains private dictionary; one word per line. 112 | spelling-private-dict-file= 113 | 114 | # Tells whether to store unknown words to indicated private dictionary in 115 | # --spelling-private-dict-file option instead of raising a message. 116 | spelling-store-unknown-words=no 117 | 118 | 119 | [SIMILARITIES] 120 | 121 | # Minimum lines number of a similarity. 122 | min-similarity-lines=4 123 | 124 | # Ignore comments when computing similarities. 125 | ignore-comments=yes 126 | 127 | # Ignore docstrings when computing similarities. 128 | ignore-docstrings=yes 129 | 130 | # Ignore imports when computing similarities. 131 | ignore-imports=no 132 | 133 | 134 | [VARIABLES] 135 | 136 | # Tells whether we should check for unused import in __init__ files. 137 | init-import=no 138 | 139 | # A regular expression matching the name of dummy variables (i.e. expectedly 140 | # not used). 141 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 142 | 143 | # List of additional names supposed to be defined in builtins. Remember that 144 | # you should avoid to define new builtins when possible. 145 | additional-builtins= 146 | 147 | # List of strings which can identify a callback function by name. A callback 148 | # name must start or end with one of those strings. 149 | callbacks=cb_,_cb 150 | 151 | # List of qualified module names which can have objects that can redefine 152 | # builtins. 153 | redefining-builtins-modules=six.moves,future.builtins 154 | 155 | 156 | [TYPECHECK] 157 | 158 | # Tells whether missing members accessed in mixin class should be ignored. A 159 | # mixin class is detected if its name ends with "mixin" (case insensitive). 160 | ignore-mixin-members=yes 161 | 162 | # List of module names for which member attributes should not be checked 163 | # (useful for modules/projects where namespaces are manipulated during runtime 164 | # and thus existing member attributes cannot be deduced by static analysis. It 165 | # supports qualified module names, as well as Unix pattern matching. 166 | ignored-modules= 167 | 168 | # List of class names for which member attributes should not be checked (useful 169 | # for classes with dynamically set attributes). This supports the use of 170 | # qualified names. 171 | ignored-classes=optparse.Values,thread._local,_thread._local 172 | 173 | # List of members which are set dynamically and missed by pylint inference 174 | # system, and so shouldn't trigger E1101 when accessed. Python regular 175 | # expressions are accepted. 176 | generated-members= 177 | 178 | # List of decorators that produce context managers, such as 179 | # contextlib.contextmanager. Add to this list to register other decorators that 180 | # produce valid context managers. 181 | contextmanager-decorators=contextlib.contextmanager 182 | 183 | 184 | [MISCELLANEOUS] 185 | 186 | # List of note tags to take in consideration, separated by a comma. 187 | notes=FIXME,XXX,TODO 188 | 189 | 190 | [LOGGING] 191 | 192 | # Logging modules to check that the string format arguments are in logging 193 | # function parameter format 194 | logging-modules=logging 195 | 196 | 197 | [FORMAT] 198 | 199 | # Maximum number of characters on a single line. 200 | max-line-length=120 201 | 202 | # Regexp for a line that is allowed to be longer than the limit. 203 | ignore-long-lines=^\s*(# )??$ 204 | 205 | # Allow the body of an if to be on the same line as the test if there is no 206 | # else. 207 | single-line-if-stmt=no 208 | 209 | # List of optional constructs for which whitespace checking is disabled. `dict- 210 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 211 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 212 | # `empty-line` allows space-only lines. 213 | no-space-check=trailing-comma,dict-separator 214 | 215 | # Maximum number of lines in a module 216 | max-module-lines=1000 217 | 218 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 219 | # tab). 220 | indent-string=' ' 221 | 222 | # Number of spaces of indent required inside a hanging or continued line. 223 | indent-after-paren=4 224 | 225 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 226 | expected-line-ending-format= 227 | 228 | 229 | [BASIC] 230 | 231 | # Good variable names which should always be accepted, separated by a comma 232 | good-names=i,j,k,ex,Run,_ 233 | 234 | # Bad variable names which should always be refused, separated by a comma 235 | bad-names=foo,bar,baz,toto,tutu,tata 236 | 237 | # Colon-delimited sets of names that determine each other's naming style when 238 | # the name regexes allow several styles. 239 | name-group= 240 | 241 | # Include a hint for the correct naming format with invalid-name 242 | include-naming-hint=no 243 | 244 | # List of decorators that produce properties, such as abc.abstractproperty. Add 245 | # to this list to register other decorators that produce valid properties. 246 | property-classes=abc.abstractproperty 247 | 248 | # Regular expression matching correct inline iteration names 249 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 250 | 251 | # Naming hint for inline iteration names 252 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 253 | 254 | # Regular expression matching correct attribute names 255 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 256 | 257 | # Naming hint for attribute names 258 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 259 | 260 | # Regular expression matching correct function names 261 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 262 | 263 | # Naming hint for function names 264 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 265 | 266 | # Regular expression matching correct variable names 267 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 268 | 269 | # Naming hint for variable names 270 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 271 | 272 | # Regular expression matching correct module names 273 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 274 | 275 | # Naming hint for module names 276 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 277 | 278 | # Regular expression matching correct argument names 279 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 280 | 281 | # Naming hint for argument names 282 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 283 | 284 | # Regular expression matching correct method names 285 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 286 | 287 | # Naming hint for method names 288 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 289 | 290 | # Regular expression matching correct class names 291 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 292 | 293 | # Naming hint for class names 294 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 295 | 296 | # Regular expression matching correct class attribute names 297 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 298 | 299 | # Naming hint for class attribute names 300 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 301 | 302 | # Regular expression matching correct constant names 303 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 304 | 305 | # Naming hint for constant names 306 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 307 | 308 | # Regular expression which should only match function or class names that do 309 | # not require a docstring. 310 | no-docstring-rgx=^_ 311 | 312 | # Minimum line length for functions/classes that require docstrings, shorter 313 | # ones are exempt. 314 | docstring-min-length=-1 315 | 316 | 317 | [ELIF] 318 | 319 | # Maximum number of nested blocks for function / method body 320 | max-nested-blocks=5 321 | 322 | 323 | [DESIGN] 324 | 325 | # Maximum number of arguments for function / method 326 | max-args=6 327 | 328 | # Argument names that match this expression will be ignored. Default to name 329 | # with leading underscore 330 | ignored-argument-names=_.* 331 | 332 | # Maximum number of locals for function / method body 333 | max-locals=15 334 | 335 | # Maximum number of return / yield for function / method body 336 | max-returns=6 337 | 338 | # Maximum number of branch for function / method body 339 | max-branches=15 340 | 341 | # Maximum number of statements in function / method body 342 | max-statements=50 343 | 344 | # Maximum number of parents for a class (see R0901). 345 | max-parents=7 346 | 347 | # Maximum number of attributes for a class (see R0902). 348 | max-attributes=7 349 | 350 | # Minimum number of public methods for a class (see R0903). 351 | min-public-methods=2 352 | 353 | # Maximum number of public methods for a class (see R0904). 354 | max-public-methods=20 355 | 356 | # Maximum number of boolean expressions in a if statement 357 | max-bool-expr=5 358 | 359 | 360 | [CLASSES] 361 | 362 | # List of method names used to declare (i.e. assign) instance attributes. 363 | defining-attr-methods=__init__,__new__,setUp 364 | 365 | # List of valid names for the first argument in a class method. 366 | valid-classmethod-first-arg=cls 367 | 368 | # List of valid names for the first argument in a metaclass class method. 369 | valid-metaclass-classmethod-first-arg=mcs 370 | 371 | # List of member names, which should be excluded from the protected access 372 | # warning. 373 | exclude-protected=_asdict,_fields,_replace,_source,_make 374 | 375 | 376 | [IMPORTS] 377 | 378 | # Deprecated modules which should not be used, separated by a comma 379 | deprecated-modules=optparse 380 | 381 | # Create a graph of every (i.e. internal and external) dependencies in the 382 | # given file (report RP0402 must not be disabled) 383 | import-graph= 384 | 385 | # Create a graph of external dependencies in the given file (report RP0402 must 386 | # not be disabled) 387 | ext-import-graph= 388 | 389 | # Create a graph of internal dependencies in the given file (report RP0402 must 390 | # not be disabled) 391 | int-import-graph= 392 | 393 | # Force import order to recognize a module as part of the standard 394 | # compatibility libraries. 395 | known-standard-library= 396 | 397 | # Force import order to recognize a module as part of a third party library. 398 | known-third-party=enchant 399 | 400 | # Analyse import fallback blocks. This can be used to support both Python 2 and 401 | # 3 compatible code, which means that the block might have code that exists 402 | # only in one or another interpreter, leading to false positives when analysed. 403 | analyse-fallback-blocks=no 404 | 405 | 406 | [EXCEPTIONS] 407 | 408 | # Exceptions that will emit a warning when being caught. Defaults to 409 | # "Exception" 410 | overgeneral-exceptions=Exception 411 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.6" 5 | install: 6 | - pip install -r requirements.txt 7 | - pip install -r travis_requirements.txt 8 | script: 9 | - pytest --cov git_stalk --cov-report term-missing --cov-report xml 10 | - pytest --flakes 11 | - pytest --pep8 12 | - pytest --mccabe -m mccabe 13 | - pytest --pylint -m pylint 14 | notifications: 15 | email: false 16 | webhooks: https://fathomless-fjord-24024.herokuapp.com/notify 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.4.2] - 2018-10-30 6 | 7 | ### Added 8 | 9 | - Fixed "since and until arguments error" by @VANKINEENITAWRUN. 10 | 11 | ## [1.4.1] - 2018-10-30 12 | 13 | ### Added 14 | 15 | - Replaced argparse with docopt as cli argument parser. 16 | 17 | ## [1.4.0] - 2018-10-30 18 | 19 | ### Added 20 | 21 | - Fixed bug " KeyError for Issue and Name" by @VANKINEENITAWRUN. 22 | 23 | ## [1.3.0] - 2018-10-26 24 | 25 | ### Added 26 | 27 | - Added --followers and --follows flags by @VANKINEENITAWRUN. 28 | 29 | ## [1.2.1] - 2018-10-25 30 | 31 | ### Bug Fixes 32 | 33 | - Fixed the application call with no arguments, now showing help by @rods-honorio 34 | 35 | ## [1.2.0] - 2018-10-23 36 | 37 | ### Added 38 | 39 | - Bump version: 1.1.1 → 1.2.0 by @aashutoshrathi 40 | - Updated .gitignore by @aashutoshrathi 41 | - Remove too-complex-functions warnings by @hyunchel 42 | - Added Bumpversion by @gpetrousov 43 | 44 | ## [1.1.1] - 2018-10-17 45 | 46 | ### Added 47 | 48 | - Replace Today string with date by @ammarmallik 49 | 50 | 51 | ## [1.1.0] - 2018-10-14 52 | 53 | ### Added 54 | 55 | - IP address shown on exceeding API call limit by @sayanmondal2098 56 | 57 | ### Bug Fixes 58 | 59 | - Fixed bug "AttributeError: 'dict' object has no attribute 'name' " by @Galixxon 60 | 61 | ## [1.0.9] - 2018-10-11 62 | 63 | ### Changes 64 | 65 | - Added --since and --until flags by @gpetrousov. 66 | - Events are filtered based on the flags above 67 | 68 | ## [1.0.8] - 2018-10-10 69 | 70 | ### Added 71 | 72 | - Added Starred repo tuple by @jeffreyrack. 73 | - String formatting instead of concatenation by @nityanandagohain. 74 | 75 | ## [1.0.7] - 2018-10-07 76 | 77 | ### Added 78 | 79 | - Test to check if invalid username by @Khukhuna. 80 | - Test to check if no internet by @Khukhuna. 81 | 82 | ## [1.0.6] - 2018-10-03 83 | 84 | ### Updated 85 | 86 | - Using global variable for URI from @bossbossk20 87 | - Throw error for non existing users from @Khukhuna 88 | 89 | ### Added 90 | 91 | - Date arguments for --until and --from by @bossbossk20 92 | - Docstrings 93 | 94 | ## [1.0.4 and 1.0.5] - 2018-10-02 95 | 96 | ### Added 97 | 98 | - GIF added to README by @NefixEstarda 99 | - Telegram web hook to Travis by @aashutoshrathi 100 | - Python3.5 101 | 102 | 103 | ## [1.0.3] - 2018-10-02 104 | 105 | ### Added 106 | 107 | - tox.ini and PEP8 108 | 109 | ## [1.0.2] - 2018-10-01 110 | 111 | ### Added 112 | 113 | - Filter by Organisation 114 | 115 | ## [1.0.0] - 2018-07-18 116 | 117 | ### Updated 118 | 119 | - View of tables and partitioned stars section. 120 | - Centered badges and description 121 | - Updated Readme 122 | 123 | ## [0.0.4] - 2018-06-06 124 | 125 | ### Added 126 | 127 | - FOSSBot License Checker 128 | 129 | ## [0.0.3] - 2018-06-04 130 | 131 | ### Added 132 | 133 | - Pretty printing of all events by user 134 | 135 | ## [0.0.2] - 2018-06-03 136 | 137 | ### Added 138 | 139 | - Test 140 | - Travis CI 141 | 142 | ## [0.0.1] - 2018-06-02 143 | 144 | ### Added 145 | 146 | - Functionality to search user on GitHub 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Aashutosh Rathi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

git-stalk-cli

3 | 4 |

5 | 6 | 7 | 8 | 9 |

10 |
11 |

A command line interface for checking your/peer's activity today.

12 | 13 | 14 | ## Installation 15 | 16 | ```sh 17 | pip install git-stalk 18 | ``` 19 | 20 | ## Demo 21 | 22 | ![Demo Gif](./demo.gif) 23 | 24 | ## Limitations 25 | 26 | Stalking too much might lead to "API Rate Limit Exceeded for your IP". 27 | 28 | ## Releasing new version 29 | 30 | ```sh 31 | git checkout master 32 | git fetch 33 | git merge 34 | tox 35 | bumpversion minor --tag --commit 36 | git push origin master --tags 37 | ``` 38 | 39 | ## Author 40 | 41 | [Aashutosh Rathi](https://github.com/aashutoshrathi) 42 | 43 | 44 | 45 | [](https://twitter.com/AashutoshRathi) 46 | [](https://linkedin.com/in/aashutoshrathi) 47 | [](https://www.facebook.com/aashutoshrathi) 48 | [](https://www.paypal.me/AashutoshRathi) 49 | 50 | 51 |

Made from scratch by Aashutosh Rathi

52 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashutoshrathi/git-stalk-cli/556b4873ca2aec1db43c7a190e4ee4add903c3f3/demo.gif -------------------------------------------------------------------------------- /git_stalk/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.6.0' 2 | __author__ = 'Aashutosh Rathi ' 3 | __all__ = [] 4 | -------------------------------------------------------------------------------- /git_stalk/stalk.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | usage: stalk [--org ORG] [-U] [--np] [--followers] [--follows] 5 | [--since SINCE] [--until UNTIL] 6 | 7 | positional arguments: 8 | name name of the user 9 | 10 | optional arguments: 11 | --org ORG Organization Name 12 | -U, --update Update this program to latest version. Make sure that you 13 | have sufficient permissions (run with sudo if needed) 14 | --np Stalks a user without showing their profile 15 | --followers display all the followers of the user 16 | --follows display all the users followed by the user 17 | --since SINCE Take into account only events since date. Date format MM-DD-YYYY 18 | --until UNTIL Take into account only events until date. Date format MM-DD-YYYY 19 | --version Print application version 20 | -h, --help show this help message and exit 21 | """ 22 | 23 | # python stdlib 24 | from __future__ import print_function 25 | import datetime 26 | import os 27 | import re 28 | import sys 29 | from collections import namedtuple 30 | 31 | # 3rd party imports 32 | import requests 33 | from dateutil import tz 34 | from docopt import docopt, DocoptExit 35 | from prettytable import PrettyTable 36 | 37 | # Git Stalk imports 38 | from git_stalk import __version__ 39 | 40 | github_uri = "https://api.github.com/users/" 41 | StarredRepo = namedtuple('StarredRepo', ['name', 'language', 'time']) 42 | 43 | 44 | def jft(user): 45 | """ Return userlink statuscode """ 46 | user_link = "{0}{1}".format(github_uri, str(user)) 47 | response = requests.get(user_link) 48 | 49 | return response.status_code 50 | 51 | 52 | def get_event(string): 53 | """Returns the event""" 54 | event = "" 55 | words = re.findall('[A-Z][^A-Z]*', string) 56 | event = " ".join(words) 57 | 58 | if event == "Pull Request Review Comment Event": 59 | event = "PR Review Event" 60 | 61 | if event == "Watch Event": 62 | event = "Starred Event" 63 | 64 | if event == "Create Event": 65 | event = "Commit Event" 66 | 67 | return event[:-6] 68 | 69 | 70 | def get_details(event): 71 | """Returns the details of the event according to the type of the event""" 72 | issue_title = event.get("payload", {}).get("issue", {}).get("title") 73 | issue_body = event.get("payload", {}).get("comment", {}).get("body") 74 | pr_title = event.get("payload", {}).get("pull_request", {}).get("title") 75 | push_event = "".join([ 76 | commit["message"]for commit in event.get("payload", {}).get("commits", {}) if commit["distinct"] 77 | ]) 78 | member_login = event.get("payload", {}).get("member", {}).get("login") 79 | release_tag_name = event.get("payload", {}).get( 80 | "release", {}).get("tag_name") 81 | repo_name = event.get("repo", {}).get("name") 82 | deleted_obj_ref = event.get("payload", {}).get("ref") 83 | deleted_obj_type = event.get("payload", {}).get("ref_type") 84 | 85 | types = { 86 | "IssuesEvent": issue_title, 87 | "IssuesCommentEvent": issue_body, 88 | "PullRequestEvent": pr_title, 89 | "PushEvent": push_event, 90 | "MemberEvent": "Added {0} as collaborator".format(member_login), 91 | "ReleaseEvent": "Released binaries for version {0}".format(release_tag_name), 92 | "ForkEvent": "Forked {0}".format(repo_name), 93 | "DeleteEvent": "Deleted {0}: {1}".format(str(deleted_obj_type).title(), deleted_obj_ref), 94 | } 95 | 96 | return types.get(event["type"], "") 97 | 98 | 99 | def check_for_fork(link, user): 100 | """Check whether it is a forked.""" 101 | tukde = link.split('/') 102 | 103 | if tukde[len(tukde) - 2] == user: 104 | response = requests.get(link) 105 | repo = response.json() 106 | 107 | if not repo["fork"]: 108 | return True 109 | 110 | return False 111 | 112 | return True 113 | 114 | 115 | def get_local_time(string): 116 | """Returns the local time.""" 117 | local_time = convert_to_local(string) 118 | tukde = local_time.split(' ') 119 | samay = tukde[1].split('+')[0] 120 | 121 | return samay 122 | 123 | 124 | def get_following_users(user): 125 | """prints the users followed by current user""" 126 | following_link = "{0}{1}/following".format(github_uri, str(user)) 127 | following_users = requests.get(following_link).json() 128 | 129 | print("Following : ") 130 | 131 | for following_user in following_users: 132 | print("{0}".format(following_user["login"])) 133 | 134 | print("\n") 135 | 136 | 137 | def get_followers(user): 138 | """prints the followers of user""" 139 | followers_link = "{0}{1}/followers".format(github_uri, str(user)) 140 | followers = requests.get(followers_link).json() 141 | 142 | print("Followed By : ") 143 | 144 | for follower in followers: 145 | print("{0}".format(follower["login"])) 146 | 147 | print("\n") 148 | 149 | 150 | def get_basic_info(user): 151 | """Prints the user's basic info.""" 152 | user_link = "{0}{1}".format(github_uri, str(user)) 153 | user_profile = requests.get(user_link) 154 | profile = user_profile.json() 155 | 156 | print("Name: {0}".format(profile["name"])) 157 | print("Company: {0}".format(profile["company"])) 158 | print("Bio: {0}".format(profile["bio"])) 159 | print("Followers: {0}".format(profile["followers"])) 160 | print("Following: {0}".format(profile["following"])) 161 | print("Public Repos: {0}".format(profile["public_repos"])) 162 | print("Public Gists: {0}".format(profile["public_gists"])) 163 | print("Open for hiring: {0} \n".format(profile["hireable"])) 164 | 165 | 166 | def convert_to_local(string): 167 | """Returns the local_stamp as string.""" 168 | from_zone = tz.tzutc() 169 | to_zone = tz.tzlocal() 170 | utc_stamp = datetime.datetime.strptime( 171 | string, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=from_zone) 172 | local_stamp = utc_stamp.astimezone(to_zone) 173 | 174 | return str(local_stamp) 175 | 176 | 177 | def date_time_validate(date_text): 178 | try: 179 | datetime.datetime.strptime(date_text, "%Y-%m-%d") 180 | except ValueError: 181 | raise ValueError("Incorrect data format, should be YYYY-MM-DD") 182 | 183 | 184 | def get_contributions(user, latest, date_text, org=None): 185 | """ 186 | Traverses the latest array, 187 | creates a table 188 | if org argument is present only the repos which belong to the org \ 189 | is added to the table 190 | and prints the table. 191 | """ 192 | print("Contributions Today: ") 193 | 194 | if latest: 195 | table = PrettyTable(["Type", "Repository", "Time", "Details"]) 196 | 197 | for event in latest: 198 | repo_name = event["repo"]["name"] 199 | 200 | if org: 201 | curr_org = "" 202 | 203 | for c in repo_name: 204 | if c == r'/': 205 | break 206 | curr_org += c 207 | 208 | if curr_org == org: 209 | table.add_row([ 210 | get_event(event["type"]), 211 | event["repo"]["name"], 212 | get_local_time(event["created_at"]), 213 | get_details(event), 214 | ]) 215 | else: 216 | table.add_row([ 217 | get_event(event["type"]), 218 | event["repo"]["name"], 219 | get_local_time(event["created_at"]), 220 | get_details(event), 221 | ]) 222 | 223 | print(table) 224 | 225 | print("{0} have made {1} public contribution(s) {2}.\n".format( 226 | user, str(len(latest)), date_text)) 227 | 228 | 229 | def get_other_activity(user, other, date_text): 230 | """ 231 | Traverses the other array, 232 | creates a table 233 | and prints the table. 234 | """ 235 | print("Other Activity {0}: ".format(date_text)) 236 | 237 | if other: 238 | other_table = PrettyTable(["Type", "Repository", "Time", "Details"]) 239 | 240 | for event in other: 241 | other_table.add_row([ 242 | get_event(event["type"]), event["repo"]["name"], 243 | get_local_time(event["created_at"]), 244 | get_details(event), 245 | ]) 246 | 247 | print(other_table) 248 | 249 | print("{0} have done {1} other public activit(y/ies) {2}.\n".format( 250 | user, str(len(other)), date_text)) 251 | 252 | 253 | def display_stars(user, stars, date_text): 254 | """ 255 | Traverses the stars array, 256 | creates a table 257 | and prints the table. 258 | """ 259 | print("Starred {0}: ".format(date_text)) 260 | 261 | if stars: 262 | star_table = PrettyTable(["Repository", "Language", "Time"]) 263 | 264 | for starred_repo in stars: 265 | star_table.add_row([ 266 | starred_repo["repo"]["name"], 267 | get_language_for_repo(starred_repo["repo"]["url"]), 268 | get_local_time(starred_repo["created_at"]), 269 | ]) 270 | 271 | print(star_table) 272 | 273 | print("{0} have starred {1} repo(s) {2}.".format( 274 | user, str(len(stars)), date_text)) 275 | 276 | 277 | def fill_todays_data(user, today, events, latest, stars, other): 278 | """ 279 | Traverses the events array and separates individual data to latest, 280 | stars and other arrays 281 | """ 282 | for event in events: 283 | starts_today = convert_to_local(event["created_at"]).startswith(today) 284 | event_type_issue_comment_event = event["type"] != "IssueCommentEvent" 285 | 286 | if starts_today and event_type_issue_comment_event: 287 | if event["type"] == "WatchEvent": 288 | stars.append(create_star(event)) 289 | elif event["type"] in ("ForkEvent", "MemberEvent"): 290 | other.append(event) 291 | elif check_for_fork(event["repo"]["url"], user): 292 | latest.append(event) 293 | 294 | return latest, stars, other 295 | 296 | 297 | def fill_dated_data(user, events, latest, stars, other): 298 | """ 299 | Traverses the events array and seperates individual data to latest, 300 | stars and other arrays 301 | """ 302 | for event in events: 303 | event_type_issue_comment_event = event["type"] != "IssueCommentEvent" 304 | 305 | if event_type_issue_comment_event: 306 | if event["type"] == "WatchEvent": 307 | stars.append(event) 308 | elif event["type"] in ("ForkEvent", "MemberEvent"): 309 | other.append(event) 310 | elif check_for_fork(event["repo"]["url"], user): 311 | latest.append(event) 312 | 313 | return latest, stars, other 314 | 315 | 316 | def get_language_for_repo(url): 317 | response = requests.get(url) 318 | repo = response.json() 319 | 320 | return repo['language'] 321 | 322 | 323 | def create_star(event): 324 | language = get_language_for_repo(event['repo']['url']) 325 | 326 | return StarredRepo( 327 | name=event['repo']['name'], language=language, 328 | time=get_local_time(event['created_at']), 329 | ) 330 | 331 | 332 | def update(): 333 | """Runs the upgrade command and upgrades git-stalk""" 334 | os.system("pip install --upgrade git-stalk") 335 | 336 | 337 | def filter_since_until_dates(events, since_date=None, until_date=None): 338 | """Filters the events based on since and until dates""" 339 | event_tuples = [(datetime.datetime.strptime( 340 | event['created_at'][:10], "%Y-%m-%d"), event) for event in events] 341 | if since_date: 342 | event_tuples = [ 343 | event_tuple for event_tuple in event_tuples if since_date <= event_tuple[0]] 344 | if until_date: 345 | event_tuples = [ 346 | event_tuple for event_tuple in event_tuples if event_tuple[0] <= until_date] 347 | return [event_tuple[1] for event_tuple in event_tuples] 348 | 349 | 350 | def getipaddress(): 351 | return requests.get("http://ipecho.net/plain?").text 352 | 353 | 354 | def parse_date_from_string(datetime_object): 355 | """Return datetime object as string.""" 356 | return datetime.datetime.strptime(datetime_object, "%m-%d-%Y") 357 | 358 | 359 | def get_dates_from_arguments(arguments): 360 | """Return triplet of dates from given arguments.""" 361 | since_date, until_date, text_date = None, None, "" 362 | 363 | if arguments["--since"]: 364 | since_date = parse_date_from_string(arguments["--since"]) 365 | text_date = "since {0}".format(since_date) 366 | 367 | if arguments["--until"]: 368 | until_date = parse_date_from_string(arguments["--until"]) 369 | if text_date == "": 370 | text_date = "until {0}".format(until_date) 371 | else: 372 | text_date = "from {0} to {1}".format(since_date, until_date) 373 | 374 | return since_date, until_date, text_date 375 | 376 | 377 | def show_contri(args=None): 378 | """ 379 | Sends a get request to GitHub REST api and display data using the 380 | utility functions 381 | """ 382 | user = args[""] 383 | today = str(datetime.datetime.now().strftime("%Y-%m-%d")) 384 | link = "{0}{1}/events".format(github_uri, str(user)) 385 | response = requests.get(link) 386 | events = response.json() 387 | latest = [] 388 | stars = [] 389 | other = [] 390 | text_date = "" 391 | 392 | # NOTE: This needs more work. Possibly creating its own class of some sorts. 393 | if response.status_code == 200: 394 | since_date, until_date, text_date = get_dates_from_arguments(args) 395 | events = filter_since_until_dates( 396 | events, since_date=since_date, until_date=until_date) 397 | 398 | if 'since_date' in vars() or 'until_date' in vars(): 399 | latest, stars, other = fill_dated_data( 400 | user, events, latest, stars, other) 401 | else: 402 | latest, stars, other = fill_todays_data( 403 | user, today, events, latest, stars, other) 404 | 405 | if not args["--np"]: 406 | get_basic_info(user) 407 | 408 | if args["--org"]: 409 | get_contributions(user, latest, text_date, args["--org"]) 410 | else: 411 | get_contributions(user, latest, text_date) 412 | 413 | if args["--follows"]: 414 | get_following_users(user) 415 | 416 | if args["--followers"]: 417 | get_followers(user) 418 | 419 | get_other_activity(user, other, text_date) 420 | display_stars(user, stars, text_date) 421 | else: 422 | error_messages = { 423 | 404: "User with username {0} does not exists, please check and try again".format(user), 424 | 403: ("API rate limit exceeded for IP address" 425 | " {0} Try again later or change IP adress.").format(getipaddress()), 426 | } 427 | fallback_error_message = ( 428 | "Something went wrong, please check your internet connection \n" 429 | "Use stalk --help for Help" 430 | ) 431 | err_msg = error_messages.get( 432 | response.status_code, fallback_error_message) 433 | 434 | print(err_msg) 435 | 436 | 437 | def run(): 438 | """Parsing the command line arguments using argparse and calls the update 439 | or show_contri function as required""" 440 | try: 441 | args = docopt(__doc__, version=__version__) 442 | except DocoptExit: 443 | print(__doc__) 444 | sys.exit(1) 445 | 446 | if args["--update"]: 447 | update() 448 | else: 449 | show_contri(args) 450 | 451 | 452 | if __name__ == '__main__': 453 | run() 454 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pep8ignore = E501 E125 E126 E127 3 | flakes-ignore = 4 | ImportStarUsage ImportStarUsed 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | prettytable 3 | python-dateutil 4 | . 5 | tox 6 | bumpversion 7 | docopt 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="git_stalk", 8 | version="1.6.0", 9 | author="Aashutosh Rathi", 10 | author_email="aashutoshrathi@gmail.com", 11 | description="For stalkers like Daddu", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/aashutoshrathi/git-stalk-cli", 15 | packages=setuptools.find_packages(), 16 | install_requires=['requests', 'prettytable', 'python-dateutil', 'tox', 'docopt'], 17 | classifiers=( 18 | 'Programming Language :: Python :: 3', 19 | 'Programming Language :: Python :: 3.6', 20 | 'Programming Language :: Python :: 3.7', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | ), 24 | entry_points={ 25 | 'console_scripts': [ 26 | 'stalk = git_stalk.stalk:run', 27 | ], 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aashutoshrathi/git-stalk-cli/556b4873ca2aec1db43c7a190e4ee4add903c3f3/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_user_existence.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_non_existing(): 5 | # GitHub's username cannot begin with a hyphen 6 | # So there is no way that account with username _O_ exists 7 | process = os.popen('stalk _O_') 8 | output = process.read() 9 | # If API limit is reached, there's no way to test this case 10 | if "API" in output or len(output) <= 1: 11 | assert True 12 | else: 13 | assert "does not exists" in output 14 | process.close() 15 | 16 | 17 | def test_existing(): 18 | process = os.popen('stalk 1') 19 | output = process.read() 20 | # If API limit is reached, there's no way to test this case 21 | if "API" in output or len(output) <= 1: 22 | assert True 23 | else: 24 | assert "followers" in output.lower() 25 | process.close() 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py36, py37 8 | 9 | [testenv] 10 | deps = 11 | pytest 12 | commands = 13 | pytest 14 | -------------------------------------------------------------------------------- /travis_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-mccabe 4 | pytest-flakes 5 | pytest-pep8 6 | pytest-pylint 7 | --------------------------------------------------------------------------------