Package: ttally
TL;DR: This converts a file like this (config file at~/.config/ttally.py):# https://github.com/seanbreckenridge/ttallyfromdatetimeimportdatetimefromtypingimportNamedTuple,OptionalclassWeight(NamedTuple):when:datetimepounds:floatclassFood(NamedTuple):when:datetimecalories:intfood:strquantity:floatwater:int# how much ml of water was in this@staticmethoddefattr_validators()->dict:# https://sean.fish/d/ttally_types.py?redirectfrommy.config.seanb.ttally_typesimportprompt_float_default# type: ignore# if I don't supply a quantity, default to 1return{"quantity":lambda:prompt_float_default("quantity")}classEvent(NamedTuple):"""e.g. a concert or something"""event_type:strwhen:datetimedescription:strscore:Optional[int]comments:Optional[str]@staticmethoddefattr_validators()->dict:frommy.config.seanb.ttally_typesimportedit_in_vim# type: ignorereturn{"comments":edit_in_vim}importosfromenumimportEnumwithopen(os.path.join(os.environ["HPIDATA"],"self_types.txt"))asf:SelfTypes=Enum("SelfTypes",[s.rstrip().upper()forsinf])classSelf(NamedTuple):when:datetimewhat:SelfTypes# type: ignoreto (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, and on-the-fly creates and runs an interactive interface like this:... which saves what I enter to a file:-when:1598856786,glasses":2.0ttallyttallyis an interactive module usingautotuito save things I do often to YAML/JSONCurrently, I use this to store info like whenever I eat something/drink water/my current weight/thoughts on concertsGiven aNamedTupledefined in~/.config/ttally.py, this creates interactive interfaces which validates my input and saves it to a fileThe{tuple}-nowaliases set the anydatetimevalues for the prompted tuple to nowThis also gives me{tuple}-recentaliases, 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.0The-recentaliases can acceptallto print all items, or a duration like1dor6hto print data from the last few hours/days.Why/HowGoalsvalidates my user input to basic typesstores it as a user-editable format (YAML)can be loaded into python as typed objectsminimal boilerplate to add a new modelcan be synced across multiple machines without conflictsallow completely custom types or prompts - seeautotui docs,my custom promptsThis intentionally uses YAML and doesn't store the info into a single "merged" database. That way:you can just open the YAML file and quickly change/edit some item, no need to re-invent a CRUD interface (thoughttally edit-recentdoes exist)files can be synced across machines and to my phone usingsyncthingwithout file conflictsprevents issues with trying to merge multiple databases from different machines together (I've tried)The YAML files are versioned with the date/OS/platform, so I'm able to add items on my linux, mac, or android (usingtermux) and sync them across all my devices usingSyncThing. Each device creates its own file it adds items to, 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:>>>frommore_itertoolsimporttake# just to grab a few items>>>fromttally.__main__importext>>>fromttally.configimportFood>>>take(3,ext.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')]... or into JSON usingttally export foodThefrom-jsoncommand 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 tocall this from a scripthpi queryfromHPIcan be used with thettally.__main__module, like:# how many calories in the last day$hpiqueryttally.__main__.food--recent1d-s|jq-r'(.quantity)*(.calories)'|datamashsum12252If you'd prefer to use JSON files, you can set theTTALLY_EXT=jsonenvironment variable.This can load data from YAML or JSON (or both at the same time), every couple months I'll combine all the versioned files to a single merged file using themergecommand:ttally merge foodInstallationpipinstallttallyUsage: 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
  edit-recent   fuzzy select/edit recent items
  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 dataConfigurationYou need to setup a~/.config/ttally.pyfile. You can use the block above as a starting point, or with mine:curl-s'https://sean.fish/d/ttally.py'>~/.config/ttally.pyTo setup aliases; You can do it each time you launch you terminal like:eval"$(python3-mttallygenerate)"Or, 'cache' the generated aliases by putting a block like this in your shell config:TTALLY_ALIASES="${HOME}/.cache/ttally_aliases"if[[!-e"${TTALLY_ALIASES}"]];then# alias file doesn't existpython3-mttallygenerate>"${TTALLY_ALIASES}"# generate and save the aliasesfisource"${TTALLY_ALIASES}"# make aliases available in your shelli.e., it runs the first time I open a terminal, but then stays the same until I remove the fileYou can set theTTALLY_DATA_DIRenvironment variable to the directory thatttallyshould save data to, defaults to~/.local/share/ttally. If you want to use a different path for configuration, you can set theTTALLY_CFGto the absolute path to the file.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.fishCachingttally update-cachecan be used to speedup theexportandrecentcommands:Usage: ttally update-cache [OPTIONS]

  Caches data for 'export' and 'recent' by saving the current data and an
  index to ~/.cache/ttally

  exit code 0 if cache was updated, 2 if it was already up to date

Options:
  --print-hashes  print current filehash debug info
  --help          Show this message and exit.I personally run itonce every 3 minutesin the background, so at least my first interaction withttallyis guaranteed to befastDefault cache directory can be overwritten with theTTALLY_CACHE_DIRenvironment variableSubclassing/ExtensionThe entirettallylibrary/CLI can also be subclassed/extended for custom usage, by usingttally.core.Extensionclass andwrap_clito add additionalclickcommands. For an example, seeflipflop.pyShell Scriptsczlets me fuzzy select something I've eaten in the past usingfzf, like: