{"id":17823233,"url":"https://github.com/purarue/ttally","last_synced_at":"2025-07-24T16:42:07.967Z","repository":{"id":43904387,"uuid":"349433621","full_name":"purarue/ttally","owner":"purarue","description":"an interactive TUI to save things I do often (a calorie counter, concerts I go to, my weight, anything else)  to JSON/YAML","archived":false,"fork":false,"pushed_at":"2025-01-30T04:42:54.000Z","size":621,"stargazers_count":10,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-16T13:16:57.753Z","etag":null,"topics":["json","prompt-toolkit","tui"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/ttally/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/purarue.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-19T13:32:03.000Z","updated_at":"2025-01-30T04:42:57.000Z","dependencies_parsed_at":"2023-09-28T10:10:57.045Z","dependency_job_id":"40c0ac90-aba8-4745-b0ae-f368c11baecb","html_url":"https://github.com/purarue/ttally","commit_stats":{"total_commits":147,"total_committers":1,"mean_commits":147.0,"dds":0.0,"last_synced_commit":"39e943138cc8309b34f38c3dc6dd454bdb172ea2"},"previous_names":["seanbreckenridge/ttally"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fttally","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fttally/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fttally/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/purarue%2Fttally/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/purarue","download_url":"https://codeload.github.com/purarue/ttally/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243871914,"owners_count":20361380,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json","prompt-toolkit","tui"],"created_at":"2024-10-27T17:57:07.403Z","updated_at":"2025-03-18T16:30:22.531Z","avatar_url":"https://github.com/purarue.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"**TL;DR**: This converts a file like this (config file at `~/.config/ttally.py`):\n\n```python\nfrom datetime import datetime\nfrom typing import NamedTuple, Optional\n\n\nclass Weight(NamedTuple):\n    when: datetime\n    pounds: float\n\n\nclass Food(NamedTuple):\n    when: datetime\n    calories: int\n    food: str\n    quantity: float\n    water: int  # how much ml of water was in this\n\n    # specify a special way to prompt for quantity\n    @staticmethod\n    def attr_validators() -\u003e dict:\n        # https://purarue.xyz/d/ttally_types.py?redirect\n        from my.config.pura.ttally_types import prompt_float_default  # type: ignore\n\n        # if I don't supply a quantity, default to 1\n        return {\"quantity\": lambda: prompt_float_default(\"quantity\")}\n\n\nclass Event(NamedTuple):\n    \"\"\"e.g. a concert or something\"\"\"\n\n    event_type: str\n    when: datetime\n    description: str\n    score: Optional[int]\n    comments: Optional[str]\n\n    @staticmethod\n    def attr_validators() -\u003e dict:\n        from my.config.pura.ttally_types import edit_in_vim  # type: ignore\n\n        return {\"comments\": edit_in_vim}\n\n\nimport os\nfrom enum import Enum\n\n# dynamically create an enum using each line of the file as an option\nwith open(os.path.expanduser(\"~/Documents/.self_types.txt\")) as f:\n    SelfT = Enum(\"SelfT\", [s.rstrip().upper() for s in f])\n\n\nclass Self(NamedTuple):\n    when: datetime\n    what: SelfT  # type: ignore\n```\n\nto (shell aliases)...\n\n```\nalias event='python3 -m ttally prompt event'\nalias event-now='python3 -m ttally prompt-now event'\nalias event-recent='python3 -m ttally recent event'\nalias food='python3 -m ttally prompt food'\nalias food-now='python3 -m ttally prompt-now food'\nalias food-recent='python3 -m ttally recent food'\nalias self='python3 -m ttally prompt self'\nalias self-now='python3 -m ttally prompt-now self'\nalias self-recent='python3 -m ttally recent self'\nalias weight='python3 -m ttally prompt weight'\nalias weight-now='python3 -m ttally prompt-now weight'\nalias weight-recent='python3 -m ttally recent weight'\n```\n\nWhenever 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:\n\n\u003cimg src=\"https://raw.githubusercontent.com/purarue/autotui/master/.assets/builtin_demo.gif\"\u003e\n\n... which saves what I enter to a file:\n\n```yaml\n- when: 1598856786,\n  glasses\": 2.0\n```\n\n## ttally\n\n`ttally` is an interactive module using [`autotui`](https://github.com/purarue/autotui) to save things I do often to YAML/JSON\n\nCurrently, I use this to store info like whenever I eat something/drink water/my current weight/thoughts on concerts\n\nGiven a `NamedTuple` defined in [`~/.config/ttally.py`](https://purarue.xyz/d/ttally.py?redirect), this creates interactive interfaces which validates my input and saves it to a file\n\nThe `{tuple}-now` aliases set the any `datetime` values for the prompted tuple to now\n\nThis also gives me `{tuple}-recent` aliases, which print recent items I've logged. For example:\n\n```\n$ water-recent 5\n2021-03-20 18:23:24     2.0\n2021-03-20 01:28:27     1.0\n2021-03-19 23:34:12     1.0\n2021-03-19 22:49:05     1.5\n2021-03-19 16:05:34     1.0\n```\n\nThe `-recent` aliases can accept `all` to print all items, or a duration like `1d` or `6h` to print data from the last few hours/days.\n\n## Why/How\n\n### Goals\n\n- validates my user input to basic types\n- stores it as a user-editable format (YAML)\n- can be loaded into python as typed objects\n- minimal boilerplate to add a new model\n- can be synced across multiple machines without conflicts\n- allow completely custom types or prompts - see [autotui docs](https://github.com/purarue/autotui#custom-types), [my custom prompts](https://purarue.xyz/d/ttally_types.py?redirect)\n\nThis intentionally uses YAML and doesn't store the info into a single \"merged\" database. That way:\n\n- you can just open the YAML file and quickly change/edit some item, no need to re-invent a CRUD interface (though `ttally edit-recent` does exist)\n- files can be synced across machines and to my phone using [syncthing](https://syncthing.net/) without file conflicts\n- prevents issues with trying to merge multiple databases from different machines together ([I've tried](https://github.com/purarue/calories-scripts/blob/master/calmerge))\n\nThe YAML files are versioned with the date/OS/platform, so I'm able to add items on my linux, mac, or android (using [`termux`](https://termux.com/)) and sync them across all my devices using [`SyncThing`](https://syncthing.net/). Each device creates its own file it adds items to, like:\n\n```\nfood-darwin-mbp.localdomain-2021-03.yaml\nfood-linux-bastion-2021-03.yaml\nfood-linux-localhost-2021-04.yaml\n```\n\n... which can then be combined back into python, like:\n\n```python\n\u003e\u003e\u003e from more_itertools import take  # just to grab a few items\n\u003e\u003e\u003e from ttally.__main__ import ext\n\u003e\u003e\u003e from ttally.config import Food\n\u003e\u003e\u003e take(3, ext.glob_namedtuple(Food))\n\n[Food(when=datetime.datetime(2020, 9, 27, 6, 49, 34, tzinfo=datetime.timezone.utc), calories=440, food='ramen, egg'),\nFood(when=datetime.datetime(2020, 9, 27, 6, 52, 16, tzinfo=datetime.timezone.utc), calories=160, food='2 eggs'),\nFood(when=datetime.datetime(2020, 9, 27, 6, 53, 44, tzinfo=datetime.timezone.utc), calories=50, food='ginger chai')]\n```\n\n... or into JSON using `ttally export food`\n\nThe `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](bin/cz)\n\n`hpi query` from [`HPI`](https://github.com/purarue/HPI) can be used with the `ttally.__main__` module, like:\n\n```bash\n# how many calories in the last day\n$ hpi query ttally.__main__.food --recent 1d -s | jq -r '(.quantity)*(.calories)' | datamash sum 1\n2252\n```\n\nIf you'd prefer to use JSON files, you can set the `TTALLY_EXT=json` environment variable.\n\nThis 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 the `merge` command:\n\n```\nttally merge food\n```\n\n## Installation\n\n```bash\npip install ttally  # can use 'pip install ttally[optional]' for extra features\n```\n\n```\nUsage: ttally [OPTIONS] COMMAND [ARGS]...\n\n  Tally things that I do often!\n\n  Given a few namedtuples, this creates serializers/deserializers and an\n  interactive interface using 'autotui', and aliases to:\n\n  prompt using default autotui behavior, writing to the ttally datafile, same\n  as above, but if the model has a datetime, set it to now, query the 10 most\n  recent items for a model\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  datafile      print the datafile location\n  drop-last     drop the last n items\n  edit          edit the datafile\n  edit-recent   fuzzy select/edit recent items\n  export        export all data from a model\n  from-json     add item by piping JSON\n  generate      generate shell aliases\n  merge         merge all data for a model into one file\n  models        list models\n  prompt        tally an item\n  prompt-now    tally an item (now)\n  recent        print recently tallied items\n  update-cache  cache export data\n```\n\n### Configuration\n\nYou need to setup a `~/.config/ttally.py` file. You can use the block above as a starting point, or with mine:\n\n```bash\ncurl -s 'https://purarue.xyz/d/ttally.py' \u003e ~/.config/ttally.py\n```\n\nTo setup aliases; You can do it each time you launch you terminal like:\n\n```bash\neval \"$(python3 -m ttally generate)\"\n```\n\nOr, 'cache' the generated aliases by putting a block like this in your shell config:\n\n```bash\nTTALLY_ALIASES=\"${HOME}/.cache/ttally_aliases\"\nif [[ ! -e \"${TTALLY_ALIASES}\" ]]; then  # alias file doesn't exist\n\tpython3 -m ttally generate \u003e\"${TTALLY_ALIASES}\"  # generate and save the aliases\nfi\nsource \"${TTALLY_ALIASES}\"  # make aliases available in your shell\n```\n\ni.e., it runs the first time I open a terminal, but then stays the same until I remove the file\n\nYou 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.\n\nFor shell completion to autocomplete options/model names:\n\n```\neval \"$(_TTALLY_COMPLETE=bash_source ttally)\"  # in ~/.bashrc\neval \"$(_TTALLY_COMPLETE=zsh_source ttally)\"  # in ~/.zshrc\neval \"$(_TTALLY_COMPLETE=fish_source ttally)\"  # in ~/.config/fish/config.fish\n```\n\n### Caching\n\n`ttally update-cache` can be used to speedup the `export` and `recent` commands:\n\n```\nUsage: ttally update-cache [OPTIONS]\n\n  Caches data for 'export' and 'recent' by saving the current data and an\n  index to ~/.cache/ttally\n\n  exit code 0 if cache was updated, 2 if it was already up to date\n\nOptions:\n  --print-hashes  print current filehash debug info\n  --help          Show this message and exit.\n```\n\nI run this using [entr](https://github.com/eradman/entr) whenever the data files change. In the background, like:\n\n```\nfind ~/data/ttally -type f | entr -n ttally update-cache\n```\n\nDefault cache directory can be overwritten with the `TTALLY_CACHE_DIR` environment variable\n\n### Subclassing/Extension\n\nThe entire `ttally` library/CLI can also be subclassed/extended for custom usage, by using `ttally.core.Extension` class and `wrap_cli` to add additional [click](https://click.palletsprojects.com/en/8.1.x) commands. For an example, see [flipflop.py](https://purarue.xyz/d/flipflop.py?redirect)\n\n### Shell Scripts\n\n[`cz`](bin/cz) lets me fuzzy select something I've eaten in the past using [`fzf`](https://github.com/junegunn/fzf), like:\n\n![](https://raw.githubusercontent.com/purarue/calories-fzf/master/demo.gif)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurarue%2Fttally","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpurarue%2Fttally","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpurarue%2Fttally/lists"}