Package: micro-components
Micro Components for PythonLightweight library to create components that can automagically be used as either CLI or as native classes in other programming languagesThe same package is availablefor Node.Check it outif you want Python and Node components talking to each other!Goals & DesignOften times, which programming language I pick is predetermined by technical requirements and the resources available in that language. In many cases I have to use a mixture of nodejs, Python, and bash scripts, and they all have to talk to each other.The intent of this library is to provide a utility class turningSingleton classesinto command line interfaces and providing an intuitive class interface between different programming languages. Since a lot of the code I produce is either Node or Python, I started by creating components for those two languages.The beauty of this is that components have a simple JSON interface to talk to each other, irregardless of the programming language. Any other language implementing the Component interface can now be imported as if it were a native class.InstallationIn your terminal run:$pipinstallmicro-componentsUsageTo run a component from the terminal, you need the following structure at minimum:frommicro_componentsimportComponentclassTaskRunner(Component):name='task_runner'defrun(task_id):return{'task_id':task_id,'status':'running'}TaskRunner.export_as_cli();To then be able to run one of the following commands from your terminal:$taskrunner.pyrun95# output: { "task_id": 95, "status": "running"  }$taskrunner.pyrun--task-id="hello world"# output: { "task_id": "hello world", "status": "running"  }$taskrunner.pyrun--task-id="[5,9,true]"# output: { "task_id": [5, 9, true], "status": "running"  }Example of Python and Node components interacting:Consider this JavaScript component calledtask_logger.js:constfs=require('fs');const{Component}=require('micro-components-js');constTaskLogger=Component({name:'task_logger',log(infos){fs.writeFileSync('log.json',infos);}});TaskLogger.export_to_cli();We can now use TaskLogger from within ourtask_runner.pycode:frommicro_componentsimportComponentTaskLogger=Component.from_cli('./task_logger.js')classTaskRunner(Component):name='task_runner'run(task_id):result={'task_id':task_id,'status':'running'}TaskLogger.log(result)returnresultTaskRunner.export_as_cli()Creation from TerminalYou can use themicro-componentsCLI to create a component like so:In your terminal:$micro-componentscreate"My Component"This will create a skeleton component file in your current directory.Naming ConventionComponents have a snake_cased filename and hold the same name as a property at minimum:frommicro-componentsimportComponentclassSomeComponent(Component):name='some_component'Class names follow usual conventions of StartCase.fromcomponents.intent_matcherimportIntentMatcherExportingYou can use one of two export methods to either import components as modules "inline" or call their methods from the command line.As CLITo turn a component into a CLI, use the commandexport_as_cli():SomeComponent.export_as_cli()Manual CreationIf you create your component by hand, make sure your file is executable by adding a Shebang at the top of it…#!/usr/bin/env python3…and by giving it execution permissions (in yourterminal)…$chmod+x./components/some_component.py…to then run the methods:$./components/some_component.pyfetch_data"parameter"15"{ \"sub-param\": \"value\" }"The Component class will automagically look at the parameter defaults of your component's methods and try to parse parameters passed through the CLI accordingly.Consider the following example component:classRecipeFetcher(Component):name='recipe_fetcher'counts=0defget_ingredients(ingredient_name,max_count=10,normalize=True,options={}):...RecipeFetcher.export_as_cli();We pass the following arguments via CLI:$./components/recipe_fetcher.pyget_ingredients"Onion Soup"15false"{ \"pepper\": false }"The component class will automatically parse 15, false, and the passed JSON string as JSON and use the true datatypes.Named ArgumentsYou can pass named arguments by prefixing the parameter names with two hyphens instead:$/components/recipe_fetcher.pyget_ingredients--ingredient_name"Mangosteen"--normalize=1Properties for the component class can be passed the same way. Arguments that don't match method names will be applied as properties. Any hyphens will be replaced by underscores, meaning--user-idwill be read asuser_id.$/components/recipe_fetcher.pyget_ingredients--user-id=1337--ingredient_name="Capers"Method ChainingMethods can be chained using the chain notation (beware that spacing is crucial):$/components/recipe_fetcher.py[load_ingredients"all",get_ingredients"Pepper"12]will first runRecipeFetcher.load_ingredients("all"), thenRecipeFetcher.get_ingredients("Pepper", 12)and return a dictionary of results for each execution.CachingBefore using any of the built-in caching functionality, make sure you have a directory calleddata/cachesto store those in.TODO: add documentation for caching decoratorsA JSON cache will be auto-generated in/data/caches/<component_name>.cache.json.APIComponents provide the following interface:EventsComponents and utility classes all emit events that can be listened to. You can attach event listeners using theonmethod.RecipeFetcher.on('spawn',lambdaself:print('RecipeFetcher loaded.')orself)The following events are available on every component:Event NameTriggered …passed argumentsspawnonce on initializationselfinstance, needs to be returned backinitevery time the component is being updated usingComponentName.init(...)selfinstance,optionsobjectcacheevery time a method's cache is being hitkeyused to retrieve value from cachecall_from_clionce upon calling the component from the command linecommandis the called method,argsthe passed arguments,verboseto turn console output on/offTotrigger(e.g. your own) events, use thetriggermethod:RecipeFetcher.trigger('recipe_outdated',['some','arguments'])If the event handler returned something, it will be passed through trigger as return value.new_recipe=RecipeFetcher.trigger('recipe_outdated',[old_recipe])PropertiesYou can access non-callable properties from the CLI using the component module's built-in get and set methods in your terminal:$./components/recipe_fetcher.pygetcounts# returns 1$./components/recipe_fetcher.pysetcounts3ShortcutsAll components automatically get ahelpmethod, so if uncertain about properties run./components/recipe_fetcher.py help.
All methods automatically get a--helpdirective, so if uncertain about parameters run./components/recipe_fetcher.py get_ingredients --help.IntegrationYou can call components written in Javascript from Python and vice-versa as if they were written in the same language using theComponentmodule.Our previousRecipeFetcherexample was written in Javascript, but now we want to use it in Python. Here's how:frommicro_componentsimportComponentRecipeFetcher=Component.from_cli('./components/recipe_fetcher.js');ingredients=RecipeFetcher.get_ingredients("Onion Soup",15,False,{'pepper':False})To access properties, use the built-in property getters and setters (see "Properties"), like so:frommicro_componentsimportComponentRecipeFetcher=Component.from_cli('./components/recipe_fetcher.py')counts=RecipeFetcher.get("counts")TestingThis package comes with a set of standard unittest cases located in[tests/test_micro-components.py][]Run them using:$pytestor$python3./tests/test_micro-components.pyExamplesCheckout thecomponents folderin the repo for some examples. The CLI used to create component templates for example is a component itself.LicenseMIT License