Package: goodenough
goodenoughA small Python lib that helps one to pick items from their collections / storages.pipinstallgoodenoughorpipinstallgoodenough[web]# if you want a simple aiohttp-based web serverIt's designed for tasks where you need a random item that is also not complete garbage like selecting the next song from a songs database to play on the radio.To use this lib you should tell it how to get the sample of items (get_items) and how to sort them from good to bad (rules).Optionaly, you can also reflect on how the items got sorted and which item has won (review_items,review_result).CallbacksAll callbacks should be async in case you want to do I/O stuff (like talking to a storage).Every callback's first argument should berequest-- an opaque data passed to.pick()or.async_pick()(in case of a web server started by.serve(), therequestis JSON-deserialized POST body, see below).get_itemsHere we defineget_itemscallback that selects random documents from Mongo (but it can be any data source of your choice):frommotor.motor_asyncioimportAsyncIOMotorClientfromgoodenoughimportGoodEnoughasyncdefget_items(request):client=AsyncIOMotorClient()collection=client.test.things# assumed test.things is prepopulatedpipeline=[{"$sample":{"size":request["size"]}}]cursor=collection.aggregate(pipeline)returnawaitcursor.to_list(None)# we return list, but any iterable should dog=GoodEnough(get_items)print(g.pick(request={"size":1}))# should print a random document from test.thingsInrequestwe pass"size"equals to 1 to select one random document.We could pass a greater size value, but.pick()and.async_pick()only return one result, so in this example it wouldn't be useful.However, if we define one or morerulesfunctions, it will make sense to select a bigger sample and then choose the "best" item.rulesEach rule function receivesrequestand anitemand is expected to return afloatbetween0.and1., where0.means the item is "the worst" and1.means the item is "the best".Here we extend our previous example and write a rule that gives greater chance to documents whose"foo"field is closer torequest["foo"].#...asyncdefrule_foo(request,item):ifitem["foo"]==request["foo"]:return1.else:return1/abs(item["foo"]-request["foo"])g=GoodEnough(get_items,rules=[rule_foo],)print(g.pick(request={"size":5,"foo":15}))You can supply many rules, in which case the resulting score of an item will be a product of scores returned from therulesfunctions.goodenoughalso supports rulesweights. See tests.If a rule functions returns0., the item is then never returned by.pick().If each item in a sample gains score0., theNoneis returned, or if you supplydefault=parameter to.pick(), that is returned.review_itemsThereview_itemscallback (if is defined) is invoked after all rules have applied and the items have got sorted from the "best" to the "worst" in terms of their scores.One use case of this is to communicate information back to the database, like for example saving last resulting scores.Its second argumentscored_itemsis a list of named pairs[... (item=item, score=score) ...]sorted from the heighest score to the lowest, implying that the picked item is going to be the first in the list.Theis_successfulflag tells the callback whether the first item is going to be returned (is_successful=True) or the default value will be returned (is_successful=False).Here we extend our example to increment a"pickedCount"value in Mongo for the picked item:#...asyncdefreview_items(request,scored_items,is_successful):ifis_successful:picked=scored_items[0]# `picked` is a named pair (item=..., score=...)client=AsyncIOMotorClient()collection=client.test.thingsawaitcollection.update_one({"_id":picked.item["_id"]},{"$inc":{"pickedCount":1}},)g=GoodEnough(get_items,review_items=review_items,rules=[rule_foo],)print(g.pick(request={"size":5,"foo":15}))review_resultAnother optional callback isreview_resultthat receives the picked item which it can modify before the item will get returned from.*pick().This callbackmustreturn and the returned valuemustbe aGoodEnoughResultobject, see below.Even if you're not going to modify the result in this callback, you have to explicitly writereturn GoodEnoughResult(result).Here we extend the previous example by writing a callback that will insert current datetime to the item before returning it from.pick().importdatetimefromgoodenoughimportGoodEnough,GoodEnoughResult# ...asyncdefreview_result(request,result,is_successful):ifis_successful:result["dtPicked"]=datetime.datetime.now(datetime.timezone.utc)returnGoodEnoughResult(result)g=GoodEnough(get_items,review_items=review_items,review_result=review_result,rules=[rule_foo],)print(g.pick(request={"size":5,"foo":15}))Web serverThis lib has a small web server that returns items in response toPOST /fetch.It requiresaiohttp, so eitherpip install aiohttporpip install goodenough[web].To start the web server call.serve()or.serve(port=YOUR_PORT).The default port is4181for no comprehensible reason.The POST body will be JSON-deserialized and passed to the.async_pick()as arequestparameter.The result of.async_pick()will be JSON-serialized and sent as the response body.Let's again update our previous example:(To avoid JSON errors trying to serialize Mongo's ObjectId and datetime, we will convert them tostrs in a new version of thereview_resultcallback.)#...asyncdefreview_result(request,result,is_successful):ifis_successful:result["dtPicked"]=str(datetime.datetime.now(datetime.timezone.utc))result["_id"]=str(result["_id"])returnGoodEnoughResult(result)#...g.serve(port=9000)And here we call it:$curl-s-XPOSThttp://localhost:9000/fetch-d'{"size": 5, "foo": 15}'|jq.{"_id":"5fcb503f107390ce97e7d04f","foo":16,"pickedCount":9,"dtPicked":"2020-12-05 14:48:46.471336+00:00"}