<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: dmikhr</title>
    <description>The latest articles on DEV Community by dmikhr (@dmikhr).</description>
    <link>https://dev.to/dmikhr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F949031%2Fd5c72c8a-cc7d-4034-ae79-0b23017ada09.png</url>
      <title>DEV Community: dmikhr</title>
      <link>https://dev.to/dmikhr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmikhr"/>
    <language>en</language>
    <item>
      <title>A Step-by-Step Tutorial for Writing a Bash Installer Script for Your App</title>
      <dc:creator>dmikhr</dc:creator>
      <pubDate>Mon, 26 May 2025 09:33:47 +0000</pubDate>
      <link>https://dev.to/dmikhr/step-by-step-bash-installer-script-tutorial-55dl</link>
      <guid>https://dev.to/dmikhr/step-by-step-bash-installer-script-tutorial-55dl</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Consider the situation: you've spent a considerable amount of time developing an app, either as a personal project or as part of your job, and now it's time to share it with your target audience.&lt;/p&gt;

&lt;p&gt;Note: here I focus mostly on console apps running on 🐧 Linux and 🍎 MacOS.&lt;/p&gt;

&lt;p&gt;CLI apps can be executed either from a directory where the binary is located and it is fine at the development stage or if you share your app with your team, but as an end user, you want the app usage to be as straightforward as possible. Recently, I developed an app for extracting images from PDF documents &lt;strong&gt;&lt;a href="https://github.com/dmikhr/pdfjuicer" rel="noopener noreferrer"&gt;pdfjuicer&lt;/a&gt;&lt;/strong&gt; which was intended for both manual usage and being integrated into data processing pipelines.&lt;/p&gt;

&lt;p&gt;Calling it like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/Users/qwe/code/cli_apps/pdfjuicer/bin/pdfjuicer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not a convenient way to run the app. I want users to be able to call the app just like any others, simply typing its name in the terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pdfjuicer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, it is convenient both for users and for calling the app from other software without bothering about the app location.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem analysis
&lt;/h2&gt;

&lt;p&gt;Making the app run simply by calling its name in a terminal is achieved by adding your app path to the environment PATH. You can also add your app to software repositories like apt repositories, homebrew, etc. However, especially at the first stages it might be an overwhelming thing to do, and you might want to strike a balance between installation convenience and ease of solution maintainability. Here, I'll explore the option of developing a custom installation script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2vm7wmydg4s2m4hxh12.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2vm7wmydg4s2m4hxh12.png" alt="Procedure" width="542" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, here we need to look at the situation on a larger scale and consider the following questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How tech-savvy is our audience?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is providing manual instructions enough?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What are the installation steps?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If choosing automated installation, should the whole process of installation be automated?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can we test the installation process before actually installing an app?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What tool or language should be used for automation?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these questions naturally arise one from another. First, we need to understand how complex the process is. We need to answer the question of whether our target audience has enough expertise to follow manual instructions or automation is needed. If an automated route is taken, maybe not all steps should be automated, giving user options or protecting user from unintended changes in system settings. And finally, if something goes wrong, can we find potential issues in advance by performing a test installation?&lt;/p&gt;

&lt;h2&gt;
  
  
  Working out the plan 🔧
&lt;/h2&gt;

&lt;p&gt;So my next step was to get answers to these questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;How tech-savvy is our audience?&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;You might think that users of console apps are mostly tech savvy, however, it is more like a spectrum. Consider CLI apps like &lt;code&gt;ffmpeg&lt;/code&gt; or &lt;code&gt;imagemagic&lt;/code&gt; which can be used by a wide range of users doing tasks like converting videos to certain formats or resizing photos from family photoarchive. I my case &lt;strong&gt;pdfjuicer&lt;/strong&gt; was made for users who need to extract pages from PDF files as images with custom image size, format and thumbnails generation. The major target audience are educators and content creators. Users in these categories do not always want or have time for digging into the intricacies of application setup. Even if we consider users like devops engineers and software engineers, they also will appreciate quick and easy ways to install an app rather than delving into documentation and figuring out how to do it themselfes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;Is providing manual instructions enough?&lt;/em&gt; 📄
&lt;/h3&gt;

&lt;p&gt;This question naturally arises from the previous one and my decision was not to go with the manual route. Even many tech-savvy users will prefer to save time by using a ready automated solution instead of installing the app manually or developing their own automation script. However, by taking the route which involves automated installation script, there is a problem of the installation process being hidden, which might concern some users: What will happen after installation? Where the app will be installed?&lt;/p&gt;

&lt;p&gt;Solution is to print messages during installation showing which steps are performed now with additional information like file and folder names which are involved in the installation process, script status (start, finish, error); also adding test mode or &lt;code&gt;dry-run&lt;/code&gt; mode will introduce additional layer of comfort and safety for a user avoiding concerns like &lt;em&gt;What will actually happen after I'll run the script?&lt;/em&gt; and answers our question &lt;strong&gt;Can we test installation process before actually installing an app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also user can check the source code of an installation script to check the process in more detail. And finally, making script output formatted with &lt;a href="[ANSI%20escape%20code%20-%20Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code)"&gt;ANSI Escape Codes&lt;/a&gt; will add more clarity to output messages, for example, making diagnostic information green and errors red.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Installation steps&lt;/em&gt; 💻
&lt;/h2&gt;

&lt;p&gt;First, let's lay down steps which must be performed to install the CLI app with brief considerations for each step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find binary of an app&lt;/strong&gt; - can be done by entering a path to the binary as a parameter to the installation script or app can be put into the same directory with the installation script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transfer binary to bin directory&lt;/strong&gt; - at this step 2 new questions arise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Transfer binary to system &lt;code&gt;bin&lt;/code&gt; directory or in user-specific &lt;code&gt;~/bin&lt;/code&gt;?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move binary or copy to &lt;code&gt;bin&lt;/code&gt; directory?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's address both.&lt;/p&gt;

&lt;p&gt;Since this app is not installed from official repositories and can be considered as custom software, user-specific &lt;code&gt;~/bin&lt;/code&gt; seems like a preferable choice. It will also avoid permissions issues that might arise when installing in the system &lt;code&gt;bin&lt;/code&gt; and potential conflicts of overwriting binaries there. Overall, in this case, custom &lt;code&gt;~/bin&lt;/code&gt; simplifies app management and avoids risks of interfering with operating system.&lt;/p&gt;

&lt;p&gt;Regarding whether it is better to move or copy binary - better to choose copying. In this way, if installation fails, it can be started again, otherwise, if the binary was moved, user will need to take the binary from &lt;code&gt;~/bin&lt;/code&gt; and put it into an original folder or download it again to retry the installation process.&lt;/p&gt;

&lt;p&gt;Also, going with user-specific &lt;code&gt;~/bin&lt;/code&gt; directory creates &lt;strong&gt;extra optional step&lt;/strong&gt; of creating such a directory if I did not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set permissions to binary&lt;/strong&gt; - binary should be executable and execution permission will be limited to its owner since the app is installed in user user-specific binary directory, which can be achieved by using &lt;code&gt;chmod u+x app_binary&lt;/code&gt; which makes user (owner) &lt;code&gt;u&lt;/code&gt; right to execute &lt;code&gt;x&lt;/code&gt; the binary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding binary to PATH&lt;/strong&gt; - this step makes app executable by simply calling it by name in terminal. To achieve that path to app must be added to shell configuration file (.bashrc, .bash_profile, .profile or .zshrc) using &lt;code&gt;export&lt;/code&gt; command &lt;code&gt;export PATH="$PATH:$target_bin_dir"&lt;/code&gt;. In order to apply changes user will need to &lt;strong&gt;reload terminal&lt;/strong&gt; which can be done by executing &lt;code&gt;source ~/.bashrc&lt;/code&gt; (or targeting another appropriate file for particular user's shell config).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;Should the whole process of installation be automated?&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Here it is crucial to focus on steps which have the most uncertainty in the produced outcome and risk of interfering with the operation system functioning and let the user manage these steps. In this installation process, this is the last step of adding the app to PATH. It is better to allow user to edit shell config file manually, choosing where to add new path - precede other paths or make it the last one. It will also help to avoid risks of potential overwriting of existing settings or duplicating settings or writing into the wrong shell configuration file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tool or language should be used for automation?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5czyqo4cg0r1ykeilt3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5czyqo4cg0r1ykeilt3.jpg" alt="Choose language" width="768" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To make installation compatible among Linux, Mac OS and BSD systems, it makes sense to implement installation using &lt;code&gt;bash&lt;/code&gt; script. However, my app is intended to be used not only by users but also by other software with scenarios of the app being installed on VPS or in Docker containers, including lightweight containers based on operating systems like &lt;strong&gt;Alpine Linux&lt;/strong&gt;, which provides only &lt;code&gt;sh&lt;/code&gt; but not &lt;code&gt;bash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, such an approach will improve overall compatibility across different operating systems and their distributives. However, it means that while implementing the installation script, it is important to be mindful about using syntax that is supported in &lt;code&gt;sh&lt;/code&gt; and avoid &lt;code&gt;bash&lt;/code&gt; specific syntax, which might mean potential tradeoffs like certain parts of code being more verbose.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about other scripting languages?
&lt;/h3&gt;

&lt;p&gt;One can ask that there are other options for scripting languages which will get this job done, like &lt;strong&gt;Python, Ruby or Lua&lt;/strong&gt; and in terms of syntax, they will make the script easier to develop and easier to maintain if in the future it will require upgrades and extensions. However, we will pay the price of portability complexity. Choosing &lt;code&gt;sh&lt;/code&gt; (Bourne shell) allows us to be as much agnostic about user's operating system environment as possible. If we choose &lt;code&gt;bash&lt;/code&gt;, we add a layer of uncertainty: what if the host operating system has only &lt;code&gt;sh&lt;/code&gt; installed?&lt;/p&gt;

&lt;p&gt;With other scripting languages like Python, Ruby or Lua uncertainty rises even higher. Now we need to take care of interpreter version (Python 2 and 3), guess wether chosen scripting language is installed on the user's machine at all, manage situations like installing scripting language if it is not presented in operating system which will significantly increase installation complexity like extra installation steps, longer installation time, need to download additional packages, more risks of things go wrong due to extra steps in installation.&lt;/p&gt;

&lt;p&gt;So in this case my choice is &lt;code&gt;sh&lt;/code&gt; - the most available scripting language on UNIX-like systems, including minimal distributives used in Docker containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation 🚀
&lt;/h2&gt;

&lt;p&gt;Add ANSI escape codes for formatting messages&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[0m"&lt;/span&gt;
&lt;span class="nv"&gt;bold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[1m"&lt;/span&gt;
&lt;span class="nv"&gt;color_green&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[32m"&lt;/span&gt;
&lt;span class="nv"&gt;color_red&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;33[31m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this script we will use green and red colors, bold font and also reset code that will return output format to default.&lt;/p&gt;

&lt;p&gt;Red color will be used to notify user that installation script is running in testing mode when no actual operations are performed. Green will be used to print message about installation success. Bold text will be used in large chunks of text to highlights key moments.&lt;/p&gt;

&lt;p&gt;Set work directory&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;workdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multiple flags&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since script has 2 flags &lt;code&gt;--dev&lt;/code&gt; and &lt;code&gt;--dry-run&lt;/code&gt; and they can be used either both or just one of them and in any order to check for current flag the following construction will be used which will check whether target flag is 1st or the 2nd argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--flag"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--flag"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    ...
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dev flag&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In addition to regular installation, script supports another mode which is activated by &lt;code&gt;--dev&lt;/code&gt; flag and intended to be used when you compile app from source.&lt;br&gt;
App was written in Go and project contains bin directory where compiled binary is stored. In regular mode, installation script expects binary to be in the same directory with script, or in case of calling install.sh from source code (script is located in the root of the project) -- dev flag modifies path to binary adding current workdir/bin as folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--dev"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--dev"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;binary_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bin/pdfjuicer"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;binary_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pdfjuicer"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dry-run&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check if this is a test/dry run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--dry-run"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"--dry-run"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;bold&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;color_red&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;This is dry run, no actual actions will be performed. Use this for testing installation script.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;reset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If dry run is enabled show message about it in red color and with &lt;strong&gt;bold&lt;/strong&gt; color using defined previously ANSI escape codes.&lt;/p&gt;

&lt;p&gt;Setting paths to &lt;strong&gt;target&lt;/strong&gt; path where it will be copied (&lt;code&gt;~/bin&lt;/code&gt;) and original binary location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;target_bin_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/bin/"&lt;/span&gt;

&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$workdir&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$binary_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;~/bin&lt;/code&gt; directory if it doesn't exist&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating directory &lt;/span&gt;&lt;span class="nv"&gt;$target_bin_dir&lt;/span&gt;&lt;span class="s2"&gt; for binary (if not exist)"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$dry_run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target_bin_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy binary to &lt;code&gt;~/bin&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Copying &lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$target_bin_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$dry_run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target_bin_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add execute permission using &lt;code&gt;chmod&lt;/code&gt;, in the message to user script will show user name who will be able to execute binary using &lt;code&gt;whoami&lt;/code&gt; command. If everything is running correctly it will show name of a user running the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Adding execute permission for user &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$dry_run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;u+x &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target_bin_dir$binary_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally installation script will show instructions for the final step which user will do manually: adding binary to PATH.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling installation script
&lt;/h2&gt;

&lt;p&gt;The install script has 2 modes of running: real installation and test (dry-run) when the user sees messages for each step of an installation process but no actual operations are performed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# testing script&lt;/span&gt;
bash ./install.sh &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# actual installation&lt;/span&gt;
bash ./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in case of installing compiled binary from source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# testing script&lt;/span&gt;
bash ./install.sh &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# actual installation from &lt;/span&gt;
bash ./install.sh &lt;span class="nt"&gt;--dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Such practice enables user to check if script is working properly: binary is found in correct folder, it will be copied to correct path, script doesn't fail during the execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Shipping app with installation script simplifies app installation compared to providing manual instructions. At the same time it's important to identify steps which are better to be performed manually for installation safety reasons. In this case user will manually add app to PATH to avoid potential mistakes and conflicts in shell configuration. Having an installation script provides a balance between manual setup and adding app to official repositories which can be time consuming especially at early stages of app development or when app is developed for internal usage in a company.&lt;/p&gt;

&lt;p&gt;Complete source code of installation script can be found in &lt;a href="https://github.com/dmikhr/pdfjuicer" rel="noopener noreferrer"&gt;pdfjuicer&lt;/a&gt; project here: &lt;a href="https://github.com/dmikhr/pdfjuicer/blob/main/install.sh" rel="noopener noreferrer"&gt;&lt;strong&gt;install.sh&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>programming</category>
      <category>bash</category>
    </item>
    <item>
      <title>Extend Python VENV: Organize Dependencies Your Way</title>
      <dc:creator>dmikhr</dc:creator>
      <pubDate>Thu, 11 May 2023 12:49:42 +0000</pubDate>
      <link>https://dev.to/dmikhr/extend-python-venv-organize-dependencies-your-way-3h0g</link>
      <guid>https://dev.to/dmikhr/extend-python-venv-organize-dependencies-your-way-3h0g</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;Virtual environments are great way to organise development process by isolating project specific packages. In such way Python has a built-in tool &lt;code&gt;venv&lt;/code&gt; for creating virtual environments. In this tutorial we are going to explore how to extend its functionality by implementing a feature that stores information about production and development related packages in separate requirements file. &lt;/p&gt;

&lt;p&gt;This tutorial will guide you how to implement such feature, explaining logic behind the solution and purpose of each function in the script. Prerequisites for this tutorial are basic knowledge of Python and experience with virtual environments. Examples from this tutorial requires Python 3.5+.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools
&lt;/h3&gt;

&lt;p&gt;There are different ways to split dependencies in Python. &lt;a href="https://python-poetry.org/docs/master/managing-dependencies/" rel="noopener noreferrer"&gt;Poetry&lt;/a&gt; provides versatile options to configure Python app including staging: production, development and testing. It also provides a lot of other useful options for Python developers. &lt;/p&gt;

&lt;p&gt;But, despite powerfull functionality there might be reasons to use simpler alternatives like &lt;code&gt;venv&lt;/code&gt;. If you are working on a complex project, planning to publish your project on python repository like PyPi or it's historically based on Poetry then staying with Poetry makes sense. &lt;/p&gt;

&lt;p&gt;Other alternatives worth mentioning are virtualenv and pipenv. In data science and scientific computing also &lt;code&gt;conda&lt;/code&gt; has substantial popularity. While these tools provide options to customize your virtual environment it's not always needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simpler alternatives
&lt;/h3&gt;

&lt;p&gt;If you are working on a pet project, simple service or just don't want to complicate code with extra third party tools then using &lt;code&gt;venv&lt;/code&gt; can be an ideal solution. It's built-in Python library and has limited set of commands which makes it easy to learn. In the simplest scenario there no need to learn &lt;code&gt;venv&lt;/code&gt; at all. Just navige to the project directory and create virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it will do is to call python module &lt;code&gt;venv&lt;/code&gt; and create virtual environment in &lt;code&gt;.venv&lt;/code&gt; directory under the current directory.&lt;/p&gt;

&lt;p&gt;Other common names for virtual environment folders are &lt;code&gt;venv&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;. Now this folder contains executables for python and it will store installed packages. Let's now install some packages. But before we can do that virtual environment must be activated&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you no longer need virtual environment active deactivate it by simply typing &lt;code&gt;deactivate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In our hypothetical scenario let's assume that we are building a web app on Flask, but before that let's check that we are really in the virtual environment by calling &lt;code&gt;which python&lt;/code&gt;. It will produce a path to currently used python executable. If path leads to the current directory and ends like &lt;code&gt;.venv/bin/python&lt;/code&gt; then environment is activated and we are good to go.&lt;/p&gt;

&lt;p&gt;Installing Flask&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;Flask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Packages especially complex ones like Flask doesn't exist in isolation, it depends on other python packages that will be installed alongside. To ensure that this is the case call &lt;code&gt;pip freeze&lt;/code&gt;. It shows a list of installed packages in our virtual environment with their corresponding versions.&lt;/p&gt;

&lt;p&gt;A good practice is to maintain a text file &lt;code&gt;requirements.txt&lt;/code&gt; with relevant dependencies so other developers can setup environment for work by installing all required packages by calling &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Creating &lt;code&gt;requirements.txt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apart from packages that are required for running an app usually there is a need in tools that are used in development and testing. For this example we choose a package for testing &lt;code&gt;pytest&lt;/code&gt;, for maintaining code style in accordance with PEP8 let's install &lt;code&gt;flake8&lt;/code&gt; (for manual check) and &lt;code&gt;autopep8&lt;/code&gt; that will format our code properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest flake8 autopep8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;pip freeze&lt;/code&gt; to see that now there are more packages. And finally save to file: &lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping dependencies separate
&lt;/h3&gt;

&lt;p&gt;Here we came to the point where &lt;code&gt;venv&lt;/code&gt; simplicity gives us a bit of inconvenience. Currently installed packages can be divided into 2 groups - one group of packages related to Flask are necessary for running the app. However it's redundant to install test packages into production environment. &lt;/p&gt;

&lt;p&gt;The solution is to maintain two separate files - one for app packages (&lt;code&gt;requirements.txt&lt;/code&gt;) another for development packages (&lt;code&gt;requirements-dev.txt&lt;/code&gt;). In this case testing packages are also stored in &lt;code&gt;-dev&lt;/code&gt; file like for example in &lt;a href="https://github.com/flasgger/flasgger#how-to-run-tests" rel="noopener noreferrer"&gt;Flasgger&lt;/a&gt;. It's possible to split dependencies manually removing them from original &lt;code&gt;requirements.txt&lt;/code&gt; and saving them into &lt;code&gt;requirements-dev.txt&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;However if new packages will be installed this process should be repeated. At this point we have two options: either moving to more advances third-party virtual environment management tools or we can automate this process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automation
&lt;/h3&gt;

&lt;p&gt;Basically this process consists of two steps: save current packages list into file: &lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt; and then some script sould sort packages keeping app related packages in &lt;code&gt;requirements.txt&lt;/code&gt; while moving development packages to its development counterpart. &lt;/p&gt;

&lt;p&gt;This sequence can be executed manually from terminal however on UNIX based operation systems including MacOS there is more convenient solution: &lt;a href="https://www.gnu.org/software/make/manual/make.html" rel="noopener noreferrer"&gt;make&lt;/a&gt; utility. Basic usage is &lt;code&gt;make command-name&lt;/code&gt;. This utility when called search for file named &lt;code&gt;Makefile&lt;/code&gt; and execute sequence of commands under command name section.&lt;/p&gt;

&lt;p&gt;Let's create our Makefile and command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;freeze:
    pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
    python &lt;span class="nt"&gt;-m&lt;/span&gt; split_dependencies.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;make freeze&lt;/code&gt; will save dependencies into &lt;code&gt;requirements.txt&lt;/code&gt; and call python script to split dependencies between separate files. To run it correctly first let's create the basis for a future script. Create file &lt;code&gt;split_dependencies.py&lt;/code&gt; and put the following code into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run &lt;code&gt;make freeze&lt;/code&gt;. If everything has been done right terminal will produce the list of executed commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip freeze &amp;gt; requirements.txt
python -m split_dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point the following file structure is presented in working directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── .venv
├── Makefile
├── requirements.txt
└── split_dependencies.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Script architecture
&lt;/h3&gt;

&lt;p&gt;It's time to start developing the script. But before coding let's think about things that our script should be doing. It's going to consists of multiple functions and main executable function &lt;code&gt;run()&lt;/code&gt; that will be responsible for executing other functions.&lt;/p&gt;

&lt;p&gt;It's a good practice to follow single responsibility principle while coding function. So, every function will have only one task. For example list of packages should be loaded from file (&lt;code&gt;load_requirements&lt;/code&gt;) but for cleaning data from newline character another function can be used. Then to obtain cleared list of packages these function will be called in a sequence one passing data to another forming data pipeline.  &lt;/p&gt;

&lt;p&gt;A list of functions with their interfaces that will be used in the script separated by their responsibility:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;load_requirements(fname="requirements.txt")&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clean_list(items: list)&lt;/code&gt; - remove newline character from each string loaded from &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Logic&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is_dev_requirement(item: str)&lt;/code&gt; - check if current package is related to development category&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_prod_requirement(item: str)&lt;/code&gt; - check if current package is related to production (or app running) category&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extract(criteria: Callable, data: list)&lt;/code&gt; - filter list of packages by criteria (&lt;code&gt;is_dev_requirement&lt;/code&gt;, &lt;code&gt;is_prod_requirement&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prepare_data(data: list)&lt;/code&gt; - prepare data for saving (joining list of filtered packages to string)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;save_requirements(fname: str, data: str)&lt;/code&gt; - save requirements to file (&lt;code&gt;requirements.txt&lt;/code&gt; and &lt;code&gt;requirements-dev.txt&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since packages related to development and testing usually much less in quantity compared to app related packages it makes sense to store information about them for filtering purposes. Here is an example of a list with packages that might be used for development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DEV_REQUIREMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;autopep8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;black&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flake8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pytest-asyncio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pytest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Faker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also during the coding we are going to use python &lt;a href="https://docs.python.org/3/library/typing.html" rel="noopener noreferrer"&gt;typing&lt;/a&gt; to make code more readable in terms what datatypes are used and your IDE can use this information for providing type hinting. &lt;/p&gt;

&lt;p&gt;Further type checkers like &lt;a href="https://github.com/python/mypy" rel="noopener noreferrer"&gt;mypy&lt;/a&gt; can be used to enable static typing (Python by design is dynamic typing language). Since Python 3.5 typing is a built-in feature. However some additional functionality for typing can be achieved by using &lt;code&gt;typing&lt;/code&gt; package (shipped with Python distribution).&lt;/p&gt;

&lt;h3&gt;
  
  
  Coding
&lt;/h3&gt;

&lt;p&gt;Using developed script architecture let's code the script. It's going to be managed by &lt;code&gt;run()&lt;/code&gt; function where script logic will be implemented. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; loading list of packages from &lt;code&gt;requirements.txt&lt;/code&gt; and cleaning it from newline characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; forming two lists of packages - for production and development&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; preparing data and saving it into two text files.&lt;/p&gt;

&lt;p&gt;Here is a final version of a script. Feel free to experiment with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;

&lt;span class="n"&gt;DEV_REQUIREMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;autopep8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;black&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flake8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pytest-asyncio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pytest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Faker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_requirements&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;dev_dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_dev_requirement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                               &lt;span class="n"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prod_dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_prod_requirement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                                &lt;span class="n"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;save_requirements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requirements-dev.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                      &lt;span class="nf"&gt;prepare_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dev_dependencies&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;save_requirements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requirements.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                      &lt;span class="nf"&gt;prepare_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prod_dependencies&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_dev_requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;package_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;package_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DEV_REQUIREMENTS&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_prod_requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_dev_requirement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_requirements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requirements.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dependencies&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_requirements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Commentary
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;load_requirements&lt;/code&gt; - loads packages from file to list using &lt;code&gt;readlines&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clean_list&lt;/code&gt; removes newline character from each string through map which applies &lt;code&gt;item.strip()&lt;/code&gt; to each element of &lt;code&gt;items&lt;/code&gt; list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extract&lt;/code&gt; - filters initial list using built-in function &lt;code&gt;filter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_dev_requirement&lt;/code&gt; - takes a string with information about package (example: &lt;code&gt;Flask==2.2.3&lt;/code&gt;), extract package name and its version and checks whether package in the list of development packages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_prod_requirement&lt;/code&gt; - check whether package is production related, calls &lt;code&gt;is_dev_requirement&lt;/code&gt; and returns negated result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prepare_data&lt;/code&gt; joins list of packages by newline character and adds one at the end since according to convention in UNIX based opearation systems&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;save_requirements&lt;/code&gt; - writes data to file, called twice since packages are saved into two separate files. Writing is performed with &lt;code&gt;w&lt;/code&gt; flag since this is text data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run&lt;/code&gt; - implements script logic and ensures data exchange between functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now if someone wants to work on the project in order to set it up dependencies should be installed from both requirements files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements-dev.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusions
&lt;/h3&gt;

&lt;p&gt;In this tutorial we looked at the way of extending &lt;code&gt;venv&lt;/code&gt; functionality with a simple Python script that can be shipped with a project. Regarding the options on extensibility of this script there are plenty of things that can be done. For example splitting packages into three parts instead of two by introducing testing stage or applying sorting to make package list more convenient to navigate. The latter can be especially handy in large projects with lots of dependencies.&lt;/p&gt;

&lt;p&gt;You can find the files and code used in this tutorial here: &lt;a href="https://github.com/dmikhr/split-dependencies-demo" rel="noopener noreferrer"&gt;https://github.com/dmikhr/split-dependencies-demo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Building CLI time tracker with Python</title>
      <dc:creator>dmikhr</dc:creator>
      <pubDate>Wed, 19 Oct 2022 14:49:58 +0000</pubDate>
      <link>https://dev.to/dmikhr/building-cli-time-tracker-with-python-o0g</link>
      <guid>https://dev.to/dmikhr/building-cli-time-tracker-with-python-o0g</guid>
      <description>&lt;h3&gt;
  
  
  So, why another time tracker?
&lt;/h3&gt;

&lt;p&gt;There are plenty of time tracking apps and services, however it doesn't mean there is no room for a new app in this niche. Like there are established clothing brands with mass production capabilities doesn't eliminate custom made clothing brands. Making time tracker app as a personal project was both a personal challenge and opportunity to create an app that is tailored to specific time management techniques like a tailor can make a suit that's made by your specific requirements. &lt;/p&gt;

&lt;p&gt;However custom made doesn't mean "only for yourself". Like they say "&lt;em&gt;If you had an idea it's likely someone came up with the same idea before&lt;/em&gt;". Same situation here  —  if you developed particular way for doing things might mean that you simply discovered an approach that has been adopted by some people for a long time. It might be a niche approach but nevertheless your are not likely to be alone. This way of thinking was encouraging for me since it's more motivating when you build something that can be useful not only for yourself but for other people as well.&lt;/p&gt;

&lt;p&gt;My first approach to time tracking was quite straightforward: jot down on a piece of paper or a sticker when task started, then put finish time and a comment what task was about. Initially time tracking was purely manual.&lt;/p&gt;

&lt;p&gt;Though I enjoy taking notes manually and handwriting in general (especially with a good fountain pen) it's hard to do much with these data after it was written. Next step was tracking time in digital form but still manually using sticky notes app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11rrta8ahv8k45u6mubv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11rrta8ahv8k45u6mubv.png" alt="Digital time tracking" width="596" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looks neater and now it's possible to do something with these data. So idea to write a script that going to show amount of time spent on a specific task by supplying time slots as parameter was originated.&lt;/p&gt;

&lt;p&gt;To automate time spent on each activity a &lt;a href="https://github.com/dmikhr/time_management" rel="noopener noreferrer"&gt;ruby script&lt;/a&gt; was written that took a string with time ranges when task was active:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ruby st.rb "10:30 - 11:25 11:45 - 12:30"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now it's possible to do something with data about time spent on different tasks, but it was still a semi-manual approach. Track time manually, then pass it in a specific format to the script didn't feel convenient and was more like a compromise. So the idea to build an app to track time was born.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prototyping phase
&lt;/h3&gt;

&lt;p&gt;Having all these in place the question was what features should be included in the app. The first obvious step was to get rid of manually typing time ranges, so the idea was that user evoke the app with a specific flag and task name that indicates whether task is active or finished, while app tracks time itself.&lt;/p&gt;

&lt;p&gt;There were lots of ideas what features such application might have. But first thing first: before building full featured app let's make a prototype, a proof of concept. Then it's going to be clear whether it's gonna stick  —  am I going to use it consistently? If app proves to be handy further development efforts are justified.&lt;/p&gt;

&lt;p&gt;Overall the following minimal functionality must be in an app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add new task&lt;/li&gt;
&lt;li&gt;remove task&lt;/li&gt;
&lt;li&gt;start tracking time for a given task&lt;/li&gt;
&lt;li&gt;finish time tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also feature that shows time stats (how much time has been spent on each task during the day) was added.&lt;br&gt;
The idea was to build a console app. CLI apps have something in common with writing on paper - it's basic, nothing extra, but this is an appeal for both. Also doing software development projects made me get used to command line interfaces, so CLI app was a no brainer option.&lt;/p&gt;

&lt;p&gt;Further app architecture was built in the same manner - identify app component and separate them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;console interface&lt;/li&gt;
&lt;li&gt;app logic (time tracking, statistics)&lt;/li&gt;
&lt;li&gt;database management (creating database, add, remove data)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Console interface
&lt;/h3&gt;

&lt;p&gt;Console interface was built with &lt;strong&gt;argparse&lt;/strong&gt;. However it worth noting that alternatives were considered and one that stands out is &lt;strong&gt;click&lt;/strong&gt;. This module takes a different approach to building command line interfaces - instead of building methods for processing incoming arguments it offers a way to build CLI interface by decorating functions that will be evoked by a given flag (&lt;a href="https://palletsprojects.com/p/click/" rel="noopener noreferrer"&gt;code example&lt;/a&gt;). While &lt;strong&gt;click&lt;/strong&gt; seems like a promising library, for initial app version I decided to stay with tried and tested argparse. However in the future when CLI interface will comprise more features decorator approach that is used by click seems to be more scalable.&lt;/p&gt;

&lt;p&gt;The following commands were included in the initial app version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options:
-h, - help show this help message and exit
-s START, - start START
Start task by providing its name
-f, - finish Finish current task. Task also can be finished by
starting a different task
-a ADD, - add ADD Add new task
-r REMOVE, - remove REMOVE Remove the task
-l, - list List of all tasks
-st, - stats Stats about tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;With database design  —  let's keep it simple, one table for storing tasks and another for time tracking. Each working session spent on a specific task will be stored as a record in &lt;code&gt;work_blocks&lt;/code&gt; table. Each task may have any number of work blocks, including none if task hasn't been tracked yet. ER-diagram of the app database is presented on the following picture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnof2i2cv4v0a2nmvetjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnof2i2cv4v0a2nmvetjl.png" alt="diagram" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a database engine &lt;strong&gt;SQLite&lt;/strong&gt; has been chosen — lightweight, data is stored in a single file, overall good option for a first version of an app. For database management an &lt;strong&gt;SQLAlchemy ORM&lt;/strong&gt; was chosen to separate data manipulation from specific database implementation. Since it's a utility with local database and large amounts of data are not expected, so moving to distributed databases was not in a plan, auto incremented id was chosen for primary keys instead of &lt;a href="https://blog.boot.dev/clean-code/what-are-uuids-and-should-you-use-them/" rel="noopener noreferrer"&gt;uuid&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing timestamps&lt;/strong&gt;&lt;br&gt;
One issue with SQLite — it doesn't have &lt;code&gt;datetime&lt;/code&gt; data type and this is a time tracking app! The solution was to store time as a Unix timestamp using &lt;code&gt;Integer&lt;/code&gt; data type. This approach is recommended by SQLite documentation, since it can support storing numbers in up to 8 bytes which corresponds to &lt;code&gt;bigint&lt;/code&gt; in database engines such as PostgreSQL. Storing time in Unix timestamp might bring some inconveniences down the road with features like obtaining daily stats, however it doesn't seem like a real roadblock for further development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database location and schema&lt;/strong&gt;&lt;br&gt;
To store database user files directory was used. The issue here is that each OS has it's own path for this directory. But there is no problem, Python is known for having libraries almost for everything and identifying user directory is no exception: for this job &lt;a href="https://github.com/ActiveState/appdirs" rel="noopener noreferrer"&gt;appdirs&lt;/a&gt; has been chosen. When app starts for the first time it will check whether database exists and if no database file was found it will create an empty database with the following schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

   &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WorkBlock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="n"&gt;cascade&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all,delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="n"&gt;backref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WorkBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;work_blocks&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

   &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
   &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;finish_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  App logic
&lt;/h3&gt;

&lt;p&gt;App logic consists of methods that evoked through CLI interface and interact with database as well as a collection of private methods that serve auxiliary purpose.&lt;/p&gt;

&lt;p&gt;Start task - app creates a record in &lt;code&gt;work_blocks&lt;/code&gt; table related to a given task that is stored in tasks table with a unix timestamp of a current time in &lt;code&gt;start_time&lt;/code&gt; field while &lt;code&gt;finish_time&lt;/code&gt; field remains empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finish task&lt;/strong&gt;  —  no arguments are required for this options. If there is an active task (&lt;code&gt;finish_time==NULL&lt;/code&gt;) current time in Unix timestamp format will be recorded in the finish_time time, thus session for a current task will be ended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add new task&lt;/strong&gt;  —  in order to track tasks there should be a way to store them. This options requires task name as input and record data in tasks table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove task&lt;/strong&gt;  —  if task is no more needed it can be removed. In the current version removing the task from tasks table triggers 'delete on cascade' and all corresponding work_block will be removed as well. In the future versions of an app an additional option might be added that's going to archive tasks instead of complete deletion. The motivation behind this is that even if task is no more needed, storing data about it can be useful for further data analysis. For example for understanding performance bottlenecks that can occur while working on similar tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;List all tasks&lt;/strong&gt;  —  show all tasks that has been added to an app. If one of these tasks is active there will be (in progress) status near it. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project_work
Exercising (in progress)
Flask_API_project
Some_task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Statistics&lt;/strong&gt;  —  shows only tasks that were active today. For each task time spent on a it is shown in two formats: hr:min and in decimal format. For example: 1:20 (1.33). The reason behind presenting time in decimal format is that it can be useful if you track time in spreadsheets and it's easier to use time in such format for further analysis like counting total time spent on a task during the last week, month, etc. I've been using this approach for identifying issues with prioritisation - track time on each task/project and at the end of the week see how much time was spend on each. If there is an outlier (task that consumed way more time that others) and there is no clear reason why it has happened (close deadline, high importance of the task) that it's the reason to change priorities and dedicate more time to other responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;p&gt;Testing was implemented using PyTest framework. Test data was generated with Faker. In order to isolate test data for each test fixtures were used. Fixtures are convenient way to generate test data and ensure its consistency among different tests. If you're not familiar with them you may check &lt;a href="https://docs.pytest.org/en/7.1.x/how-to/fixtures.html" rel="noopener noreferrer"&gt;this example&lt;/a&gt; to get an idea how it works.&lt;/p&gt;

&lt;p&gt;Since application features mostly use database it was important to use a test database. In the simplest case requests to database can be stubbed, but then most test won't be useful in this particular case. &lt;/p&gt;

&lt;p&gt;First idea was to create temporary SQLite database file each time tests run and delete it after they're finished, however this approach didn't feel right. Thankfully it's possible to create SQLite database in memory. This approach doesn't require any read/write operations with file system, no need to think about deleting database file after testing. In case of in-memory database memory will be cleared automatically after connection is closed.&lt;/p&gt;

&lt;p&gt;While choosing in-memory database for testing it's important to consider how much test data is planned to put into database. In this case it was about dozens of records at most, so exceeding memory limit was not an issue.&lt;br&gt;
To establish connection and create in-memory sqlite database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite+pysqlite:///:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing approach was to cover with tests all public methods. Mostly I tried to follow &lt;a href="https://testdriven.io/test-driven-development/" rel="noopener noreferrer"&gt;TDD approach&lt;/a&gt; by writing tests first and then implementing a feature, followed by several iterations of test and implementation improvements and code refactoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Giving a proper name to project
&lt;/h3&gt;

&lt;p&gt;When you're working on a personal project it might seem that project name is not a big deal. However giving a project a proper name achieves at least two important goals. First, when personal project has a name it feels subconsciously that I began to take it more seriously, not just an exercise for improving programming skills but more like I'm building a tool or a product that might be useful not only for personal purpose but for other people too. Secondly, if you decide to share your project with community via repository such as &lt;em&gt;pypi&lt;/em&gt; or &lt;em&gt;npm&lt;/em&gt; you will need a unique name for a project. In case of this project I gave it a name &lt;strong&gt;tiempo-tracker&lt;/strong&gt; (tiempo means time in Spanish).&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusions
&lt;/h3&gt;

&lt;p&gt;So far I found myself using this app consistently during my work process and new ideas were born on how to improve an app and what features might be useful to add. However it's important not to be overwhelmed with implementing all ideas at once but instead keeping track of them and planning on implementing them when they're truly needed.&lt;/p&gt;

&lt;p&gt;Pet-projects is something that can be useful and educational. Using a self-developed app tailored to your own requirements is an interesting experience, however personal projects take time and the more features app has the more maintenance it requires: more tests, bug-fixes. It's important not to be overly consumed in this activity since a lot of other things requires our attention both professional responsibilities and personal. However it's something that easier to be said than done especially when you're passionate about your project.&lt;/p&gt;

&lt;p&gt;A short demo is available on &lt;a href="https://asciinema.org/a/7RFZiBHamLTBqOuwvsIYyp71Z" rel="noopener noreferrer"&gt;Asciinema&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can check the source code of &lt;a href="https://github.com/dmikhr/tiempo-tracker" rel="noopener noreferrer"&gt;tiempo-tracker on Github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
