interactive module to generate code/aliases to save things I do often
Project description
ttally
TL;DR: This converts a file like this (config file at ~/.config/ttally.py
):
# https://github.com/seanbreckenridge/ttally
from datetime import datetime
from typing import NamedTuple
class Weight(NamedTuple):
when: datetime
pounds: float
class Food(NamedTuple):
when: datetime
calories: int
food: str
quantity: float
water: int # how much ml of water was in this
@staticmethod
def attr_validators() -> dict:
# https://sean.fish/d/ttally_types.py?dark
from seanb.ttally_types import prompt_float_default
# if I don't supply a quantity, default to 1
return {"quantity": lambda: prompt_float_default("quantity")}
class Event(NamedTuple):
"""e.g. a concert or something"""
event_type: str
when: datetime
description: str
score: int | None
comments: str | None
@staticmethod
def attr_validators() -> dict:
# https://sean.fish/d/ttally_types.py?redirect
from seanb.ttally_types import edit_in_vim
return {"comments": edit_in_vim}
from seanb.ttally_self import SelfTypes
class Self(NamedTuple):
when: datetime
what: SelfTypes
to (shell aliases)...
alias event='python3 -m ttally prompt event'
alias event-now='python3 -m ttally prompt-now event'
alias event-recent='python3 -m ttally recent event'
alias food='python3 -m ttally prompt food'
alias food-now='python3 -m ttally prompt-now food'
alias food-recent='python3 -m ttally recent food'
alias self='python3 -m ttally prompt self'
alias self-now='python3 -m ttally prompt-now self'
alias self-recent='python3 -m ttally recent self'
alias weight='python3 -m ttally prompt weight'
alias weight-now='python3 -m ttally prompt-now weight'
alias weight-recent='python3 -m ttally recent weight'
Whenever I run any of those aliases, it inspects the model in the config file, dynamically creates and runs an interactive interface like this:
... which saves some information I enter to a file:
- when: 1598856786,
glasses": 2.0
ttally
is an interactive module using autotui
to save things I do often to YAML
Currently, I use this to store info like whenever I eat something/drink water/my current weight/random thoughts periodically
Given a NamedTuple
defined in ~/.config/ttally.py
, this creates interactive interfaces which validate my input to save information to JSON/YAML files
The {tuple}-now
aliases set the any datetime
values for the prompted tuple to now
This also gives me {tuple}-recent
aliases, which print recent items I've logged. For example:
$ water-recent 5
2021-03-20 18:23:24 2.0
2021-03-20 01:28:27 1.0
2021-03-19 23:34:12 1.0
2021-03-19 22:49:05 1.5
2021-03-19 16:05:34 1.0
Library Usage
The whole point of this interface is that it validates my input to types, stores it as a basic editable format (YAML), but is still loadable into typed python objects, with minimal boilerplate. I just need to add a NamedTuple to ~/.config/ttally.py
, and all the interactive interfaces and resulting YAML files are automatically created
This intentionally uses YAML and doesn't store the info into a single "merged" database. A single database:
- requires some way to edit/delete items - at that point I'm essentially re-implementing a CRUD interface again
- makes it harder to merge them together (I've tried)
YAML isn't perfect but at least I can open it in vim and delete/edit some value. Since the YAML files are pretty-printed, its also pretty trivial to grep/duplicate items by copying a few lines around. Without writing a bunch of code, this seems like the least amount of friction to immediately create new interfaces
The YAML files are versioned with the date/OS/platform, so I'm able to add items on my linux, mac, or android (using termux
) and sync them across all my devices using SyncThing
. Those look like:
food-darwin-seans-mbp.localdomain-2021-03.yaml
food-linux-bastion-2021-03.yaml
food-linux-localhost-2021-04.yaml
... which can then be combined back into python, like:
from more_itertools import take # just to grab a few items
from ttally.autotui_ext import glob_namedtuple
from ttally.config import Food
> take(3, glob_namedtuple(Food))
[Food(when=datetime.datetime(2020, 9, 27, 6, 49, 34, tzinfo=datetime.timezone.utc), calories=440, food='ramen, egg'),
Food(when=datetime.datetime(2020, 9, 27, 6, 52, 16, tzinfo=datetime.timezone.utc), calories=160, food='2 eggs'),
Food(when=datetime.datetime(2020, 9, 27, 6, 53, 44, tzinfo=datetime.timezone.utc), calories=50, food='ginger chai')]
The from-json
command can be used to send this JSON which matches a model, i.e. providing a non-interactive interface to add items, in case I want to call this from a script
hpi query
from HPI
can be used with the ttally.funcs
module, like:
# how many calories in the last day
$ hpi query ttally.funcs.food --recent 1d -s | jq -r '(.quantity)*(.calories)' | datamash sum 1
2252
If you'd prefer to use JSON files, you can set the TTALLY_EXT=json
environment variable.
This can still load data from YAML or JSON (or both), every couple months I'll combine all the versioned files to a single merged file using the merge
command:
ttally merge food
Installation
pip install ttally
Usage: ttally [OPTIONS] COMMAND [ARGS]...
Tally things that I do often!
Given a few namedtuples, this creates serializers/deserializers and an
interactive interface using 'autotui', and aliases to:
prompt using default autotui behavior, writing to the ttally datafile, same
as above, but if the model has a datetime, set it to now, query the 10 most
recent items for a model
Options:
--help Show this message and exit.
Commands:
datafile print the datafile location
edit edit the datafile
export export all data from a model
from-json add item by piping JSON
generate generate shell aliases
merge merge all data for a model into one file
models list models
prompt tally an item
prompt-now tally an item (now)
recent print recently tallied items
update-cache cache export data
Configuration
You need to setup a ~/.config/ttally.py
file. You can use the block above as a starting point, or with mine:
curl -s 'https://sean.fish/d/ttally.py' > ~/.config/ttally.py
You can set the TTALLY_DATA_DIR
environment variable to the directory that ttally
should save data to, defaults to ~/.local/share/ttally
. If you want to use a different path for configuration, you can set the TTALLY_CFG
to the absolute path to the file.
I cache the generated aliases by putting a block like this in my shell config (i.e., it runs the first time I start a terminal, but then stays the same until I remove the file):
TTALLY_ALIASES="${HOME}/.cache/ttally_aliases"
if [[ ! -e "${TTALLY_ALIASES}" ]]; then # alias file doesn't exist
if havecmd ttally; then # if ttally is installed
python3 -m ttally generate >"${TTALLY_ALIASES}" # generate and save the aliases
fi
fi
[[ -e "${TTALLY_ALIASES}" ]] && source "${TTALLY_ALIASES}" # if the file exists, make the aliases available
For shell completion to autocomplete options/model names:
eval "$(_TTALLY_COMPLETE=bash_source ttally)" # in ~/.bashrc
eval "$(_TTALLY_COMPLETE=zsh_source ttally)" # in ~/.zshrc
eval "$(_TTALLY_COMPLETE=fish_source ttally)" # in ~/.config/fish/config.fish
Caching
ttally update-cache
can be used to speedup the export
and recent
commands:
Usage: ttally update-cache [OPTIONS]
Caches data for 'export' and 'recent' by saving the current data and an
index to ~/.cache/ttally
Options:
--print-hashes print current filehash debug info
--help Show this message and exit.
I personally run it once every 5 minutes in the background, so at least my first interaction with ttally
is guaranteed to be fast
Shell Scripts
cz
lets me fuzzy select something I've eaten in the past using fzf
, like:
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.