Package: autotui
autotuiThis uses type hints to convertNamedTuple's (short struct-like classes) to JSON/YAML, and back to python objects.It also wrapsprompt_toolkitto prompt the user and validate the input for common types, and is extendible to whatever types you want.This has built-ins to prompt, validate and serialize:intfloatboolstrdatetimeEnumDecimalOptional[<type>](or<type> | None)List[<type>](orlist[<type>])Set[<type>](orset[<type>])otherNamedTuples (recursively)I wrote this so that I don't have to repeatedly write boilerplate-y python code to validate/serialize/deserialize data. As a more extensive example of its usage, you can see myttallyrepo, which I use to track things like calories/water etc...InstallThis requirespython3.8+, specifically for moderntypingsupport.To install with pip, run:pip install autotuiAs an example, if I want to log whenever I drink water to a file:fromdatetimeimportdatetimefromtypingimportNamedTuplefromautotui.shortcutsimportload_prompt_and_writebackclassWater(NamedTuple):at:datetimeglass_count:floatif__name__=="__main__":load_prompt_and_writeback(Water,"~/.local/share/water.json")Which, after running a few times, would create:~/.local/share/water.json[{"at":1598856786,"glass_count":2.0},{"at":1598856800,"glass_count":1.0}](datetimes are serialized into epoch time)If I want to load the values back into python, its just:fromautotui.shortcutsimportload_fromclassWater(NamedTuple):#... (same as above)if__name__=="__main__":print(load_from(Water,"~/.local/share/water.json"))#[Water(at=datetime.datetime(2020, 8, 31, 6, 53, 6, tzinfo=datetime.timezone.utc), glass_count=2.0),# Water(at=datetime.datetime(2020, 8, 31, 6, 53, 20, tzinfo=datetime.timezone.utc), glass_count=1.0)]A lot of my usage of this only ever uses 3 functions in theautotui.shortcutsmodule;dump_toto dump a sequence of myNamedTuples to a file,load_fromto do the opposite, andload_prompt_and_writeback, to load values in, prompt me, and write back to the file.Enabling OptionsSome options/features can be enabled using global environment variables, or by using a contextmanager to temporarily enable certain prompts/features.As an example, there are two versions of thedatetimepromptThe one you see above using a dialogA live version which displays the parsed datetime while typing. Since that can cause some lag, it can be enabled by setting theLIVE_DATETIMEoption.You can enable that by:setting theAUTOTUI_LIVE_DATETIME(prefix the name of the option withAUTOTUI_) environment variable, e.g., addexport AUTOTUI_LIVE_DATETIME=1to your.bashrc/.zshrcusing theoptionscontextmanager:importautotuiwithautotui.options("LIVE_DATETIME"):autotui.prompt_namedtuple(...)Options:LIVE_DATETIME: Enables the live datetime promptCONVERT_UNKNOWN_ENUM_TO_NONE: If an enum value is not found on the enumeration (e.g. you remove some enum value), convert it toNoneinstead of raising aValueErrorPartial promptsIf you want to prompt for only a few fields, you can supply theattr_use_valuesortype_use_valuesto supply default values:# water-now script -- set any datetime values to nowfromdatetimeimportdatetimefromtypingimportNamedTuplefromautotuiimportprompt_namedtuplefromautotui.shortcutsimportload_prompt_and_writebackclassWater(NamedTuple):at:datetimeglass_count:floatload_prompt_and_writeback(Water,"./water.json",type_use_values={datetime:datetime.now()})# or specify it with a function (don't call datetime.now, just pass the function)# so its called when its neededval=prompt_namedtuple(Water,attr_use_values={"at":datetime.now})Since you can specify a function to either of those arguments -- you're free towrite a completely custom prompt functionto prompt/grab data for that field however you wantFor example, to prompt for strings by openingviminstead:fromdatetimeimportdatetimefromtypingimportNamedTuple,List,Optionalfromautotui.shortcutsimportload_prompt_and_writebackimportclickdefedit_in_vim()->str:m=click.edit(text=None,editor="vim")returnmifmisNoneelsem.strip()classJournalEntry(NamedTuple):creation_date:datetimetags:Optional[List[str]]# one or more tags to tag this journal entry withcontent:strif__name__=="__main__":load_prompt_and_writeback(JournalEntry,"~/Documents/journal.json",attr_use_values={"content":edit_in_vim},)Can also define those as astaticmethodon the class, so you don't have to pass around the extra state:classJournalEntry(NamedTuple):...@staticmethoddefattr_use_values()->Dict:return{"content":edit_in_vim}# pulls attr_use_values from the functionprompt_namedtuple(JournalEntry,"~/Documents/journal.json")YamlSince YAML is a superset of JSON, this can also be used with YAML files.autotui.shortcutswill automatically decode/write to YAML files based on the file extension.# using the water example aboveif__name__=="__main__":load_prompt_and_writeback(Water,"~/.local/share/water.yaml")Results in:-at:1645840523glass_count:1.0-at:1645839340glass_count:1.0You can also passformat="yaml"to thenamedtuple_sequence_dumps/namedtuple_sequence_loadsfunctions (shown below)PickingThis has a basicfzfpicker usingpyfzf-iter, which lets you pick one item from a list/iterator:fromautotuiimportpick_namedtuplefromautotui.shortcutsimportload_frompicked=pick_namedtuple(load_from(Water,"~/.local/share/water.json"))print(picked)To install the required dependencies, installfzfandpip install 'autotui[pick]'EditingThis also provides a basic editor, which lets you edit a single field of aNamedTuple.$ python3 ./examples/edit.py
Water(at=datetime.datetime(2023, 3, 5, 18, 55, 59, 519320), glass_count=1)
Which field to edit:

	1. at
	2. glass_count

'glass_count' (float) > 30
Water(at=datetime.datetime(2023, 3, 5, 18, 55, 59, 519320), glass_count=30.0)In python:fromautotui.editimportedit_namedtuplewater=edit_namedtuple(water,print_namedtuple=True)# can also 'loop', to edit multiple fieldswater=edit_namedtuple(water,print_namedtuple=True,loop=True)Any additional arguments toedit_namedtupleare passed toprompt_namedtuple, so you can specifytype_validatorstoattr_validatorsto prompt in some custom wayTo install,pip install 'autotui[edit]'orpip install clickCustom TypesIf you want to support custom types, or specify a special way to serialize another NamedTuple recursively, you can specifytype_validators, andtype_[de]serializerto handle the validation, serialization, deserialization for that type/attribute name.As a more complicated example, heres a validator fortimedelta(duration of time), being entered as MM:SS, and the corresponding serializers.# see examples/timedelta_serializer.py for imports# handle validating the user input interactively# can throw a ValueErrordef_timedelta(user_input:str)->timedelta:iflen(user_input.strip())==0:raiseValueError("Not enough input!")minutes,_,seconds=user_input.partition(":")# could throw ValueErrorreturntimedelta(minutes=float(minutes),seconds=float(seconds))# serializer for timedelta, converts to JSON-compatible integerdefto_seconds(t:timedelta)->int:returnint(t.total_seconds())# deserializer from integer to timedeltadeffrom_seconds(seconds:int)->timedelta:returntimedelta(seconds=seconds)# The data we want to persist to the fileclassAction(NamedTuple):name:strduration:timedelta# AutoHandler describes what function to use to validate# user input, and which errors to wrap while validatingtimedelta_handler=AutoHandler(func=_timedelta,# accepts the string the user is typing as inputcatch_errors=[ValueError],)# Note: validators are of type# Dict[Type, AutoHandler]# serializer/deserializers are# Dict[Type, Callable]# the Callable accepts one argument,# which is either the python value being serialized# or the JSON value being deserialized# use the validator to prompt the user for the NamedTuple data# name: str automatically uses a generic string prompt# duration: timedelta gets handled by the type_validatora=prompt_namedtuple(Action,type_validators={timedelta:timedelta_handler,},)# Note: this specifies timedelta as the type,# not int. It uses what the NamedTuple# specifies as the type for that field, not# the type of the value thats loaded from JSON# dump to JSONa_str:str=namedtuple_sequence_dumps([a],type_serializers={timedelta:to_seconds,},indent=None,)# load from JSONa_load=namedtuple_sequence_loads(a_str,to=Action,type_deserializers={timedelta:from_seconds,},)[0]# can also specify with attributes instead of typesa_load2=namedtuple_sequence_loads(a_str,to=Action,attr_deserializers={"duration":from_seconds,},)[0]print(a)print(a_str)print(a_load)print(a_load2)Output:$ python3 ./examples/timedelta_serializer.py
'name' (str) > on the bus
'duration' (_timedelta) > 30:00
Action(name='on the bus', duration=datetime.timedelta(seconds=1800))
[{"name": "on the bus", "duration": 1800}]
Action(name='on the bus', duration=datetime.timedelta(seconds=1800))
Action(name='on the bus', duration=datetime.timedelta(seconds=1800))The general philosophy I've taken for serialization and deserialization is send a warning if the types aren't what the NamedTuple expects, but load the values anyways. If serialization can't serialize something, it warns, and ifjson.dumpdoesn't have a way to handle it, it throws an error. When deserializing, all values are loaded from their JSON primitives, and then converted into their corresponding python equivalents; If the value doesn't exist, it warns and sets it to None, if theres a deserializer supplied, it uses that. This is meant to help facilitate quick TUIs, I don't want to have to fight with it.(If you know what you're doing and want to ignore those warnings, you can set theAUTOTUI_DISABLE_WARNINGS=1environment variable)There are lots of examples on how this is handled/edge-cases in thetests.You can also take a look at theexamplesTestsgitclonehttps://github.com/seanbreckenridge/autotuicd./autotui
pipinstall'.[testing]'mypy./autotui
pytest