Creating CLI tool in Python - part 4
In a previous post, I added an image option to blog command, did some refactoring and
added some unit tests. I this post I'm going to do some more refactoring, to come up with a better project structure, add additional tasks and try to get it all together so that I could start adding more commands.
Probably this will be my last post about this tool, from now on I will extend it if I have some task that can be automated.
Overview
The current version of assistant contains quite a lot of structural changes, I moved files from one place to another, refactored a lot of code and added additional tests, all for a better future.
Blog command got a git task - now if I run the command it also creates a separate branch and makes an initial commit to it with starter file and image included. So that I can easily just jump into my Visual Studio and start writing a new blog post. This same post is also made using an assistant blog command, cool right?
This is a current flow of a blog command:
When runnind tool on command line it gives now a really good overview what tasks are running and how many left.
After running this command I can see new branch:
And I can see a initial commit for a new post:
Project structure
1assistant/2├── README.md3├── assistant4│ ├── __init__.py5│ ├── app.py6│ ├── commands7│ │ ├── __init__.py8│ │ └── blog9│ │ ├── __init__.py10│ │ ├── blog_command.py11│ │ ├── dto12│ │ │ └── image_dto.py13│ │ └── tasks14│ │ ├── __init__.py15│ │ ├── commit_push_changes.py16│ │ ├── create_new_branch.py17│ │ ├── create_starter_file.py18│ │ ├── download_img.py19│ │ ├── reguest_img_data.py20│ │ ├── validate_image.py21│ │ ├── validate_project_path.py22│ │ └── validate_title.py23│ ├── common24│ │ ├── file_handler.py25│ │ ├── logger.py26│ │ └── str_helper.py27│ └── dto28│ ├── __init__.py29│ └── config_dto.py30├── install-dev.sh31├── setup.py32└── tests33 ├── commands34 │ └── blog35 │ └── tasks36 │ ├── __init__.py37 │ ├── test_create_new_branch.py38 │ ├── test_create_starter_file.py39 │ ├── test_download_img.py40 │ ├── test_request_img_data.py41 │ ├── test_validate_image.py42 │ ├── test_validate_project_path.py43 │ └── test_validate_title.py44 └── common45 ├── __init__.py46 └── test_str_helper.py
Each command will now be separated into their folder and each command will have entry file (for example blog_command.py), data transfer object and tasks.
I'm just going to point out here changes made in blog command, rest is the same and can be found in GitHub (link under resources).
blog_command.py
1from assistant.common import logger2from assistant.commands.blog.tasks.download_img import DownloadImg3from assistant.commands.blog.tasks.validate_image import ValidateImage4from assistant.commands.blog.tasks.validate_title import ValidateTitle5from assistant.commands.blog.tasks.reguest_img_data import RequestImageData6from assistant.commands.blog.tasks.create_new_branch import CreateNewBranch7from assistant.commands.blog.tasks.create_starter_file import CreateStarterFile8from assistant.commands.blog.tasks.commit_push_changes import CommitPushChanges9from assistant.commands.blog.tasks.validate_project_path import ValidateProjectPath1011def handle(config, title, img_url, project_path):12 try:13 image = {}14 tasks = [15 ValidateProjectPath(project_path),16 ValidateTitle(title),17 ValidateImage(img_url),18 RequestImageData(img_url, title, config),19 CreateNewBranch(title, project_path),20 DownloadImg(project_path, img_url, title),21 CreateStarterFile(title, project_path),22 CommitPushChanges(project_path, title)23 ]2425 num_of_tasks = len(tasks)26 i = 12728 for task in tasks:29 logger.info(config.verbose, task.start_message)3031 # RequestImageData returns multiple result and image object.32 # Image object is later used for creating a template.33 if (type(task).__name__ == 'RequestImageData'):34 result, image = task.execute()35 elif (type(task).__name__ == 'CreateStarterFile'):36 result = task.execute(image)37 else:38 result = task.execute()3940 message = "[%i/%i] %s" % (i, num_of_tasks, result)41 logger.success(message)42 i+=14344 except ValueError as er:45 logger.error('Validation Error: {}'.format(er))46 except Exception as ex:47 logger.error(format(ex))
When comparing blog command with the previous postcode, then it's now more readable.
Each task is now a separate class and is registered to a task list. This helps me to count all processes and give numeric feedback to the user how many tasks are done and how many to go.
Each task is then called using execute()
method.
Summary
In conclusion, this was a really fun and useful project. I learned quite a lot about Python and unit testing. I would that now the main structure for this tool is created it should be quite easy to add new commands. So now I need to just come up with an idea what to automate :)