<?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: Kurt McAlpine</title>
    <description>The latest articles on DEV Community by Kurt McAlpine (@kurtmc).</description>
    <link>https://dev.to/kurtmc</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%2F888838%2F61839c7a-cad7-4bce-b8b4-aee3426211ca.jpg</url>
      <title>DEV Community: Kurt McAlpine</title>
      <link>https://dev.to/kurtmc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kurtmc"/>
    <language>en</language>
    <item>
      <title>Unifi Autobackup Data Recovery and Restore</title>
      <dc:creator>Kurt McAlpine</dc:creator>
      <pubDate>Sun, 02 Jun 2024 03:52:45 +0000</pubDate>
      <link>https://dev.to/kurtmc/unifi-autobackup-data-recovery-and-restore-1fc4</link>
      <guid>https://dev.to/kurtmc/unifi-autobackup-data-recovery-and-restore-1fc4</guid>
      <description>&lt;h1&gt;
  
  
  Unifi Autobackup Data Recovery and Restore
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://community.ui.com/releases/UniFi-Network-Application-8-0-24/43b24781-aea8-48dc-85b2-3fca42f758c9" rel="noopener noreferrer"&gt;Unifi controller&lt;/a&gt; is a great piece of software that allows you to easily manage hundreds of WiFi access points, configuring multiple SSIDs, VLAN, etc. The insights dashboard can help you identify clients performance issue and rogue APs. In general I think Unifi is a great tool for home or small business use. So you might have guess, this is a cautionary tale that I hope you don't have to experience yourself since you have a robust backup process that is tested regularly!&lt;/p&gt;

&lt;p&gt;Some background: at my work we run Unifi network controller on AWS EC2. There is a redundant backup process, each day we make a full snapshot of the data directory of the Unifi controller application. We do this by first stopping the application, running a simple tar command &lt;code&gt;tar -czvf $(unifi--data-$(date +%s).tar.gz) config/data&lt;/code&gt;, upload the file to AWS S3 then starting the application back up again. Simple and effective. The restore process has been tested many times and works just as you'd expect. Just for good measure, Unifi provides an incremental backup file that it creates each day too, it creates an autobackup up with a name that looks something like this &lt;code&gt;autobackup_8.0.24_20240527_1200_1716811200004.unf&lt;/code&gt;, and these are synced to AWS S3 as an insurance policy.&lt;/p&gt;

&lt;p&gt;We made a couple mistakes with our S3 backup approach, we put a lifecycle rule on the bucket to delete files after a certain age. The intention was to be able to keep the last &lt;code&gt;n&lt;/code&gt; backups, but in practice if something goes wrong with the backup process after a few days all the backups will get deleted. We should have implemented a monitor that checks that backups are being produced and alarm early if something is wrong so it can be fixed before any backups get deleted. Hindsight is 20/20.&lt;/p&gt;

&lt;p&gt;Some work was being carried out on the EC2 instance that runs Unifi and as part of a normal procedure with these types of third party applications that we run, the instance was terminated and a new one created that would usually look for the latest backup and run the restore procedure automatically. The new instance never restored the data and when it was investigated, we noticed that all the backups were missing.&lt;/p&gt;

&lt;p&gt;No worries, the autobackups were still on S3, surely we can just use that right?!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fkurtmc%2Fblog%2Fraw%2Fmaster%2F2024-06%2Funifi-autobackup-data-recovery-and-restore%2Fimages%2Fnot_a_valid_backup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fkurtmc%2Fblog%2Fraw%2Fmaster%2F2024-06%2Funifi-autobackup-data-recovery-and-restore%2Fimages%2Fnot_a_valid_backup.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error restoring backup
"{filename}" is not a valid backup.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*For the purposes of helping out others who might be in this situation, this all relates to version 8.0.24 of Unifi controller and specifically we were using this docker image &lt;a href="https://hub.docker.com/r/linuxserver/unifi-controller" rel="noopener noreferrer"&gt;https://hub.docker.com/r/linuxserver/unifi-controller&lt;/a&gt; which is now deprecated and you should be using the following instead &lt;a href="https://hub.docker.com/r/linuxserver/unifi-network-application" rel="noopener noreferrer"&gt;https://hub.docker.com/r/linuxserver/unifi-network-application&lt;/a&gt; *&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ok, there must be something useful in the HTTP response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 400 
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Content-Length: 63
Date: Sun, 02 Jun 2024 03:00:25 GMT
Connection: close
{"meta":{"rc":"error","msg":"api.err.InvalidBackup"},"data":[]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nope just a 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can anything be done?
&lt;/h2&gt;

&lt;p&gt;So there is some hope here, I found some things online which can help us inspect the data. First step, there is this repository which provides a bash script to decrypt the backup: &lt;a href="https://github.com/zhangyoufu/unifi-backup-decrypt" rel="noopener noreferrer"&gt;https://github.com/zhangyoufu/unifi-backup-decrypt&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget https://raw.githubusercontent.com/zhangyoufu/unifi-backup-decrypt/master/decrypt.sh
chmod +x decrypt.sh
./decrypt autobackup_8.0.24_20240527_1200_1716811200004.unf autobackup.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a zip file, that seems to be completely broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ unzip autobackup.zip 
Archive:  autobackup.zip
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of autobackup.zip or
        autobackup.zip.zip, and cannot find autobackup.zip.ZIP, period.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but, this can be extracted further if you use &lt;a href="https://www.7-zip.org/download.html" rel="noopener noreferrer"&gt;7zip&lt;/a&gt;, which can be installed on Linux or Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Ubuntu
sudo apt install p7zip-full

# Arch Linux
pacman -S p7zip

# Mac
brew install p7zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now extract the zip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7z x autobackup.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces &lt;code&gt;db.gz&lt;/code&gt;, which can further be extacted with &lt;code&gt;gunzip&lt;/code&gt; to produce a BSON file named &lt;code&gt;db&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;gunzip db.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the &lt;code&gt;db&lt;/code&gt; file can be converted to plain text with MongoDB Database Tools, which can be downloaded from here: &lt;a href="https://www.mongodb.com/try/download/database-tools" rel="noopener noreferrer"&gt;https://www.mongodb.com/try/download/database-tools&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bsondump db &amp;gt; dump.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may produce a very large file depending on your Unifi deployment size and if you look into the data you see sections and data that seems to be for that section. For for example the lines after &lt;code&gt;{"__cmd":"select","collection":"devices"}&lt;/code&gt; contain all the mongodb objects in the &lt;code&gt;devices&lt;/code&gt; collection&lt;/p&gt;

&lt;p&gt;I wrote a &lt;a href="https://github.com/kurtmc/blog/blob/master/2024-06/unifi-autobackup-data-recovery-and-restore/files/main.go" rel="noopener noreferrer"&gt;small program in go&lt;/a&gt; that can be used to load this data directly into the mongodb database. Which you can do by following these steps:&lt;/p&gt;

&lt;p&gt;Copy the &lt;code&gt;dump.json&lt;/code&gt; file into the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker container cp dump.json 5ab135e2d58b:/dump.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download and run the program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec -it 5ab135e2d58b bash
curl -O https://github.com/kurtmc/blog/raw/master/2024-06/unifi-autobackup-data-recovery-and-restore/files/unifi-restore
./unifi-restore /dump.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the size of your backup, this can take hours, but once it has completed you should restart the Unifi application. If the program fails, make sure you read the error, it may be due to the buffer size being too small or the max token size being too small, both of which can be configured using environment variables.&lt;/p&gt;

&lt;p&gt;I hope you don't find yourself in this situation where you must rely on the autobackups, but if you do, I hope this helps!&lt;/p&gt;

</description>
      <category>unifi</category>
      <category>bash</category>
      <category>go</category>
    </item>
    <item>
      <title>Lightweight artifact repository with Python and GitHub</title>
      <dc:creator>Kurt McAlpine</dc:creator>
      <pubDate>Sun, 10 Jul 2022 03:54:59 +0000</pubDate>
      <link>https://dev.to/kurtmc/lightweight-artifact-repository-with-python-and-github-4ko2</link>
      <guid>https://dev.to/kurtmc/lightweight-artifact-repository-with-python-and-github-4ko2</guid>
      <description>&lt;h1&gt;
  
  
  Lightweight artifact repository with Python and GitHub
&lt;/h1&gt;

&lt;p&gt;Code reuse is fundamental to reducing the cost of software development, reusing a function implementation rather than developing it again is faster. Fixing a bug in a library rather than in multiple re-implementations is easier. A common way to reuse code is to package up related functionality and publish it as a library. Usually you could publish to &lt;a href="https://jfrog.com/artifactory/" rel="noopener noreferrer"&gt;Arifactory&lt;/a&gt; or &lt;a href="https://www.sonatype.com/products/nexus-repository" rel="noopener noreferrer"&gt;Nexus&lt;/a&gt; but occasionally you may have a business constraint that makes it painful and slow to onboard a new tool, often for valid reasons, they maybe be expensive to onboard and support.&lt;/p&gt;

&lt;p&gt;Coming from a background of Node.js and Go programming, it had been quite a shock to me when I saw the state of Python dependency management. Node.js and Go have canonical dependency management practices, with node you have &lt;code&gt;npm&lt;/code&gt; and &lt;code&gt;yarn&lt;/code&gt; and Go it's built into the toolchain. When you are ready to abstract some logic into it’s own library, it’s as simple as creating a new git repository, writing the appropriate metadata files (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;) push and you have a dependency you can import into your project!&lt;/p&gt;

&lt;p&gt;Here are some examples of importing directly from git in the tools I am familiar with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js: &lt;code&gt;yarn add https://github.com/octokit/rest.js.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go: &lt;code&gt;go get github.com/google/go-github/v45&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is so incredibly easy and I want to have the same experience with Python. Is this possible? Almost!&lt;/p&gt;

&lt;p&gt;I discovered that I could achieve similar ergonomics using existing and widely used tools, and I would like to demonstrate that here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python Library Project setup
&lt;/h2&gt;

&lt;p&gt;The first step is to setup a Python library project, the best way to go about doing this is to follow the official documentation which can be found here: &lt;a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/" rel="noopener noreferrer"&gt;https://packaging.python.org/en/latest/tutorials/packaging-projects/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will summarise what needs to be done to demonstrate a working example.&lt;/p&gt;

&lt;p&gt;You will need to create the following structure in your git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── pyproject.toml
├── README.md
├── src
│   ├── example_package
│   │   ├── example.py
│   │   └── __init__.py
└── tests
    └── test_example.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contents of these files are listed here, you should update the fields in&lt;br&gt;
&lt;code&gt;pyproject.toml&lt;/code&gt; to match your organisation or project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pyproject.toml&lt;/code&gt; Update this to match your organisation.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[build-system]&lt;/span&gt;
  &lt;span class="py"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hatchling.build"&lt;/span&gt;
  &lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"hatchling"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[project]&lt;/span&gt;
  &lt;span class="py"&gt;classifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Programming Language :: Python :: 3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"License :: OSI Approved :: MIT License"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Operating System :: OS Independent"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["boto3=&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.23&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"A small example package"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"python-library-test"&lt;/span&gt;
  &lt;span class="py"&gt;readme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"README.md"&lt;/span&gt;
  &lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.8&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.1"&lt;/span&gt;

  &lt;span class="nn"&gt;[[project.authors]]&lt;/span&gt;
    &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kurt.mcalpine@sourcedgroup.com"&lt;/span&gt;
    &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Kurt McAlpine"&lt;/span&gt;

  &lt;span class="nn"&gt;[project.urls]&lt;/span&gt;
    &lt;span class="py"&gt;"Bug Tracker"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/kurtmc/python-library-test/issues"&lt;/span&gt;
    &lt;span class="py"&gt;Homepage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/kurtmc/python-library-test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; you may add additional dependencies this project may have to the dependencies field under [project] . In this example I have added boto3 as a dependency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/example_package/example.py&lt;/code&gt; example code
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&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;number&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tests/test_example.py&lt;/code&gt; example test
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&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="s"&gt;/../src&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_package.example&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add_one&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestExamplePackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&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;test_add_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&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="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once you have this structure setup, you can commit it and push it to your git repository. The next incredibly useful feature to add will be automatic versioning and tagging. We can use GitHub actions to automatically increment a version number and apply git tags. Later we will use the git tag to specify exactly which version of the library we want to include as a dependency to a new project.&lt;/p&gt;

&lt;p&gt;Create the following files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.github/workflows/update-version.yml&lt;/code&gt; You may want to change the branch name if main is not your default branch name.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Updates version and tags&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update_version_and_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Python &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.8&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update version&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kurtmc/github-action-python-versioner@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now any changes to &lt;code&gt;main&lt;/code&gt; will be tagged and the version in &lt;code&gt;pyproject.toml&lt;/code&gt; will be updated by GitHub actions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fkurtmc%2Fblog%2Fraw%2Fmaster%2F2022-07%2Flightweight-artifact-repository-with-python-and-github%2Fimages%2F1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fkurtmc%2Fblog%2Fraw%2Fmaster%2F2022-07%2Flightweight-artifact-repository-with-python-and-github%2Fimages%2F1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whilst we are here, we should add a GitHub action that runs on pull requests to enforce code style consistency and validate that the unit tests pass.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/pull-request.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests against pull requests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Python &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.8&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install flake8==4.0.1&lt;/span&gt;
          &lt;span class="s"&gt;flake8 ./src --ignore E501&lt;/span&gt;
  &lt;span class="na"&gt;unit_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Python &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.8&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install -e .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Python unittest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python -m unittest tests/*.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can ensure that all new code added to the library follows consistent code style and the unit tests pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fkurtmc%2Fblog%2Fraw%2Fmaster%2F2022-07%2Flightweight-artifact-repository-with-python-and-github%2Fimages%2F2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fkurtmc%2Fblog%2Fraw%2Fmaster%2F2022-07%2Flightweight-artifact-repository-with-python-and-github%2Fimages%2F2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We now have a Python library project in GitHub, following code style best practices and automatic version incrementing. How do we import it into a Python project?&lt;/p&gt;

&lt;p&gt;Using the git URL in &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;example-python-library @ git+https://github.com/YourOrg/example-python-library.git@0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now lets try install it:&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;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
Collecting example-python-library@ git+https://github.com/YourOrg/example-python-library.git@0.0.1
  Cloning https://github.com/YourOrg/example-python-library.git &lt;span class="o"&gt;(&lt;/span&gt;to revision 0.0.1&lt;span class="o"&gt;)&lt;/span&gt; to /tmp/pip-install-1h4qrmmg/example-python-library_22bdfa1c6ab242c18e0e17b700c1be60
  Running &lt;span class="nb"&gt;command &lt;/span&gt;git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--quiet&lt;/span&gt; https://github.com/YourOrg/example-python-library.git /tmp/pip-install-1h4qrmmg/example-python-library_22bdfa1c6ab242c18e0e17b700c1be60
Username &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com'&lt;/span&gt;:
Password &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com'&lt;/span&gt;:
  remote: Repository not found.
  fatal: Authentication failed &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/YourOrg/example-python-library.git/'&lt;/span&gt;
  error: subprocess-exited-with-error

  × git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--quiet&lt;/span&gt; https://github.com/YourOrg/example-python-library.git /tmp/pip-install-1h4qrmmg/example-python-library_22bdfa1c6ab242c18e0e17b700c1be60 did not run successfully.
  │ &lt;span class="nb"&gt;exit &lt;/span&gt;code: 128
  ╰─&amp;gt; See above &lt;span class="k"&gt;for &lt;/span&gt;output.

  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--quiet&lt;/span&gt; https://github.com/YourOrg/example-python-library.git /tmp/pip-install-1h4qrmmg/example-python-library_22bdfa1c6ab242c18e0e17b700c1be60 did not run successfully.
│ &lt;span class="nb"&gt;exit &lt;/span&gt;code: 128
╰─&amp;gt; See above &lt;span class="k"&gt;for &lt;/span&gt;output.

note: This error originates from a subprocess, and is likely not a problem with pip.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails to install because in the case of a private repository. We need to tell git to use our SSH credentials when cloning this private repository, which we can do with &lt;code&gt;git config&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;git config &lt;span class="nt"&gt;--global&lt;/span&gt; url.&lt;span class="s2"&gt;"git@github.com:"&lt;/span&gt;.insteadOf &lt;span class="s2"&gt;"https://github.com/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempting the install again:&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;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
Collecting example-python-library@ git+https://github.com/YourOrg/example-python-library.git@0.0.1
  Cloning https://github.com/YourOrg/example-python-library.git &lt;span class="o"&gt;(&lt;/span&gt;to revision 0.0.1&lt;span class="o"&gt;)&lt;/span&gt; to /tmp/pip-install-z0q1jh7e/example-python-library_0e007d22fbd1439d9481e28d224387bf
  Running &lt;span class="nb"&gt;command &lt;/span&gt;git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--quiet&lt;/span&gt; https://github.com/YourOrg/example-python-library.git /tmp/pip-install-z0q1jh7e/example-python-library_0e007d22fbd1439d9481e28d224387bf
  Running &lt;span class="nb"&gt;command &lt;/span&gt;git checkout &lt;span class="nt"&gt;-q&lt;/span&gt; 32600d1874df73fc209736eef6bbd09553cf2dc0
  Resolved https://github.com/YourOrg/example-python-library.git to commit 32600d1874df73fc209736eef6bbd09553cf2dc0
  Installing build dependencies ... &lt;span class="k"&gt;done
  &lt;/span&gt;Getting requirements to build wheel ... &lt;span class="k"&gt;done
  &lt;/span&gt;Installing backend dependencies ... &lt;span class="k"&gt;done
  &lt;/span&gt;Preparing metadata &lt;span class="o"&gt;(&lt;/span&gt;pyproject.toml&lt;span class="o"&gt;)&lt;/span&gt; ... &lt;span class="k"&gt;done
&lt;/span&gt;Requirement already satisfied: &lt;span class="nv"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.23.6 &lt;span class="k"&gt;in&lt;/span&gt; /usr/local/lib/python3.10/site-packages &lt;span class="o"&gt;(&lt;/span&gt;from example-python-library@ git+https://github.com/YourOrg/example-python-library.git@0.0.1-&amp;gt;-r requirements.txt &lt;span class="o"&gt;(&lt;/span&gt;line 1&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;1.23.6&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🥳 This is working locally! Now we should configure our CI/CD platform in the same way if we use SSH authentication, however if you are using personal access tokens registered against a service user you can configure git like this (assuming the personal access token is available under the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; environment variable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; url.&lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@github.com/"&lt;/span&gt;.insteadOf &lt;span class="s2"&gt;"https://github.com/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Above, is a demonstration on how to build your own private dependency management system for Python using git and GitHub actions. Is this the best solution for private dependency management? Probably not, if you are in the position to pick technologies and services or are starting a greenfield project, you will be able to pick something that works out of the box (examples include: &lt;a href="https://jfrog.com/artifactory/" rel="noopener noreferrer"&gt;Artifactory&lt;/a&gt;, &lt;a href="https://www.sonatype.com/products/nexus-repository" rel="noopener noreferrer"&gt;Nexus&lt;/a&gt;, &lt;a href="https://aws.amazon.com/codeartifact/" rel="noopener noreferrer"&gt;AWS CodeArtifact&lt;/a&gt;) and establish best practices from the beginning. Not everyone is so lucky, and you may not be able to onboard a new tool so you need to stick with what you already have, and you almost certainly already have GitHub, this may be a solution for you.&lt;/p&gt;

</description>
      <category>python</category>
      <category>git</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
