Frameworks

InvenTree Traefik reverse proxy container labels

This post is based on How to install InvenTree using docker in just 5 minutes and uses the auto-generated docker-compose.yml from there. However it should be useable for almost any setup.

The setup is pretty standard since the inventree proxy container runs the webserver on port 80. Therefore, you don’t even have to explicitly specify a load balancer port

In your docker-compose.yml, add the following labels to the inventree-proxy container:

Wildcard certificate setup:

For more details on the base Traefik setup, see Simple Traefik docker-compose setup with Lets Encrypt Cloudflare DNS-01 & TLS-ALPN-01 & HTTP-01 challenges

labels:
    - "traefik.enable=true"
    - "traefik.http.routers.inventree-mydomain.rule=Host(`inventree.mydomain.com`)"
    - "traefik.http.routers.inventree-mydomain.entrypoints=websecure"
    - "traefik.http.routers.inventree-mydomain.tls.certresolver=cloudflare"
    - "traefik.http.routers.inventree-mydomain.tls.domains[0].main=mydomain.com"
    - "traefik.http.routers.inventree-mydomain.tls.domains[0].sans=*.mydomain.com"

 

Posted by Uli Köhler in InvenTree, Traefik

How to install InvenTree using docker in just 5 minutes

The following script is an automated installation script for InvenTree that fetches the current docker-compose.yml and other configs from GitHub, modifies them so that only local directories are used for storage and then setups InvenTree.

First, create a directory such as /opt/inventree-mydomain. I recommend to choose a unique directory name and not just inventree to keep instae

 

#!/bin/sh
wget -O nginx.prod.conf https://github.com/inventree/InvenTree/raw/master/docker/production/nginx.prod.conf
wget -O docker-compose.yml https://github.com/inventree/InvenTree/raw/master/docker/production/docker-compose.yml
wget -O .env https://github.com/inventree/InvenTree/raw/master/docker/production/.env

sed -i -e 's/#INVENTREE_DB_USER=pguser/INVENTREE_DB_USER=inventree/g' .env
sed -i -e "s/#INVENTREE_DB_PASSWORD=pgpassword/INVENTREE_DB_PASSWORD=$(pwgen 30 1)/g" .env
sed -i -e "s/INVENTREE_WEB_PORT=1337/INVENTREE_WEB_PORT=$(shuf -i 1024-65535 -n 1)/g" .env
sed -i -e "s/#INVENTREE_ADMIN_USER=/INVENTREE_ADMIN_USER=admin/g" .env
sed -i -e "s/#INVENTREE_ADMIN_PASSWORD=/INVENTREE_ADMIN_PASSWORD=$(pwgen 30 1)/g" .env
sed -i -e "s/#INVENTREE_ADMIN_EMAIL=/[email protected]/g" .env
sed -i -e 's/COMPOSE_PROJECT_NAME=inventree-production//g' .env
# Enable cache
sed -i -e "s/#INVENTREE_CACHE_HOST=inventree-cache/INVENTREE_CACHE_HOST=inventree-cache/g" .env
sed -i -e "s/#INVENTREE_CACHE_PORT=6379/INVENTREE_CACHE_PORT=6379/g" .env
# Use direct directory mapping to avoid mounting issues
sed -i -e "s%- inventree_data:%- $(pwd)/inventree_data:%g" docker-compose.yml
# ... now we can remove the volume declarations from docker-compose.yml
sed -i -e '/^volumes:/,$d' docker-compose.yml

sed -z -i -e 's#profiles:\s*- redis\s*##g' docker-compose.yml # Make redis start always, even without docker-compose --profile redis
# Use standard docker-compose directory naming to facilitate multiple parallel installations
sed -z -i -e 's#container_name:\s*[a-zA-Z0-9_-]*\s*##g' docker-compose.yml # Remove container_name: ... statements
# Create data directory which is bound to the docker volume
mkdir -p inventree_data
# Initialize database
docker-compose up -d inventree-cache inventree-db # database initialization needs cache
docker-compose run inventree-server invoke update

After that, you can check .env for the randomly generated  INVENTREE_ADMIN_PASSWORD and INVENTREE_WEB_PORT.

Now you can enable autostart & start the service using systemd, for more details see our post Create a systemd service for your docker-compose project in 10 seconds:

curl -fsSL https://techoverflow.net/scripts/create-docker-compose-service.sh | sudo bash /dev/stdin

Don’t forget to configure your reverse proxy to point to InvenTree.

Posted by Uli Köhler in Docker, InvenTree

How to fix InvenTree API remote_image: Server responded with invalid status code: 403

Problem:

While trying to create a Company using the InvenTree API you see an error message like

requests.exceptions.HTTPError: {'detail': 'Error occurred during API request', 'url': 'https://inventree.techoverflow.net/api/company/', 'method': 'POST', 'status_code': 400, 'body': '{"remote_image":["Server responded with invalid status code: 403"]}', 'headers': {'AUTHORIZATION': 'Token 340fdf063d5433b83bc37c50a4b52ee2f246021b'}, 'params': {'format': 'json'}, 'data': {'name': 'DigiKey', 'website': 'https://www.digikey.de/', 'remote_image': 'https://www.digikey.de/-/media/Images/Header/logo_dk.png', 'description': 'Test', 'is_manufacturer': False, 'is_supplier': True, 'is_customer': False, 'currency': 'EUR'}}

Solution:

The core issue here is:

{"remote_image":["Server responded with invalid status code: 403"]}

InvenTree tries to download the remote_image URL – but that URL can’t be downloaded and the upstream server (digikey.de in this case) generates a 403 response. This is often the case when an URL can only be downloaded with correct Referer headers or other headers set to a specific value.

In order to fix this issue, change the remote_image URL to an image URL which can be fetched correctly.

Sometimes this issue can also arise if the IP address of the InvenTree server is blocked by the server serving the image. This can easily found out by trying to download the given image URL from the server running inventree using wget [URL].

Posted by Uli Köhler in InvenTree

Inventree Python API: How to list all stock locations

See our previous post Inventree Python API: Minimal API connect example using YAML config for our method of creating the api object using a YAML config file!

from inventree.stock import StockLocation

all_stock_locations = StockLocation.list(api)

# Dict of part categories by name
# (e.g. 'OpAmps')
stock_locations_by_name = {
    category["name"]: category
    for category in all_stock_locations
}
# Dict of part categories by public key (e.g. 7)
part_locations_by_pk = {
    category.pk: category
    for category in all_stock_locations
}
# Dict of part categories by hierarchical path
# (e.g. 'Office/Spart parts box')
stock_locations_by_pathstring = {
    category.pathstring: category
    for category in all_stock_locations
}

 

Posted by Uli Köhler in InvenTree, Python

How to manually run InvenTree backup

By default, InvenTree creates a daily backup of both its data files and the database.

You can manually run the backup by using the following command

invoke backup

In a docker-compose-based setup, for example, you can run the backup using

docker-compose run inventree-server invoke restore

Example output:

Loading config file : /home/inventree/data/config.yaml
InvenTree translation coverage: 22%
Restoring InvenTree database...
Finding latest backup
Restoring backup for database 'default' and server 'None'
Restoring: default-de1837564ff0-2023-01-20-004732.psql.bin.gz
Restore tempfile created: 391.4 KiB
Restoring InvenTree media files...
Restoring backup for media files
Finding latest backup
Restoring: de1837564ff0-2023-01-20-004736.tar.gz

 

Posted by Uli Köhler in InvenTree

Which PostgreSQL version to use for InvenTree?

At the time of writing this post (2022-01-20), InvenTree only support PostgreSQL version 11, 12 & 13.

Newer versions of PostgreSQL such as version 14 and 15 are not supported due to dependencies in the backup workflow.

Source: This reddit post

Posted by Uli Köhler in InvenTree

How to access InvenTree backup settings?

In order to access InvenTree’s backup settings, you can go straight to it using

https://inventree.mydomain.com/admin/django_q/schedule/8

or accessing it by opening Admin, then Scheduled tasks and then searching for backup and opening the

InvenTree.tasks.run_backup

task.

By default, InvenTree will perform a full backup daily.

Posted by Uli Köhler in InvenTree

Inventree Python API: How to create a new part

See our previous post Inventree Python API: Minimal API connect example using YAML config for our method of creating the api object using a YAML config file!

The following code finds a part category called ICs and creates a new part in that category.

First, we need to fetch the part categories, this is from our previous post Inventree Python API: Minimal API connect example using YAML config:

from inventree.part import PartCategory

all_categories = PartCategory.list(api)

# Dict of part categories by name
# (e.g. 'OpAmps')
part_categories_by_name = {
    category["name"]: category
    for category in all_categories
}
# Dict of part categories by public key (e.g. 7)
part_categories_by_pk = {
    category.pk: category
    for category in all_categories
}
# Dict of part categories by hierarchical path
# (e.g. 'Electronics-Components/ICs/OpAmps')
part_categories_by_pathstring = {
    category.pathstring: category
    for category in all_categories
}

Now let’s select the correct category:

ics = part_categories_by_name['ICs']

Now it’s finally time to create the part:

from inventree.part import Part

new_part = Part.create(api, {
    'name': 'L78L33ABD',
    'description': '100mA 3.3V fixed LDO regulator, SOIC-8',
    'category': ics.pk
})

You can read the primary key (pk) for the newly generated part using

new_part.pk
Posted by Uli Köhler in InvenTree, Python

Inventree Python API: How to list all part categories

See our previous post Inventree Python API: Minimal API connect example using YAML config for our method of creating the api object using a YAML config file!

from inventree.part import PartCategory

all_categories = PartCategory.list(api)

# Dict of part categories by name
# (e.g. 'OpAmps')
part_categories_by_name = {
    category["name"]: category
    for category in all_categories
}
# Dict of part categories by public key (e.g. 7)
part_categories_by_pk = {
    category.pk: category
    for category in all_categories
}
# Dict of part categories by hierarchical path
# (e.g. 'Electronics-Components/ICs/OpAmps')
part_categories_by_pathstring = {
    category.pathstring: category
    for category in all_categories
}

Note that this code by itself does not take into account the hierarchy of the categories.

Example output

part_categories_by_name:

{'Elektronik-Komponenten': <inventree.part.PartCategory at 0x7fd735125510>,
 'ICs': <inventree.part.PartCategory at 0x7fd7356d1720>,
 'OpAmps': <inventree.part.PartCategory at 0x7fd735ead4e0>}

part_categories_by_pk:

{1: <inventree.part.PartCategory at 0x7fd7346b5db0>,
 2: <inventree.part.PartCategory at 0x7fd73438aa10>,
 3: <inventree.part.PartCategory at 0x7fd727325b40>}

part_categories_by_pathstring:

{'Elektronik-Komponenten': <inventree.part.PartCategory at 0x7fd727bd3040>,
 'Elektronik-Komponenten/ICs': <inventree.part.PartCategory at 0x7fd727bd1db0>,
 'Elektronik-Komponenten/ICs/OpAmps': <inventree.part.PartCategory at 0x7fd727bd1060>}

 

Posted by Uli Köhler in InvenTree, Python

Inventree Python API: Minimal API connect example using YAML config

This example allows you to create a config file inventree.yml:

server: 'https://inventree.mydomain.com'
username: 'admin'
password: 'Chuiquu5Oqu8il5ahLoja4aecai4ee'

and then read it in Python and connect to the Inventree API:

from inventree.api import InvenTreeAPI

# Load config
import yaml

with open("inventree.yml", "r") as file:
    config = yaml.safe_load(file)
    
api = InvenTreeAPI(config["server"], username=config["username"], password=config["password"])

 

Posted by Uli Köhler in InvenTree, Python

How to setup & use WordPress REST API authentication using Python

WordPress authentication plugin setup

First, install the WordPress REST API Authentication wordpress plugin, which you can find by searching for WordPress REST API Authentication:

Then you need to open the plugin configuration page. Open Plugins in the WordPress admin panel, locate the  WordPress REST API Authentication plugin and click Configure

Select Basic Authentication:

Then click Next on the top right:

and click Finish on the next page:

Setup in Python

Assuming you have a WordPress user admin with password abc123 we can modify our code from How to get WordPress posts as JSON using Python & the WordPress REST API in order to query a non-public endpoint:

import requests
import base64

# Compute basic authentication header
auth_header = b"Basic " + base64.b64encode(b"admin:abc123")

# posts is a list of JSON objects, each representing a post
posts = requests.get("https://mydomain.com/wp-json/wp/v2/posts",
                     params={"context": "edit"},
                     headers={"Authorization": auth_header}).json()

 

Posted by Uli Köhler in Python, Wordpress

WordPress backup script using bup (bup remote)

This script backups a WordPress installation (including data,base files & directories, excluding cache) to a bup remote server running on 10.1.2.3. You need to ensure passwordless access to that server.

It is based on automated extraction of database host, username & password, see How to grep for WordPress DB_NAME, DB_USER, DB_PASSWORD and DB_HOST in wp-config.php for more details.

#!/bin/bash
export NAME=$(basename $(pwd))
export BUP_DIR=/var/bup/$NAME.bup
export REMOTE_BUP_DIR=/bup-backups/$NAME.bup
export REMOTE_SERVER=10.1.2.3
export BUP_REMOTE=$REMOTE_SERVER:$REMOTE_BUP_DIR

# Init
bup -d $BUP_DIR init -r $BUP_REMOTE
# Save MariaDB dump (extract MariaDB config from wp-config.php)
DB_NAME=$(grep -oP "define\(['\"]DB_NAME['\"],\s*['\"]\K[^'\"]+(?=[\'\"]\s*\)\s*;)" wp-config.php)
DB_USER=$(grep -oP "define\(['\"]DB_USER['\"],\s*['\"]\K[^'\"]+(?=[\'\"]\s*\)\s*;)" wp-config.php)
DB_PASSWORD=$(grep -oP "define\(['\"]DB_PASSWORD['\"],\s*['\"]\K[^'\"]+(?=[\'\"]\s*\)\s*;)" wp-config.php)
DB_HOST=$(grep -oP "define\(['\"]DB_HOST['\"],\s*['\"]\K[^'\"]+(?=[\'\"]\s*\)\s*;)" wp-config.php)
mysqldump -h$DB_HOST -u$DB_USER -p$DB_PASSWORD $DB_NAME | bup -d $BUP_DIR split -n $NAME-$DB_NAME.sql

# Save wordpress directory
bup -d $BUP_DIR index --exclude wp-content/cache --exclude wp-content/uploads/cache . && bup save -r $BUP_REMOTE -9 --strip-path $(pwd) -n $NAME .

# OPTIONAL: Add par2 information
#   This is only recommended for backup on unreliable storage or for extremely critical backups
#   If you already have bitrot protection (like BTRFS with regular scrubbing), this might be overkill.
# Uncomment this line to enable:
# bup on $REMOTE_SERVER -d $REMOTE_BUP_DIR fsck -g

# OPTIONAL: Cleanup old backups
bup on $REMOTE_SERVER -d $REMOTE_BUP_DIR prune-older --keep-all-for 1m --keep-dailies-for 6m --keep-monthlies-for forever -9 --unsafe

 

Posted by Uli Köhler in bup, Wordpress

How to get all WordPress posts as JSON using Python & the WordPress REST API

In our previous post How to get WordPress posts as JSON using Python & the WordPress REST API we showed how to fetch a single page of 10 posts using the WordPress REST API in Python.

In this post, we’ll use the pagination in order to fetch a list of all the posts.

Firstly, we observe that once we query an invalid page such as ?page=1000000, the returned JSON will be

{'code': 'rest_post_invalid_page_number',
 'message': 'The page number requested is larger than the number of pages available.',
 'data': {'status': 400}}

instead of the JSON array representing the list of posts.

Using this information, we can write a fetcher that fetches pages of 100 posts each until this error message is encountered:

from tqdm import tqdm
import requests

def page_numbers():
    """Infinite generate of page numbers"""
    num = 1
    while True:
        yield num
        num += 1

posts = []
for page in tqdm(page_numbers()):
    # Fetch the next [pagesize=10] posts
    posts_page = requests.get("https://mydomain.com/wp-json/wp/v2/posts", params={"page": page, "per_page": 100}).json()
    # Check for "last page" error code
    if isinstance(posts_page, dict) and posts_page["code"] == "rest_post_invalid_page_number": # Found last page
        break
    # No error code -> add posts
    posts += posts_page

 

 

Posted by Uli Köhler in Python, Wordpress

How to get WordPress posts as JSON using Python & the WordPress REST API

You can use the requests library to fetch the posts as JSON from /wp-json/wp/v2/posts

On the wordpress site, you typically don’t need to configure this – the JSON API is enabled by default and accessing publically available info can be done without authentication.

import requests

# posts is a list of JSON objects, each representing a post
posts = requests.get("https://mydomain.com/wp-json/wp/v2/posts").json()

This will, by default, fetch the most recent 10 posts. See TODO for more info on how to fetch all posts using pagination

Posted by Uli Köhler in Python, Wordpress

Free wordpress plugin for searching categories while editing posts & pages

WP Admin Category Search is a free search plugin for searching categories while editing your posts or pages. It allows you to quickly find categories without scrolling through a long list of categories:

Step 1: Install WP Admin Category Search

Go to Plugins -> Install and search for WP Admin Category Search. Install and activate it by clicking the button

Step 2: When editing posts, search for categories…

Step 3: … and find and select them

Posted by Uli Köhler in Wordpress

How to configure SMTP E-Mail for InvenTree (docker/docker-compose)

You can easily configure SMTP email for InvenTree by adding the following config to your .env file (I’m using the docker production config):

INVENTREE_EMAIL_HOST=smtp.mydomain.com
[email protected]
INVENTREE_EMAIL_PASSWORD=cheen1zaiCh4yaithaecieng2jazey
INVENTREE_EMAIL_TLS=true
[email protected]

Even after setting up InvenTree, it is sufficient to just add this config to the .env file and restarting the server.

Posted by Uli Köhler in Docker, InvenTree

How to layer/composite multiple images using imagemagick (convert)

In order to layer two images a.png and b.png using convert, use this syntax:

convert a.png b.png -composite out.png

In order to layer three images a.png, b.png and c.png using convert, use this syntax:

convert a.png b.png -composite c.png -composite out.png

In order to layer four images a.png up to d.png using convert, use this syntax:

convert a.png b.png -composite c.png -composite d.png -composite out.png         

In general, you need to list the first two input images without -composite, and then list every other filename including the output filename using -composite.

Posted by Uli Köhler in ImageMagick

How to convert color to transparency using ImageMagick

You can use ImageMagick to convert a given color to transparency:

convert in.png -fuzz 10% -transparent #ffffff out.png

The fuzz parameter tells ImageMagick to also convert colors within a 20% tolerance range to alpha. This is especially important for JPEG images containing compression artifacts, i.e. pixels that are not purely white. In practice, you’ll often need to play around with the fuzz parameter to select the right value.

Example:

Applied to the following image:

using

convert Transparency-example.png -fuzz 10% -transparent #ffffff out.png

you’ll generate a out.png like this:

 

Posted by Uli Köhler in ImageMagick

Recommended local Google Font hosting plugin for WordPress

I tested different local Google Fonts (GPDR) plugins and for some websites especially with Elementor/WPBakery etc, out of all plugins tested, only the OMGF (optimize my google fonts) plugin really worked in removing all fonts for GPDR compliance.

Therefore, I can recommend installing OMGF specifically, even though most other plugins like Self hosted Google Fonts will work for most websites.

You can installing it by opening the WordPress admin panel, clicking Plugins, clicking Install, and then entering OMGF in the search field.

Posted by Uli Köhler in Wordpress

How to fix WordPress docker image upload size 2M limit

Problem:

You are running your WordPress instance using the official WordPress Apache image.

However, the WordPress Media page has a maximum upload size of 2 Megabytes.

Solution:

This setting is configured in the php.ini used by the WordPress docker image internally. While it is possible to use a custom php.ini, it’s much easier to edit .htaccess . Just edit .htaccess in the wordpress directory where wp-config.php is located and append this after # END WordPress to set the upload limit to 256 Megabytes:

php_value upload_max_filesize 256M
php_value post_max_size 256M
php_value max_execution_time 300
php_value max_input_time 300

The change should be effective immediately after reloading the page. Note that you still might need to configure your reverse proxy (if any) to allow larger uploads. My recommendation is to just try it out as is and if large uploads fail, it’s likely that your reverse proxy is at fault.

Full .htaccess example:

# BEGIN WordPress
# Die Anweisungen (Zeilen) zwischen „BEGIN WordPress“ und „END WordPress“ sind
# dynamisch generiert und sollten nur über WordPress-Filter geändert werden.
# Alle Änderungen an den Anweisungen zwischen diesen Markierungen werden überschrieben.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^en/wp-login.php /wp-login.php [QSA,L]
RewriteRule ^de/wp-login.php /wp-login.php [QSA,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

php_value upload_max_filesize 256M
php_value post_max_size 256M
php_value max_execution_time 300
php_value max_input_time 300

 

Posted by Uli Köhler in Container, Docker, Wordpress