In my last article in this series, I showed how to write a script in Python that returned a list of RPM-installed software installed on a machine. The output looks like this:
$ rpmqa_simple.py --limit 20
linux-firmware-20210818: 395,099,476
code-1.61.2: 303,882,220
brave-browser-1.31.87: 293,857,731
libreoffice-core-7.0.6.2: 287,370,064
thunderbird-91.1.0: 271,239,962
firefox-92.0: 266,349,777
glibc-all-langpacks-2.32: 227,552,812
mysql-workbench-community-8.0.23: 190,641,403
java-11-openjdk-headless-11.0.13.0.8: 179,469,639
iwl7260-firmware-25.30.13.0: 148,167,043
docker-ce-cli-20.10.10: 145,890,250
google-noto-sans-cjk-ttc-fonts-20190416: 136,611,853
containerd.io-1.4.11: 113,368,911
ansible-2.9.25: 101,106,247
docker-ce-20.10.10: 100,134,880
ibus-1.5.23: 90,840,441
llvm-libs-11.0.0: 87,593,600
gcc-10.3.1: 84,899,923
cldr-emoji-annotation-38: 80,832,870
kernel-core-5.14.12: 79,447,964
Now I want to package an application so that I can install it easily, including all the dependencies, on other machines. In this article, I'll show how to use the setuptools package to do that.
That's a lot to cover, so basic knowledge of Python is required. Even if you don't know much, the code is simple to follow, and the boilerplate code is small.
Set up your environment
My previous article explained how to install the code and use virtual environments, but here is a shortcut:
$ sudo dnf install -y python3-rpm
$ git clone git@github.com:josevnz/tutorials.git
$ cd rpm_query
$ python3 -m venv --system-site-packages ~/virtualenv/rpm_query
. ~/virtualenv/rpm_query/bin/activate
(rpm_query)$
Package and install the distribution
The next step is to package the application. However, I won't use RPM for that.
Why not use an RPM to package the Python application?
Well, there is no short answer to that.
RPM is great if you want to share your application with all the users of your system, especially because RPM can automatically install related dependencies.
For example, the RPM Python bindings (rpm-python3
) are distributed this way, which makes sense as it is thinly tied to RPM.
In some cases, it is also a disadvantage:
- Users need root elevated access to install an RPM. If the code is malicious, it will take control of the server very easily. (That's why you always check the RPM signatures and download code from well-known sources, right?)
- Attempting to upgrade to an RPM that is incompatible with older dependent applications fails.
- RPM is not well suited to share "test" code created during continuous integration, at least in bare-metal deployments. With a container, that is probably a different story.
- If the Python code has dependencies, they will likely have to be packaged as RPMs.
Use virtual environments and pip + setuptools
Using virtual environments, pip, and setup tools solves these RPM limitations because:
- Virtual environments allow installing applications without elevated permissions.
- The application is self-contained in the virtual environment. Administrators can install different versions of the libraries without affecting the whole system.
- It is very easy to integrate a virtual environment with continuous integration and unit testing. After the tests pass, the environment can be recycled.
- setuptools solves the problem of packaging your application in a nice directory structure for making scripts and libraries available to users.
- setuptools also deals with keeping track of dependencies by using proper version checks to make the build process repeatable.
- setuptools works with pip, the Python package manager.
- Both virtual environments and setuptools have excellent support in IDEs like PyCharm and VS Code.
[ Decrease the complexity of getting into the cloud by downloading the Hybrid cloud strategy for dummies eBook. ]
Work with setuptools
Once you're ready to deploy the application, you can package it, copy its wheel file, and then install it in a new virtual environment. First, define a very important file, setup.py
, which is used by setuptools.
The most important sections in the file below are:
- *_requires sections: Build and installation dependencies
- packages: The location of Python classes
- scripts: The scripts that the end user calls to interact with the libraries (if any)
"""
Project packaging and deployment
More details: https://setuptools.pypa.io/en/latest/userguide/quickstart.html
"""
import os
from setuptools import setup
from reporter import __version__
def __read__(file_name):
return open(os.path.join(os.path.dirname(__file__), file_name)).read()
setup(
name="rpm_query",
version=__version__,
author="Jose Vicente Nunez Zuleta",
author_email="kodegeek.com@protonmail.com",
description=__doc__,
license="Apache",
keywords="rpm query",
url="https://github.com/josevnz/rpm_query",
packages=[
'reporter'
],
long_description=__read__('README.md'),
# https://pypi.org/pypi?%3Aaction=list_classifiers
classifiers=[
"Development Status :: 3 - Alpha",
"Topic :: Utilities",
"Environment :: X11 Applications",
"Intended Audience :: System Administrators"
"License :: OSI Approved :: Apache Software License"
],
setup_requires=[
"setuptools==49.1.3",
"wheel==0.37.0",
"rich==9.5.1",
"dearpygui==1.0.2"
],
install_requires=[
"rich==9.5.1",
],
scripts=[
"bin/rpmq_simple.py",
]
)
Things to note:
- I stored the version in the
reporter/__init__.py
package module to share with other parts of the application, not just setuptools. I also used a semantic version schema naming convention. - The module's README is also stored as an external file. This makes editing the file much easier without worrying about size or breaking the Python syntax.
- Classifiers make it easier to see the application's intent.
- I can define packaging dependencies (
setup_requires
) and runtime dependencies (install_requires
). - I need the
wheel
package to create a precompiled distribution that is faster to install than other modes.
The pyproject.toml
file is also very important. Here is the file:
[build-system]
requires = ["setuptools", "wheel"]
The pyproject.toml
file specifies what is used for packaging the scripts and installing from source.
Quick check before uploading
Before you upload the wheel, you should ask Twine to check your settings for errors, like this:
(rpm_query) [josevnz@dmaf5 rpm_query]$ twine check dist/rpm_query-0.0.1-py3-none-any.whl
Checking dist/rpm_query-0.0.1-py3-none-any.whl: FAILED
`long_description` has syntax errors in markup and would not be rendered on PyPI.
line 20: Warning: Bullet list ends without a blank line; unexpected unindent.
warning: `long_description_content_type` missing. defaulting to `text/x-rst`.
The Markdown is incorrect in the file. One way to fix this issue is to install:
$ pip install readme_renderer[md]
Also, the long_description_content_type
section is there:
long_description_content_type="text/markdown",
long_description=__read__('README.md'),
If you rerun it after making the changes above, you still see the warning:
rpm_query) [josevnz@dmaf5 rpm_query]$ twine check dist/rpm_query-0.0.1-py3-none-any.whl
Checking dist/rpm_query-0.0.1-py3-none-any.whl: PASSED, with warnings
warning: `long_description_content_type` missing. defaulting to `text/x-rst`.
No serious errors and one false alarm. You are ready to upload your wheel.
How to deploy while testing
You don't need to package and deploy the application in full mode. This is because setuptools has a very convenient develop mode that installs dependencies and allows editing the code while testing it:
(rpm_query)$ python setup.py develop
This mode creates special symbolic links that put the scripts (remember that scripts:
section inside setup.py
?) into your path.
By the way, once you are finished testing, you can remove development mode:
(rpm_query)$ python setup.py develop --uninstall
The official documentation recommends migrating from a setup.py
configuration to setup.cfg.
I decided to use setup.py
because it is still the most popular format.
Create a precompiled distribution
Compiling is as simple as typing this:
(rpm_query)$ python setup.py bdist_wheel
running bdist_wheel
... # Omitted output
(rpm_query)$ ls dist/
rpm_query-0.0.1-py3-none-any.whl
Then you can install it on the same machine or a new virtual machine:
(rpm_query)$ python setup.py install \
dist/rpm_query-1.0.0-py3-none-any.whl
Or with the new preferred way, using build
. First make sure the module is installed:
(rpm_query) $ pip install build
Collecting build
Downloading build-0.7.0-py3-none-any.whl (16 kB)
Collecting tomli>=1.0.0
Downloading tomli-1.2.2-py3-none-any.whl (12 kB)
Requirement already satisfied: packaging>=19.0 in /usr/lib/python3.9/site-packages (from build) (20.4)
Collecting pep517>=0.9.1
Downloading pep517-0.12.0-py2.py3-none-any.whl (19 kB)
Requirement already satisfied: pyparsing>=2.0.2 in /usr/lib/python3.9/site-packages (from packaging>=19.0->build) (2.4.7)
Requirement already satisfied: six in /usr/lib/python3.9/site-packages (from packaging>=19.0->build) (1.15.0)
Installing collected packages: tomli, pep517, build
Successfully installed build-0.7.0 pep517-0.12.0 tomli-1.2.2
And then you can package your module like this (note that we do not tell build
to use a virtual environment because we are already in one):
(rpm_query) $ python3 -m build --no-isolation
* Getting dependencies for wheel...
* Building wheel...
running bdist_wheel
running build
running build_scripts
# ... Omitted output
whl' and adding 'build/bdist.linux-x86_64/wheel' to it
adding 'rpm_query-0.1.0.data/scripts/rpmq_dearpygui.py'
adding 'rpm_query-0.1.0.data/scripts/rpmq_rich.py'
adding 'rpm_query-0.1.0.data/scripts/rpmq_simple.py'
adding 'rpm_query-0.1.0.data/scripts/rpmq_tkinter.py'
adding 'rpm_query-0.1.0.dist-info/LICENSE.txt'
adding 'rpm_query-0.1.0.dist-info/METADATA'
adding 'rpm_query-0.1.0.dist-info/WHEEL'
adding 'rpm_query-0.1.0.dist-info/top_level.txt'
adding 'rpm_query-0.1.0.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built rpm_query-0.1.0-py3-none-any.whl
What if you want to share the .pip file with other users? I could copy the wheel file to the different machines and have the users install it, but there is a better way.
Set up a private PyPI server
Note: This setup is not production quality because:
- It is not secure because it uses passwords instead of tokens for authentication.
- There's no SSL encryption. HTTP sends clear-text passwords over the wire.
- There's no storage redundancy. Ideally, the pip storage should have some sort of redundancy and backups.
You can do a lot more than just installing from a wheel file. For example, you can set up a private server compatible with PyPI using a container running pypiserver.
[ Download the datasheet for information about Red Hat OpenShift Container Platform. ]
First, create a directory to store the packages:
$ mkdir -p -v $HOME/pypiserver
$ mkdir: created directory '/home/josevnz/pypiserver'
Then use htpasswd to set up a user and password to upload the packages:
htpasswd -c $HOME/.htpasswd josevnz
New password:
Re-type new password:
Adding password for user josevnz
After that, run the container in a detached mode:
$ docker run --detach --name privatepypiserver --publish 8080:8080 --volume ~/.htpasswd:/data/.htpasswd --volume $HOME/pypiserver:/data/packages pypiserver/pypiserver:latest -P .htpasswd --overwrite packages
f95f59a882b639db4509081de19a670fa8fdd93c63c3d4562c89e49e70bf6ee5
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f95f59a882b6 pypiserver/pypiserver:latest "/entrypoint.sh -P .…" 7 seconds ago Up 6 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp privatepypiserver
Confirm it is running by pointing curl or lynx to the new privatepypiserver
:
[josevnz@dmaf5 ~]$ lynx http://localhost:8080 Welcome to pypiserver!
Welcome to pypiserver!
This is a PyPI compatible package index serving 0 packages.
To use this server with pip, run the following command:
pip install --index-url http://localhost:8080/simple/ PACKAGE [PACKAGE2...]
To use this server with easy_install, run the following command:
easy_install --index-url http://localhost:8080/simple/ PACKAGE [PACKAGE2...]
The complete list of all packages can be found here or via the simple index.
This instance is running version 1.4.2 of the pypiserver software.
Next, upload the wheel to your private PyPI server.
Upload the application to a repository with Twine
The most common way to share Python code is to upload it to an artifact manager like Sonatype Nexus or pypiserver using a tool like Twine.
(rpm_query)$ pip install twine
The next step is to set up ~/.pypirc to allow passwordless uploads to the local PyPI server:
[distutils]
index-servers =
pypi
privatepypi
[pypi]
repository = https://upload.pypi.org/legacy/
[privatepypi]
repository = http://localhost:8080/
username = josevnz
It's best to not put the password = XXXX
inside the file. Let Twine ask for it instead. Also, make the configuration accessible only to the owner:
$ chmod 600 ~/.pypirc
Finally, upload the wheel using the twine
command:
(rpm_query) twine upload -r privatepypi dist/rpm_query-0.0.1-py3-none-any.whl
Uploading distributions to http://localhost:8080/
Uploading rpm_query-0.0.1-py3-none-any.whl
100%|██████████████████████████████████
Confirm it was installed with lynx http://localhost:8080/packages/
:
Index of packages
rpm_query-0.0.1-py3-none-any.whl
Commands: Use arrow keys to move, '?' for help, 'q' to quit, '<-' to go back.
Arrow keys: Up and Down to move. Right to follow a link; Left to go back.
H)elp O)ptions P)rint G)o M)ain screen Q)uit /=search [delete]=history list
Install from the local privatepypi server
Wait, don't leave yet. It is time to install the package from your private PyPI server:
First, tell pip to look for packages on the private PyPI server:
$ mkdir --verbose --parents ~/.pip
$ cat<<PIPCONF>~/.pip/.pip.conf
[global]
extra-index-url = http://localhost:8080/simple/
trusted-host = http://localhost:8080/simple/
PIPCONF
To prove this works well, install it to a different virtual environment (or you can override any previous installation with pip install --force
):
$ python3 -m venv ~/virtualenv/test2
$ . ~/virtualenv/test2/bin/activate
(test2)$ pip install --index-url http://localhost:8080/simple/ rpm_query
Looking in indexes: http://localhost:8080/simple/
Collecting rpm_query
Downloading http://localhost:8080/packages/rpm_query-0.0.1-py3-none-any.whl (12 kB)
Collecting rich==9.5.1
Using cached rich-9.5.1-py3-none-any.whl (180 kB)
Collecting pygments<3.0.0,>=2.6.0
Downloading Pygments-2.10.0-py3-none-any.whl (1.0 MB)
|████████████████████████████████| 1.0 MB 5.4 MB/s
Collecting colorama<0.5.0,>=0.4.0
Downloading colorama-0.4.4-py2.py3-none-any.whl (16 kB)
Collecting typing-extensions<4.0.0,>=3.7.4
Downloading typing_extensions-3.10.0.2-py3-none-any.whl (26 kB)
Collecting commonmark<0.10.0,>=0.9.0
Downloading commonmark-0.9.1-py2.py3-none-any.whl (51 kB)
|████████████████████████████████| 51 kB 5.8 MB/s
Installing collected packages: typing-extensions, pygments, commonmark, colorama, rich, rpm-query
Successfully installed colorama-0.4.4 commonmark-0.9.1 pygments-2.10.0 rich-9.5.1 rpm-query-0.0.1 typing-extensions-3.10.0.2
What you've learned
I provided a lot of information here. Here's what I covered:
- Package the application with setuptools.
- Run a private PyPI server using a container.
- Upload the generated wheel package to the private repository using Twine.
- Install the wheel from the private repository instead of a file using pip.
In my next article in this series, I'll explore writing user interface applications in Python.
About the author
Proud dad and husband, software developer and sysadmin. Recreational runner and geek.
More like this
Browse by channel
Automation
The latest on IT automation for tech, teams, and environments
Artificial intelligence
Updates on the platforms that free customers to run AI workloads anywhere
Open hybrid cloud
Explore how we build a more flexible future with hybrid cloud
Security
The latest on how we reduce risks across environments and technologies
Edge computing
Updates on the platforms that simplify operations at the edge
Infrastructure
The latest on the world’s leading enterprise Linux platform
Applications
Inside our solutions to the toughest application challenges
Original shows
Entertaining stories from the makers and leaders in enterprise tech
Products
- Red Hat Enterprise Linux
- Red Hat OpenShift
- Red Hat Ansible Automation Platform
- Cloud services
- See all products
Tools
- Training and certification
- My account
- Customer support
- Developer resources
- Find a partner
- Red Hat Ecosystem Catalog
- Red Hat value calculator
- Documentation
Try, buy, & sell
Communicate
About Red Hat
We’re the world’s leading provider of enterprise open source solutions—including Linux, cloud, container, and Kubernetes. We deliver hardened solutions that make it easier for enterprises to work across platforms and environments, from the core datacenter to the network edge.
Select a language
Red Hat legal and privacy links
- About Red Hat
- Jobs
- Events
- Locations
- Contact Red Hat
- Red Hat Blog
- Diversity, equity, and inclusion
- Cool Stuff Store
- Red Hat Summit