Things that live here:

  1. Work log, where I note things I feel I'll have to Google later.
  2. Journal, very similar but about non-IT topics.
  3. Blog for rare longer-form posts (last one below).
  4. Link wiki, almost abandoned, formerly a way to track links.

Feel free to look at what you can find here and enjoy yourself.

Latest posts from the Work log

Day 1067 / 211202-0008 Hugo and HTML anchors

Hugo generates anchors from headers automatically 1. Tested it - yes, except they’re lowercased (which makes sense).

As a refresher, in HTML it’s

<h2 id="anchor">..</h2>
<a name="anchor"></a>

<a href="#anchor">anchor link </a>

  1. Creating anchors in Hugo pages [SOLVED] - support - HUGO ↩︎

Day 1066 / 211201-1637 mypy and python typing

One additional way to check the type hints in #python is mypy, installable as python package.

mypy -p package_name checks the typing in the package, and found some potential errors in corner cases I didn’t know about in one of the projects I’m working on!

Finds wrong typing, missing/wrong return values, that kind of stuff.

It doesn’t like what: str or Path typing output, I guess only Union[str, Path] - is there a reason for it?

In any case I like it more than Pycharm’s way of outputting things and will be using it along with black and flake8 in the future (along with typing itself).


Day 1065

211130-1925 providing user and pass to wget through teamcity

Tried to download a Teamcity artifact through wget, and apparently you can if you provide a user/pass through wget!

I assume it’s HTTP auth or something

wget --user username --password my-password https://teamcity.location/repository/download/....

211130-1751 git rebase on branch vs origin-branch + git fetch

Had issues, asked for help, and then learned a lot of stuff.

git rebase branchname != git rebase origin/branchname!

The first one is about the current local state of the branch, the second one about the one on remote.

BUT the one on remote as known by local != one on remote-remote! You need to update first!

git fetch --all or whatever.

I’d previouly update / pull before through PyCharm before doing that, and this abstracted all of this away from me.

Day 1064 / 211129-0023 obsidian console

To access the #obsidian console, <C-S-i> worked. It was the standard “Dev tools”.1

  1. How to access the console? - Help - Obsidian Forum ↩︎

Day 1063 / 211128-2120 simple-scan for scanning

Since I seem to keep forgetting: simple-scan is the program I use to talk to scanners. You can select various options (scan document, photo etc).

Keeps #scanning in the exact same PDF document until you break it.

Day 1061 / 211126-1301 pycharm pinning tabs

In #pycharm, “Pin tab” exists! But then it’s not “Tab 1” etc anymore and I can’t use my shortcuts

Day 1059

211124-1744 argparse notes

(Those too after a long talk to MO at work, this time #py/argpars e)

Cool things about argparse:1

  • parser.add_argument('--two-words') would automatically map to args.two_words (_ vs -)!
  • One can provide complex types!2 For files, two options.
    • The first one allows to set file permissions etc., but it opens them and returns the handle to you, which you may not want.
    • pathlib.Path() works as expected, and even automagically parses string paths from args into the Path!
      • Additionally we can then establish that we’re working with Paths from the very beginning, getting rid of the str or Path ambiguity.
      • “Be strict and clear from the very beginning, then you don’t have to deal Path or str”

    • Sample of both from official documentation:
      parser.add_argument('a', type=argparse.FileType('w', encoding='latin-1'))
      parser.add_argument('b', type=pathlib.Path)
  • You can get defalut values from os.environ()! Then you can also run it as
    WHATVEER_VALUE=234 python3

A nice structure for it all is:

  1. if __name__ == '__main__': runs a function like main() getting rid of the scope issues
  2. Parsing is done my a separate function, that returns the Namespace:
    def parse_args() -> argparse.Namespace:
        parser = argparse.ArgumentParser()
        parser.add_argument('--input-directory' ..)
        return parser.parse_args()
  3. Then in main() we use it like args = parse_args(); if args.input_directory == ... This is nice also because then we don’t have to deal with an arparse object in main, just its results.

Also, in general, CLI programs have arguments like program --arg-one, not program --arg_one. I write the latter one because I still feel I’m in a python world, but Python would parse such dashed arguments into classic ones (see above). TODO look for some best practices for CLI programs, including Python ones, POSIX etc etc etc.

  1. argparse — Parser for command-line options, arguments and sub-commands — Python 3.10.0 documentation ↩︎

  2. argparse — Parser for command-line options, arguments and sub-commands — Python 3.10.0 documentation ↩︎

211124-1731 python logging setup

Really nice workshop-thing by MO at work about #py/logging

Naming loggers after the package / files

Logger names can be used to cleanly output and separate them.

Assuming one has a package with multiple files/subfolders in it, it’s possible to give each one their own logger, like this:

In the main file of the package:

logger = logging.getLogger(__package__)

In all the other files:

logger = logging.getLogger(__name__)

That way paths ./package/ lead to loggers named like package.my_module that map the semantical and the directory structure.

Changing settings of the loggers

In a setup above, one can then easily change the settings of the loggers referring to them by their names.

Configuring logging: Logging HOWTO — Python 3.10.0 documentation

Changing loglevel is easy from code,

if args.debug:

logging.config allows to change the config from ini-like config files. Two main ways: logging.config.fileConfig reads ini-like config files, logging.config.dictConfig 1 from dictionaries.

Sample .yaml that when converted to dict would change the loglevel of different loggers:

version: 1
		level: DEBUG
		level: DEBUG

These loggers can even include external ones!

  1. logging.config — Logging configuration — Python 3.10.0 documentation ↩︎

Day 1058

211123-2348 poetry for package management

Short notes about #py/poetry for package management

poetry new packagename creates a poetry project

From within the folder with the package:

  • poetry install == pip3 install -r requierements.txt
  • poetry shell == source .venv/bin/activate
  • exit == deactivate

Basic usage | Documentation | Poetry - Python dependency management and packaging made easy:

  • venvs live in {cache-dir}/virtualenvs, which on my box is /home/me/.cache/pypoetry/virtualenvs/ptest-eeSDLvcF-py3.6/bin/activate
  • poetry.lock caches the resolved packages once we install things once.
    • Must mach pyproject.toml, a warning will be shown otherwise
    • It’s important to commit it to a VCS! It has the exact versions it resolves, beneficial for everyone to use them
  • poetry update updates everything to the latest versions, overwriting poetry.lock
  • poetry init initializes a project and creates a pyproject.toml interactively, allowing even to search for packages etc!

Adding packages:

  • poetry add yaml adds a package
  • poetry search yaml looks for packages in remote repos! Will tell you that you actually want pyyaml

211123-2345 python packaging

Providing a along with makes the package itself executable:

$ python -m module_name would have an usual if __name__ == "__main__" block and run stuff imported from other files of that package.

211123-2333 python packaging and poetry

(From MO’s python riddle at work)

Things declared in if __name__ == '__main__' are in global scope. Not because it’s special, but because scope. All these bugs go away if you move main() to a separate function.

Code from SO answer:[^2]

In main:

>>> if __name__ == '__main__':
...     x = 1
... print 'x' in globals()

Inside a function:

>>> def foo():
...     if __name__ == '__main__':
...         bar = 1
... foo()
... print 'bar' in globals()

Python doesn’t have block-local scope, so any variables you use inside an if block will be added to the closest enclosing “real” scope.

Someone mentioned that if __name__ == '__main__' can happen anywhere in the code. Never thought about this

211123-2333 python packaging and poetry

(From MO’s python riddle at work)

Things declared in if __name__ == '__main__' are in global scope. Not because it’s special, but because scope.

Code from SO answer:[^2]

In main:

>>> if __name__ == '__main__':
...     x = 1
... print 'x' in globals()

Inside a function:

>>> def foo():
...     if __name__ == '__main__':
...         bar = 1
... foo()
... print 'bar' in globals()

Python doesn’t have block-local scope, so any variables you use inside an if block will be added to the closest enclosing “real” scope.

Someone mentioned that if __name__ == '__main__' can happen anywhere in the code. Never thought about this

211123-2122 obsidian undeleting files

If sync is enabled, in settings -> Sync there’s a “Deleted files” with versions and actions.

If not, unless a setting is set to delete to Obsidian’s trash, it’s left to the filesystem, so trash can or extundelete in my case or whatever.

211123-1558 nix data science overlay

Link by M.O.: nix-community/nix-data-science: Standard set of packages and overlays for data-scientists [maintainer=@tbenst]

Day 1057

211122-0905 detectron Instances initialization

Detectron’s Instances object gets created like this, creating attributes with names unknown initially:

def __init__(self, image_size: Tuple[int, int], **kwargs: Any):
        image_size (height, width): the spatial size of the image.
        kwargs: fields to add to this `Instances`.
    self._image_size = image_size
    self._fields: Dict[str, Any] = {}
    for k, v in kwargs.items():
        self.set(k, v)

Which is neat.

To create an Instances object for unit tests I did:

pred_boxes = Boxes(tensor(
    [ 143.8892, 1166.6632, 1358.7292, 1411.6588],
    [ 131.3727,  864.3126, 1355.7804, 1144.3668],
    [ 585.6373,  747.7184,  922.6433,  815.9998]
scores = tensor(
    [0.9971, 0.9967, 0.9938]
pred_classes = tensor([3, 3, 3])

instances = Instances(
    image_size=(2122, 1500),

211122-0256 quickly forming an URI in markdown

Found this in old markdown code from my old blog, I guess I forgot about this:


Day 1056

211121-2201 vim opening more than 10 tabs

When opening a lot of files as vim -p *.md* only 10 kept being opened, finally googled it.

Solution: adding set tabpagemax=50 to ~/.vimrc

211121-2137 replace a string in all files recursively

From SO1:

find . -name '*.php' -exec sed -i -e 's/' {} \;

  1. bash - Replace a string with another string in all files below my current dir - Stack Overflow ↩︎

211121-2123 git undoing git add unstaging files

title: “211121-2123 Undoing git add / unstaging files” tags:

  • “zc”
  • “zc/it”
  • “git” fulldate: 2021-11-21T21:11:47+0100 date: 2021-11-21 layout: post hidden: false draft: false

Two different questions here! Both options are: 1 If you add a file for the first time, git rm --cached . or git -rm -r --cached . will reverse that.

If you want to un-add changes to a file that’s already in the repo, git reset <file> / git reset will undo that.

  1. How do I undo ‘git add’ before commit? - Stack Overflow ↩︎

Latest post from Blog

Attempting to parse Obsidian tags with Templater templates


After more than a month of use, I’m still totally completely absolutely in love with Obsidian.

The standard scenario for any new technology I get excited about is something like

  1. Discover something really cool
  2. Read the entire Internet about it
  3. Try to use it to solve all my problems, to see what sticks
  4. After a week or so, either I stop using it completely, or I keep using it for a narrow use-case. I consider the latter a success.

I think this is the first time the opposite happened: I started to play with Obsidian as a way to quickly sync notes between my computer and phone (better than one-person Telegram groups or Nextcloud or Joplin or…), then started using it to write the Diensttagebuch, then realized I can drag-n-drop pictures/PDFs into it and can use it to keep my vaccination certificate and other stuff I need often on my phone, then oh nice it’s also a markdown editor, and we’re not done yet.

I’d usually avoid closed-source solutions, but it has everything I could ask for except that:

  • a really active community, with blog posts being written, repositories, plugins etc being constantly developed by very different people
  • notes being stored markdown, which then can be version-controlled and edited using any of my existing tools
    • this is much better than being able to “export” them to markdown, one less step to do (or forget, or easily disable). Fits seamlessly into the backup strategies being used.
  • Downloadable program that works even without an Internet connection, even if Obsidian HQ gets hit by a meteorite
  • Obsidian themselves having paid options1, which means a chance, clear plan and incentive to survive, and clear ways to support them. Better than whatever I’d be able to do with an abandoned open source project I rely on. (That said, I’d love them to open source at least the client at some point.)

This mix between personal-and-not-personal stuff, and having both my phone and my laptop as first-class citizens, is something I’ve never had before, and something I like, then if I also try to make it into my published blog I’m bound to find edge cases, one of them being tags. That said until now, and including this bit, everything I tried to do worked seamlessly.

This post describes my attempt to set up tags in a way that both Obsidian’s native autocompletion/search AND Hugo’s tagging work.

Two types of tags

This blog is written in markdown and converted into a static website with Hugo. Both the Diensttagebuch and the journal take files from specific subfolders in a specific Obsidian vault, and get converted into Hugo-friendly markdown using Obyde.

There are two kinds of tags as a result of that:

  • Obsidian’s #tags in the body of the text. They are the main kind supported, they get autocompleted, various #tag/subtag options can happen, etc. They get ignored by obyde/Hugo during conversion.
  • Front matter tags. They look like this in yaml/obsidian:
    Obsidian metadata
    They are parsed by Obsidian, as in searching by them works, but adding them manually is painful and has no autocompletion. One needs to set the ‘metadata’ to be shown in settings for them to be displayed.
    They are the only ones understood by Obyde/Hugo, though.

I decided to write something that takes care of this for me easily.

Obsidian templates and Templater

Logical first step was Templater, which is a plugin for Obsidian I already use for my templating needs, such as generating a YAML frontmatter based only on a filename (see Day 1021).

I wanted:

  • obsidian’s #tags to become part of the yaml frontmatter
  • optionally - adding a line at the end of the file, like “Tags: #one #two #three” with the tags found in the frontmatter but not in the body.

I found a template2 doing pretty much this in the Template Showcase. It uses purely Obsidian’s/Templater’s JS/Templating language and is very close to what I want - but not exactly, and somehow I didn’t want to write more Javascript than needed.

My solution is more of a hack, less portable, and uses Python, because that’s what I know how to write best.

My solution

Python script to parse/add the tags to frontmatter

I wrote a (quick-n-dirty) Python script that:

  • gets the tags from the frontmatter of the input .md file
  • gets the tags found by Obsidian from an environment variable
  • finds tags found by obsidian but not in frontmatter
  • rewrites the .md file with all the tags added to the frontmatter.
from frontmatter import Frontmatter
from pathlib import Path
import os
import argparse
import yaml

ENV_VAR_TAGS = 'tags'
TAGS_LINE = "\nTags: "
IGNORE_TAGS = {"zc"}

def get_args() -> argparse.Namespace:
  parser = argparse.ArgumentParser(description='Remove tags')
  parser.add_argument('--input_file', type=Path, help='Input file')
  parser.add_argument('--print', default=False, action="store_true", help='if set, will print to screen instead of rewriting the input file')
  parser.add_argument('--add_new_tagline', default=False, action="store_true", help='if set, will create/edit a "Tags: " line at the end of the file with tags found in front matter but not text body')
  return parser.parse_args()

def str_tags_to_set(tags: str) -> set:
  Converts tags like "#one,#two,#three" into a set
  of string tags without the "#"s:
    {'one', 'two', 'three'}
  def parse_tag(tag: str):
    ret_tag = tag.lower() if TAGS_TO_LOWER else tag
    return ret_tag[1:]

  return_set = set([parse_tag(x) for x in tags.split(",")])
  return return_set

def set_tags_to_str(tags: set, ignore_tags: set = IGNORE_TAGS) -> str:
  The opposite of str_tags_to_set, returns space-separated "#tag1 #tag, ..".
  Ignores tags that contain even part of any ignore_tags.

  final_tags = ''
  for tag in tags:
    for i in ignore_tags:
      if i in tag:
      tag: str = tag.lower()
    final_tags+=f"#{tag} "
  return final_tags

def line_is_tag_line(line: str) -> bool:
  return line[0:len(TAGS_LINE)] == TAGS_LINE

def get_tags_in_tagline(tagline: str) -> set:
  words = tagline.split(" ")
  return {w for w in words if w[0]=="#"}

def main() -> None:
  args = get_args()
  input_file = args.input_file

  # input tags
  tags_frontmatter = set()
  tags_all = set()
  tags_obs = set()

  # output tags
  missing_obs_tags = set()
  missing_yaml_tags = set()

  parsed_yaml_fm = Frontmatter.read_file(input_file)
  frontmatter_dict = parsed_yaml_fm['attributes']
  post_body = parsed_yaml_fm['body']

  has_tags_in_fm = 'tags' in frontmatter_dict

  # all tags (yaml + #obsidian)
  env_tags_by_obsidian = os.getenv(ENV_VAR_TAGS)
  tags_all = str_tags_to_set(env_tags_by_obsidian)

  # tags in yaml frontmatter 
  tags_frontmatter = set()
  if has_tags_in_fm:
    tags_frontmatter = set(frontmatter_dict['tags'])

  # "obsidian" tags (basically #tags in the text)
  tags_obs = tags_all.difference(tags_frontmatter)

  # print(f"{input_file}: \n\
  #     all_tags: {tags_all}\n \
  #     obs_tags: {tags_obs}\n \
  #     fm_tags: {tags_frontmatter}\n"
  #     )

  # tags found in frontmatter but not in #obsidian
  missing_obs_tags = tags_frontmatter.difference(tags_obs)
  # #obsidian tags not found in frontmatter
  missing_fm_tags = tags_obs.difference(tags_frontmatter)

  if missing_fm_tags:
    if not has_tags_in_fm:
      frontmatter_dict['tags'] = list()

  if missing_obs_tags and args.add_new_tagline:
    final_tags = set_tags_to_str(missing_obs_tags)

    # If last line is "Tags: "
    last_line = post_body.splitlines()[-1]
    if line_is_tag_line(last_line):
      # tags_in_tl = get_tags_in_tagline(last_line)
      # Remove last "\n" in post body
      post_body = post_body[:-1]
      # Add the missing tags to the last line
      post_body += " " + final_tags + "\n"
      # If we have no "Tags: " line, we add one
      final_string = TAGS_LINE + final_tags

  new_fm_as_str = "---\n" + yaml.dump(frontmatter_dict)  + "\n---"
  final_file_content = new_fm_as_str + "\n" + post_body

  # print(final_file_content)
  if args.print:

if __name__ == "__main__":

Obsidian templates

To use the python file, I added to Templater a “System command user function” 3 add_tags with this code:

python3 _templates/tags/ --input_file "<% tp.file.path() %>" 

It calls the python file and passes to it the location of the currently edited file.

Then I create the template that calls it:

<% tp.user.add_tags({"tags": tp.file.tags}) %>

tp.file.tags returns all (all) tags found by Obsidian, so #body tags and frontmatter ones.

They get passed to the python script as an environment variable, canonical way to do this as per Templater docu4.

Initially I tried to pass them as parameter, but tp.file.tags passed in the system command user function always returned an empty list.


I write the text in Obsidian as usual, adding any #body tags I like, then at the end run that template through the hotkey I bound it to (<C-S-a>), done.

Better ways to do this

Problems with this solution:

  • won’t work on Android
  • too many moving parts

Ways to improve it:

  • It’s possible to do this without Python. The template I found2 uses JS to find the editor window, then the text in it, then the YAML node. Though intuitively I like parsing the YAML as YAML more.
  • I wrote it with further automatization in mind, for example running it in so that it parses and edits all the markdown files in one go. This would mean splitting the thing by words, taking care of correctly grouping #tags-with/subtags and not looking inside code blocks etc. Not sure here. I’d still have Obsidian as ultimate source of truth about what it considers a tag and what not.

What now?

This is the first long-ish blog post I’ve written in a while. Feels awesome. Let’s see if I can get this blog started again.

  1. Pricing - Obsidian ↩︎

  2. Gather up tags and format for YAML frontmatter · Discussion #140 · SilentVoid13/Templater ↩︎

  3. System Command User Functions | Templater ↩︎

  4. System Command User Functions | Templater ↩︎