Creating CLI tool in Python - part 4

Photo by: Franck V. / Unsplash

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

Copy
1assistant/
2├── README.md
3├── assistant
4│ ├── __init__.py
5│ ├── app.py
6│ ├── commands
7│ │ ├── __init__.py
8│ │ └── blog
9│ │ ├── __init__.py
10│ │ ├── blog_command.py
11│ │ ├── dto
12│ │ │ └── image_dto.py
13│ │ └── tasks
14│ │ ├── __init__.py
15│ │ ├── commit_push_changes.py
16│ │ ├── create_new_branch.py
17│ │ ├── create_starter_file.py
18│ │ ├── download_img.py
19│ │ ├── reguest_img_data.py
20│ │ ├── validate_image.py
21│ │ ├── validate_project_path.py
22│ │ └── validate_title.py
23│ ├── common
24│ │ ├── file_handler.py
25│ │ ├── logger.py
26│ │ └── str_helper.py
27│ └── dto
28│ ├── __init__.py
29│ └── config_dto.py
30├── install-dev.sh
31├── setup.py
32└── tests
33 ├── commands
34 │ └── blog
35 │ └── tasks
36 │ ├── __init__.py
37 │ ├── test_create_new_branch.py
38 │ ├── test_create_starter_file.py
39 │ ├── test_download_img.py
40 │ ├── test_request_img_data.py
41 │ ├── test_validate_image.py
42 │ ├── test_validate_project_path.py
43 │ └── test_validate_title.py
44 └── common
45 ├── __init__.py
46 └── 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

Copy
1from assistant.common import logger
2from assistant.commands.blog.tasks.download_img import DownloadImg
3from assistant.commands.blog.tasks.validate_image import ValidateImage
4from assistant.commands.blog.tasks.validate_title import ValidateTitle
5from assistant.commands.blog.tasks.reguest_img_data import RequestImageData
6from assistant.commands.blog.tasks.create_new_branch import CreateNewBranch
7from assistant.commands.blog.tasks.create_starter_file import CreateStarterFile
8from assistant.commands.blog.tasks.commit_push_changes import CommitPushChanges
9from assistant.commands.blog.tasks.validate_project_path import ValidateProjectPath
10
11def 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 ]
24
25 num_of_tasks = len(tasks)
26 i = 1
27
28 for task in tasks:
29 logger.info(config.verbose, task.start_message)
30
31 # 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()
39
40 message = "[%i/%i] %s" % (i, num_of_tasks, result)
41 logger.success(message)
42 i+=1
43
44 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 :)

Available resources