├── DODOPIE ├── LICENSE ├── MANIFEST.in ├── README.md ├── dodo ├── dodo.py ├── logo.png └── setup.py /DODOPIE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmb4u/dodo/1ce6426794e7e5a237020879570b012c5d436349/DODOPIE -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Anoop Thomas Mathew 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of this project nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![DoDo](https://github.com/atmb4u/dodo/blob/master/logo.png?raw=true) 2 | 3 | --- 4 | 5 | DoDo - Task Management for Hackers 6 | ---------------------------------- 7 | 8 | Dodo is an easily maintainable task list for version controlled projects and hackers. We can call `dodo` a ticket tracking inside the repo itself. 9 | Tasks are called as *DoDo*, and __your goal is to make DoDos extinct.__ 10 | 11 | In the latest version, username is automatically populated from the system username if not passed explicitly. And the usage flow has been stripped down so as to take the ease of use to the next level 12 | 13 | 14 | ## Getting Started 15 | 16 | ```python 17 | pip install dodopie 18 | # use sudo if you want to install dodo globally 19 | # sudo pip install dodopie 20 | ``` 21 | 22 | ### Initializing 23 | 24 | ```python 25 | dodo init 26 | ``` 27 | Options 28 | 29 | dodo init -f [dodo filename] 30 | 31 | 32 | ### Basic WorkFlow 33 | 34 | 35 | ```python 36 | dodo add "New Task" # add a new DoDo 37 | ``` 38 | ``` python 39 | dodo l # list all dodo tasks 40 | > ID Status Date(-t) Owner(-u) Description (-d) 41 | > 1 [+] 1 minute ago (atm) New Task 42 | ``` 43 | ```python 44 | dodo workon 1 # mark DoDo #1 as working on 45 | ``` 46 | ```python 47 | dodo finish 1 # mark DoDo #1 as finished 48 | ``` 49 | ```python 50 | dodo l 51 | > ID Status Date(-t) Owner(-u) Description (-d) 52 | > 1 [.] 1 minute ago (atm) New Task 53 | ``` 54 | 55 | ## Detailed Documentation 56 | 57 | ### List all Tasks 58 | ```python 59 | dodo l 60 | ``` 61 | 62 | ### Propose a new Task 63 | ```bash 64 | 65 | # simple version 66 | dodo add "This is a new task" 67 | 68 | # verbose version 69 | dodo c -u atmb4u -d "dodo new version" 70 | dodo add -u atmb4u -d "dodo new version" 71 | dodo propose -u atmb4u -d "dodo new version" 72 | ``` 73 | 74 | ### Accept a Tasks 75 | ```bash 76 | 77 | # simple version 78 | dodo accept 2 79 | 80 | # verbose version 81 | 82 | dodo accept --id 2 -u atmb4u -d "dodo new version" 83 | ``` 84 | 85 | ### Reject a proposed Tasks 86 | ```bash 87 | 88 | # simple version 89 | dodo reject 2 90 | 91 | # verbose version 92 | dodo reject --id 2 -u atmb4u 93 | ``` 94 | 95 | ### Work on a new Tasks 96 | ```bash 97 | 98 | # simple version 99 | dodo workon 2 100 | 101 | # verbose version 102 | dodo workon --id 2 -u atmb4u 103 | ``` 104 | 105 | ### Mark a task as Finished 106 | ```bash 107 | 108 | # simple version 109 | dodo finish 1 110 | 111 | # verbose version 112 | dodo finish --id 1 -u atmb4u -d "dodo new version" 113 | ``` 114 | 115 | ### Remove a Task 116 | ```bash 117 | dodo remove 1 118 | ``` 119 | 120 | ### Flush finished Tasks 121 | ```bash 122 | dodo flush 123 | # will remove all finished or rejected tasks 124 | ``` 125 | 126 | ### Export Tasks 127 | ```bash 128 | dodo export -o filename.json 129 | # will export all the tasks to filename.json 130 | # Can use --output as well 131 | 132 | dodo export 133 | # will print all the tasks in json format 134 | ``` 135 | 136 | ### Import Tasks 137 | ```bash 138 | dodo import -i filename.json 139 | # will import all the tasks from filename.json 140 | # Can use --input as well 141 | 142 | Sample Input File Format: [{"id":1, "description":"Read Docs Now", "entry":"20150405T020324Z", 143 | "status":"pending", "uuid":"1ac1893d-db66-40d7-bf67-77ca7c51a3fc","urgency":"0"}] 144 | ``` 145 | 146 | 147 | ## Author 148 | 149 | [atmb4u](https://github.com/atmb4u) 150 | 151 | ## Contributors 152 | 153 | [btnpushnmunky](https://github.com/btnpushnmunky) 154 | 155 | [jsnathan](https://github.com/jsnathan) 156 | 157 | [zeroSteiner](https://github.com/zeroSteiner) 158 | 159 | [legacy-code](https://github.com/legacy-code) 160 | 161 | 162 | Thanks to IanCal, GuyOnTheInterweb, elrac1, iambeard for the **super creative** suggestions in [reddit](http://www.reddit.com/r/coding/comments/2zgie7/dodo_task_management_for_developers/) 163 | 164 | 165 | 166 | ### Breaking Changes 167 | 168 | * Changed default filename in 1.0 to DODOPIE from DODO in 0.99 for Macintosh 169 | -------------------------------------------------------------------------------- /dodo: -------------------------------------------------------------------------------- 1 | dodo.py -------------------------------------------------------------------------------- /dodo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import calendar 4 | import json 5 | import re 6 | import time 7 | import os 8 | import sys 9 | from datetime import datetime 10 | from time import mktime 11 | 12 | 13 | DODO_FILE = os.path.join(os.getcwd(), 'DODOPIE') 14 | VERSION = "1.1" 15 | 16 | 17 | class TerminalColors(object): 18 | """ 19 | Color class for listing out dodos 20 | """ 21 | HEADER = '\033[95m' 22 | BLUE = '\033[94m' 23 | GREEN = '\033[92m' 24 | YELLOW = '\033[93m' 25 | RED = '\033[91m' 26 | BOLD = '\033[1m' 27 | UNDERLINE = '\033[4m' 28 | END = '\033[0m' 29 | 30 | def __init__(self): 31 | pass 32 | 33 | statuses = { 34 | '+': 'add', 35 | '*': 'accepted', 36 | '-': 'rejected', 37 | '#': 'working', 38 | '.': 'complete' 39 | } 40 | 41 | 42 | def pretty_date(date_string): 43 | timestamp = calendar.timegm((datetime.strptime(date_string, "%d-%m-%y %H:%M")).timetuple()) 44 | date = datetime.fromtimestamp(timestamp) 45 | diff = datetime.now() - date 46 | s = round(diff.seconds, 2) 47 | if diff.days > 7 or diff.days < 0: 48 | return date.strftime('%d %b %y') 49 | elif diff.days == 1: 50 | return '1 day ago' 51 | elif diff.days > 1: 52 | return '{} days ago'.format(diff.days) 53 | elif s <= 1: 54 | return 'just now' 55 | elif s < 60: 56 | return '{} seconds ago'.format(s) 57 | elif s < 120: 58 | return '1 minute ago' 59 | elif s < 3600: 60 | return '{} minutes ago'.format(round(s/60, 1)) 61 | elif s < 7200: 62 | return '1 hour ago' 63 | else: 64 | return '{} hours ago'.format(round(s/3600, 1)) 65 | 66 | 67 | def parse_dodo(line): 68 | if line: 69 | do_id = re.search("#\d+", line).group()[1:] 70 | do_status = re.search(r'\[\[\W+\]\]', line).group()[2:-2] 71 | do_time = re.search(r'(<<.+>>)', line) 72 | do_description = re.search(r'({{.+}})', line) 73 | if do_time: 74 | do_time = do_time.group().replace("<<", "").replace(">>", "") 75 | do_user = re.search(r'(\(\(.+\)\))', line) 76 | if do_user: 77 | do_user = do_user.group().replace("((", "").replace("))", "") 78 | if do_description: 79 | do_description = do_description.group().replace("{{", "").replace("}}", "") 80 | return { 81 | "id": do_id, 82 | "time": do_time, 83 | "user": do_user, 84 | "status": do_status, 85 | "description": do_description 86 | } 87 | 88 | 89 | def dodo_load(args): 90 | global DODO_FILE 91 | do_dict = {} 92 | DODO_FILE = args.file or DODO_FILE 93 | with open(DODO_FILE, 'r') as file_inst: 94 | contents = file_inst.readlines() 95 | for content in contents: 96 | do_data = parse_dodo(content) 97 | do_dict.update({do_data["id"]: do_data}) 98 | return do_dict 99 | 100 | 101 | def dodo_unload(final_do_base): 102 | content = "" 103 | for key, value in sorted(iter(final_do_base.items()), key=lambda key_value: int(key_value[0])): 104 | content += "#%s [[%s]] <<%s>> ((%s)) {{%s}}\n" % (value["id"], value["status"], value["time"], 105 | value["user"], value["description"]) 106 | dodo_write(content, "w") 107 | 108 | 109 | def dodo_init(args): 110 | file_name = args.file or DODO_FILE 111 | try: 112 | try: 113 | open(file_name, "r") 114 | print("DoDo already exist.") 115 | except IOError: 116 | file_inst = open(file_name, "w") 117 | file_inst.close() 118 | print("Successfully initialized DoDo") 119 | except IOError: 120 | print("Cannot create file in the following location: %s" % file_name) 121 | 122 | 123 | def dodo_write(content, mode="a"): 124 | global DODO_FILE, do_base 125 | file_inst = open(DODO_FILE, mode) 126 | file_inst.write(content) 127 | file_inst.close() 128 | dodo_list() 129 | 130 | 131 | def dodo_new_id (): 132 | if len (do_base) == 0: 133 | return "1" 134 | else: 135 | return str(max(int(id) for id in do_base.keys()) + 1) 136 | 137 | 138 | def dodo_change_status(args, mod_do_base, status): 139 | if not args.id: 140 | print("ID (-id) can't be empty. May be try creating the task first") 141 | return 142 | do_entry = mod_do_base.get(args.id) 143 | if do_entry: 144 | do_entry["status"] = status 145 | if args.desc: 146 | do_entry["description"] = args.desc 147 | if args.user: 148 | do_entry["user"] = args.user 149 | if args.time: 150 | do_entry["time"] = args.time 151 | else: 152 | if not args.desc: 153 | print("Description (-d) can't be empty") 154 | return 155 | do_id = dodo_new_id () 156 | do_description = args.desc 157 | do_user = args.user 158 | do_time = args.time or time.strftime("%d-%m-%y %H:%M", time.gmtime()) 159 | mod_do_base[do_id] = { 160 | "id": do_id, 161 | "time": do_time, 162 | "user": do_user, 163 | "status": status, 164 | "description": do_description 165 | } 166 | dodo_unload(mod_do_base) 167 | return 168 | 169 | 170 | def dodo_add(args): 171 | """ 172 | + add/proposed 173 | * accepted 174 | - rejected 175 | # working 176 | . complete 177 | """ 178 | do_user = args.user 179 | if args.operation in ["add", "propose", "c"]: 180 | if args.id: 181 | print("Error: DoDo assigns id for you.") 182 | exit() 183 | do_id = dodo_new_id () 184 | do_description = args.desc 185 | do_time = args.time or time.strftime("%d-%m-%y %H:%M", time.gmtime()) 186 | do_base[do_id] = { 187 | "id": do_id, 188 | "time": do_time, 189 | "user": do_user, 190 | "status": "+", 191 | "description": do_description 192 | } 193 | dodo_unload(do_base) 194 | elif args.operation == "accept": 195 | dodo_change_status(args, do_base, "*") 196 | elif args.operation == "reject": 197 | dodo_change_status(args, do_base, "-") 198 | elif args.operation == "workon": 199 | dodo_change_status(args, do_base, "#") 200 | elif args.operation == "finish": 201 | dodo_change_status(args, do_base, ".") 202 | elif args.operation in ["remove" or "d"]: 203 | try: 204 | do_base.pop(args.id) 205 | except KeyError: 206 | print("No task with id %s" % args.id) 207 | dodo_unload(do_base) 208 | elif args.operation == "flush": 209 | for do_entry in list(do_base.values()): 210 | if do_entry["status"] in ["-", "."]: 211 | do_base.pop(do_entry["id"]) 212 | dodo_unload(do_base) 213 | return 214 | 215 | 216 | def dodo_list(): 217 | global do_base 218 | print("%s%sID\tStatus\t\tDate(-t)\tOwner(-u)\t\tDescription (-d)\n%s" % (TerminalColors.BOLD, 219 | TerminalColors.UNDERLINE, 220 | TerminalColors.END)) 221 | for key, value in sorted(iter(do_base.items()), key=lambda key_value1: int(key_value1[0])): 222 | color = TerminalColors.YELLOW 223 | if value["status"] == ".": 224 | color = TerminalColors.GREEN 225 | elif value["status"] in ["-", 'x']: 226 | color = TerminalColors.RED 227 | elif value["status"] == "#": 228 | color = TerminalColors.UNDERLINE + TerminalColors.YELLOW 229 | elif value["status"] == "+": 230 | color = TerminalColors.BLUE 231 | user = value["user"] if value["user"] != "None" else "anonymous" 232 | human_time = pretty_date(value["time"]) 233 | print("%s%s\t[%s]\t\t%s\t(%s)\t\t%s%s" % (color, value["id"], value["status"], human_time, 234 | user, value["description"], TerminalColors.END)) 235 | print("\n%sAvailable Operations: c accept propose reject workon finish remove d flush\n" \ 236 | "Available Options: -id -d(description) -u(user) -t(time) -f(file)\n" \ 237 | "Status: + proposed - rejected * accepted # working . complete%s" % ( 238 | TerminalColors.BOLD, TerminalColors.END)) 239 | 240 | 241 | def dodo_import(args): 242 | """ 243 | Sample import JSON format (same as taskwarrior export format) 244 | {"id":1,"description":"Read Docs Now","entry":"20150405T020324Z","status":"pending", 245 | "uuid":"1ac1893d-db66-40d7-bf67-77ca7c51a3fc","urgency":"0"} 246 | """ 247 | do_user = args.user 248 | json_file = args.input 249 | json_source = json.loads(open(json_file).read()) 250 | for task in json_source: 251 | do_id = dodo_new_id () 252 | do_description = task["description"] 253 | utc_time = time.strptime(task["entry"], "%Y%m%dT%H%M%S%fZ") 254 | do_time = time.strftime("%d-%m-%y %H:%M", utc_time) 255 | do_status = "+" 256 | if task["status"] == "pending": 257 | do_status = "+" 258 | if task["status"] == "completed": 259 | do_status = "." 260 | do_base[do_id] = { 261 | "id": do_id, 262 | "time": do_time, 263 | "user": do_user, 264 | "status": do_status, 265 | "description": do_description 266 | } 267 | dodo_unload(do_base) 268 | print("Imported %d tasks successfully" % len(json_source)) 269 | 270 | 271 | def dodo_export(args): 272 | """ 273 | {"id":1,"description":"Read Docs Now","entry":"20150405T020324Z","status":"pending", 274 | "uuid":"1ac1893d-db66-40d7-bf67-77ca7c51a3fc","urgency":"0"} 275 | Time is in UTC 276 | """ 277 | dodo_data = [] 278 | for instance in sorted(list(do_base.values()), key=lambda value: int(value["id"])): 279 | dodo_data.append({ 280 | "id": instance["id"], 281 | "time": instance["time"], 282 | "user": instance["user"], 283 | "status": statuses[instance["status"]], 284 | "description": instance["description"] 285 | } 286 | ) 287 | if args.output: 288 | try: 289 | file_name = args.output 290 | file_inst = open(file_name, "w") 291 | file_inst.write(json.dumps(dodo_data)) 292 | file_inst.close() 293 | print("%sExported DODO to %s%s" % \ 294 | (TerminalColors.GREEN, file_name, TerminalColors.END)) 295 | except IOError: 296 | print("%sExport failed; Check for permission to create/edit %s%s" % \ 297 | (TerminalColors.RED, args.output, TerminalColors.END)) 298 | else: 299 | print("%sUse -e or --export to to export to a file.%s" % \ 300 | (TerminalColors.YELLOW, TerminalColors.END)) 301 | print("%s" % TerminalColors.GREEN) 302 | print(dodo_data) 303 | print("%s" % TerminalColors.END) 304 | 305 | 306 | def dodo_switch(args): 307 | global do_base 308 | if args.operation == "init": 309 | dodo_init(args) 310 | elif args.operation in ['add', 'propose', 'accept', 'reject', 'workon', 'finish', 'flush', 'remove', "c", "d"]: 311 | dodo_add(args) 312 | elif args.operation == 'import': 313 | dodo_import(args) 314 | elif args.operation == 'export': 315 | dodo_export(args) 316 | else: 317 | dodo_list() 318 | 319 | 320 | if __name__ == "__main__": 321 | default_operation = 'list' 322 | default_user = os.path.split(os.path.expanduser('~'))[-1] 323 | parser = argparse.ArgumentParser() 324 | parser.add_argument("operation", nargs='?', default=default_operation, 325 | choices=[ 326 | 'init', 327 | 'accept', 328 | 'add', 329 | 'finish', 330 | 'flush', 331 | 'list', 332 | 'propose', 333 | 'reject', 334 | 'remove', 335 | 'workon', 336 | 'c', 337 | 'd', 338 | 'import', 339 | 'export' 340 | ], 341 | help="The operation to perform") 342 | parser.add_argument("quick_access", nargs='?', default='', 343 | help="Task ID for a operation or Description for the new task") 344 | parser.add_argument("-d", "--desc", "--description", 345 | help="Task Description") 346 | parser.add_argument("-u", "--user", default=default_user, help="User ID") 347 | parser.add_argument("-t", "--time", 348 | help="Expected/Completed Date - 11-03-2015") 349 | parser.add_argument("--id", help="List all existing dodos") 350 | parser.add_argument("-f", "--file", help="DODO filename") 351 | parser.add_argument("-i", "--input", help="Import from JSON file") 352 | parser.add_argument("-o", "--output", help="Export to JSON file") 353 | arguments = parser.parse_args() 354 | 355 | if (arguments.operation == default_operation 356 | and not os.path.isfile(arguments.file or DODO_FILE)): 357 | parser.print_help() 358 | sys.exit(0) 359 | 360 | quick_access = arguments.quick_access 361 | if quick_access: 362 | if arguments.quick_access.isdigit(): 363 | arguments.id = quick_access 364 | elif quick_access: 365 | arguments.desc = quick_access 366 | global do_base 367 | do_base = {} 368 | if arguments.operation == "init": 369 | dodo_init(arguments) 370 | else: 371 | do_base = dodo_load(arguments) 372 | dodo_switch(arguments) 373 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmb4u/dodo/1ce6426794e7e5a237020879570b012c5d436349/logo.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Dodo setup.py script 2 | # 3 | # It doesn't depend on setuptools, but if setuptools is available it'll use 4 | # some of its features, like package dependencies. 5 | 6 | from distutils.command.install_data import install_data 7 | from distutils.command.install import INSTALL_SCHEMES 8 | import os 9 | import sys 10 | 11 | 12 | class OsxInstallData(install_data): 13 | # On MacOS, the platform-specific lib dir is /System/Library/Framework/Python/.../ 14 | # which is wrong. Python 2.5 supplied with MacOS 10.5 has an Apple-specific fix 15 | # for this in distutils.command.install_data#306. It fixes install_lib but not 16 | # install_data, which is why we roll our own install_data class. 17 | 18 | def finalize_options(self): 19 | # By the time finalize_options is called, install.install_lib is set to the 20 | # fixed directory, so we set the installdir to install_lib. The 21 | # install_data class uses ('install_data', 'install_dir') instead. 22 | self.set_undefined_options('install', ('install_lib', 'install_dir')) 23 | install_data.finalize_options(self) 24 | 25 | 26 | if sys.platform == "darwin": 27 | cmdclasses = {'install_data': OsxInstallData} 28 | else: 29 | cmdclasses = {'install_data': install_data} 30 | 31 | 32 | def full_split(path, result=None): 33 | """ 34 | Split a pathname into components (the opposite of os.path.join) in a 35 | platform-neutral way. 36 | """ 37 | if result is None: 38 | result = [] 39 | head, tail = os.path.split(path) 40 | if head == '': 41 | return [tail] + result 42 | if head == path: 43 | return result 44 | return full_split(head, [tail] + result) 45 | 46 | # Tell distutils to put the data_files in platform-specific installation 47 | # locations. See here for an explanation: 48 | # http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb 49 | for scheme in list(INSTALL_SCHEMES.values()): 50 | scheme['data'] = scheme['purelib'] 51 | 52 | # Compile the list of packages available, because distutils doesn't have 53 | # an easy way to do this. 54 | packages, data_files = [], [] 55 | root_dir = os.path.dirname(__file__) 56 | if root_dir != '': 57 | os.chdir(root_dir) 58 | 59 | 60 | def is_not_module(filename): 61 | return os.path.splitext(filename)[1] not in ['.py', '.pyc', '.pyo'] 62 | 63 | scripts = ['dodo', 'dodo.py'] 64 | 65 | 66 | setup_args = { 67 | 'name': 'dodopie', 68 | 'version': '1.1', 69 | 'url': 'http://atmb4u.github.io/dodo', 70 | 'description': 'Task Management for Hackers', 71 | 'author': 'Anoop Thomas Mathew', 72 | 'author_email': 'atmb4u@gmail.com', 73 | 'license': 'BSD', 74 | 'packages': packages, 75 | 'cmdclass': cmdclasses, 76 | 'data_files': data_files, 77 | 'scripts': scripts, 78 | 'include_package_data': True, 79 | 'classifiers': [ 80 | 'Programming Language :: Python', 81 | 'Programming Language :: Python :: 2.7', 82 | 'License :: OSI Approved :: BSD License', 83 | 'Operating System :: OS Independent', 84 | 'Intended Audience :: Developers', 85 | 'Environment :: Console', 86 | 'Topic :: Software Development :: Libraries :: Python Modules' 87 | ] 88 | } 89 | 90 | try: 91 | from setuptools import setup 92 | except ImportError: 93 | from distutils.core import setup 94 | setup(**setup_args) 95 | --------------------------------------------------------------------------------