Package: clima
Create a command line interface with minimal setup.clima - command line interface with a schemaTable of contentsBrieflyFeaturesCli definitionConfiguration object in a spiffyInstallingUsageExamples and platformsTesting the examplesVersion printingAutocompletion..in IDEs (wip)..in bashPost init hookCli.post_init()Schema.post_init()Configuration optionsConfiguration file and environment variablesType casting with configuration definitionConfiguration file in the home directory.env filePassword unwrapping/decryption with passAdditional features via FireTruncated error printingWays to run the script for the uninitiatedLinking executable script to ~/.local/binPackaging a module (pip ready)Building/Installing from sourceLong description and backgroundWhy another cli framework?DependenciesBrieflyFeaturesClima handles loading and parsing command
line arguments with some off-the-shelf features, including:a global configuration objecta quick definition of defaultsdefining defaults doubles as a description for help on the command linetype handling with annotationsdefinitions with configuration filesenv variablesloading .env filessecrets stored withpasspost_init hookCli definitionCreating a command-line interface for your program:Import all necessary parts from the package clima(optional) Define configuration i.e., SchemaDefine the command line commands i.e., CLI-class:Example: to set up a configuration and a command-line interface ready to go.from clima import c

@c
class Cli:
    def say_hi(self):
        print('oh hi - whatever this is..')The command line usage form could be as simple as:my_tool say_hiConfiguration object in a spiffyfrom clima import c

# Defining the settings (configuration object)
class S(Schema):
    place = 'world'
    
@c
class Cli:
    def say_hi(self):
        # using configuration object 'c'
        print(f'oh hi - {c.place}')Again, the command line usage form could be as simple as:my_tool say_hi
 my_tool say_hi --place 'other world'See theexamplesfolder and other sections for more examples. For example, the folder includessomething that resembles
the example above.Installingpip install --user climatocUsageSee the example file inexamples/script_example.py. Here's a rundown of the individual
parts in such a script (adapted from another example atmodule example).First, import the required components:from clima import c, SchemaIn your code, define theSchemasubclass:class Configuration(Schema):
    a: str = 'A'  # a description
    x: int = 1  # x descriptionHere "Configuration" is an arbitrary name, no magic there. The inheritedSchemaclass
defines the attributes (i.e.aandxin this example).Note the specific formatting of theSchemasubclass:# attribute[: type] = default value  [# Description for the --help]
    a: str = 'A'  # a descriptionais the attribute that can be called in the code later withc.a. In this example, it has a type of 'str' and a default
value of 'A'. The values in square brackets[]are
optional.Clima parses the comment after the definition for the command-line help printout. In other words, Clima parses all of these parts to be displayed when the program is called using the argument '--help'. For example like this:./script.py foo -hWill now produce:Usage:       script.py foo [ARGS]
 
 Description: Args:
     --a (str): a description (Default is 'A')
     --x (int): x description (Default is 1)The example shown in this readme is atexamples/readme_example.py.Clima parses the methods as subcommands with their respective doc-strings, when the class is wrapped with the decorator@c, and will show these in the subcommand's help printout.The subcommands should be defined somewhat as follows:@c
class Cli:
    def subcommand_foo(self):
        """This will be shown in --help for subcommand-foo"""
        print('foo')
        print(c.a)
        print(c.x)

    def subcommand_bar(self):
        """This will be shown in --help for subcommand-bar"""
        print('bar')Note the double usage of thec- first as a decorator and later as the parsed configuration
inside the method:...
    ...
    print(c.a)
    print(c.x)As a decorator,@cdefines the class to be parsed as the subcommands. As an objectcit is used to access all the arguments.tocExamples and platformsTried and used on Linux, macOS, and windows. However, packaging and dependency management in python is sometimes hairy and
your mileage may vary.More examples in theexamples directorywith printouts of the defined subcommands and helps.Testing the examplesTheexamplescan be tried out by cloning the repo and running from repo directory root (on linux and the like):git clone https://github.com/d3rp/clima.git 
cd clima
PYTHONPATH=$PWD python ./examples/readme_example.py foo -hRunning the examples that wrap a module:PYTHONPATH=$PWD python ./examples/module_example/__main__.py -h
PYTHONPATH=$PWD python ./examples/module_example/__main__.py subcommand-foo -h
PYTHONPATH=$PWD python ./examples/module_example/__main__.py subcommand-bar
...The output should resemble this (fire v0.1.3 prints out Args, fire v0.2.1 does not (though it looks much nicer))$ tester subcommand-foo -- -h

Type:        method
String form: <bound method Cli.subcommand_foo of <__main__.Cli object at 0x000002995AD74BE0>>
File:        C:\Users\foobar\code\py\clima\tester\__main__.py
Line:        18
Docstring:   This will be shown in --help for subcommand-foo
Args:
    --a (str): a description (Default is 'A')
    --x (int): x description (Default is 1)

Usage:       __main__.py subcommand-foo [--X ...]All of the example scripts can be run by installingpoetryand running therun_examples.bashscript:pip install --user poetry
./run_examples.bashtocVersion printingVersion printing works via theversionsubcommand and provides a version check functionality for (poetry) packaged scripts. Thus with bumping the version with poetry, Clima will handle parsing the current version of the packaged tool thus providing a subcommand such as:my_tool versionClima parses theversionattribute into thecobject, so if you want control over it, you can overwrite it with thepost_initor by handling thec.versionotherwise.Autocompletion..in IDEs (wip)Also, to enable autocompletion in IDEs, this hack suffices:c: Configuration = cPut it in the "global namespace" e.g. just after defining the template. See theexamples/script_example.pyfor a specific example.When all is complete, the importedcvariable should have all the bits and pieces for the configuration. It can be
used inside the Cli class as well as imported around the codebase thus encapsulating all the configurations into one
container with quick access to the attributes (c.a,c.x, ...)...in bashRunning the script with-- --completionarguments should print an autocompletion declaration to include in a bash completions file:my_tool -- --completionTBD: zsh etc. completionsPost init hookThere are two ways to define apost_inithook depending on theSchemasubclass or theClidefinition.Cli.post_init()On some occasions, it is helpful to deduce specific defaults from the given arguments,
for example, in a cross-platform build allowing
only minimal CLI arguments. For those cases, there is apost_inithook
When defining thepost_init()in theCliclass, for example:@c
class Cli:

    @staticmethod
    def post_init(s):
        if s.platform = 'win':
            self.bin_path = 'c:/Users/foo/bar'
        else:
            s.bin_path = '/Users/mac/sth'
        
    def subcommand(self):
        passThe method will have access to the cli args, but can not introduce new variables to the schema.This is arguably the more useful of the two variations of post_inits.Note: The signature of thepost_init()differs in these two options. For the time being, it is a@staticmethodSchema.post_init()This alternative provides post_init-like features that are overrideable with the CLI arguments.class SoAdvanced(Schema):

    platform: str = 'win'  # a description
    bin_path: pathlib.Path = ''  # x description
    
    def post_init(self, *args):
        if self.platform = 'win':
            self.win_specific_field = 'All your files are locked by us..'Note: This post_init() does not have access to the CLI arguments, but theSchema's post_init can introduce new
attributes/properties/fields/arguments to the configuration, which theCli-class post-init can't.
TheSchema'spost_init()hook is run after the schema object's initialisation, but BEFORE the command-line object's initialisation.tocConfiguration optionsIt is tedious to write a long list of parameters on the command line when most use cases follow a similar pattern. There are several options to choose from to facilitate the use of configurations.Thecdecorator/configuration chains multiple configuration options together in order of priority (lower numbers refer to higher priority):command line argumentsEnvironment variables.env fileconfiguration file definitionsdecrypted passwords from~/.password-storeif gnugpg is installeddefaults in the subclass inheritingSchemaConfiguration file and environment variablesThe configuration file should be named with either the postfix.cfgor.conf, for examplefoo.conf, and have an ini type formatting including
an explicit 'Clima' section:# foo.conf
[Clima]
x = 2The keys are the same as in the schema class' declaration. Default values can be defined for all, some or none of the attributes. The same applies to the env variables.# linux example
X=2 tester subcommand-fooA configuration file defined this way can be located in the current working directory or - if yourSchemadefines acwdfield - there. Clima
will try to use the first configuration file it finds, so that might produce some caveats.class Conf(Schema):
    cwd = ''

# Running ./script.py --cwd <folder> would automatically load the first *.conf file in <folder>Type casting with configuration definitionTheSchemadefinition can have type annotations, which are used to cast the given arguments. For exampleclass C(Schema):
    p: Path = ''  # Path to somethingResults inc.p's type cast asPath.Configuration file in the home directoryYou can also define the config file in the configuration class (one inheritingSchema) by defining the
magic fieldCFG.For example, lets say the commandmy_tool(packaged etc) has a user configuration file at~/.my_tool.conf. This
can now be handled with just addingCFG = Path.home() / '.my_tool.confto the Schema:from pathlib import Path

class S(Schema):
    bing = 'bang'
    CFG = Path.home() / '.my_tool.conf'Then, for example, the configuration file would be written as:#~/.my_tool.conf
[Clima]
bing = diudiuRunning the commandmy_toolwould produce the value in the configuration file, though the arguments can still be overriden.my_tool run 
# diudiu

my_tool run --bing bam
# bam.env fileThis is handled bydotenv. In short, all the defaults defined in theSchemasubclass can be overridden either by:<field> = <value>orexport <field> = <value>Password unwrapping/decryption with passIf gnugpg(2) is installed, clima can leverage it to decrypt secrets on-the-fly.Note: Currently this works most conveniently with gpg-keys without a password. Gpg handles the password prompts which might
fail on some platforms or configurations.Note 2: Leading and trailing whitespace (including\nlinefeeds) are stripped, when decrypted.pass(not required) can be used to store passwords as gpg encrypted files under the home directory. Clima
uses the default path of ~/.password-store and the files found within. It will then match the arguments with the
stored passwords, for example:tree -A ~/.password-store                                                                                                                                                                                                                                                                             ✔ | 41s | anaconda3 
 /Users/me/.password-store
 ├── work
 │   ├── ci
 │   │   ├── sign_id.gpg
 │   │   ├── sign_pw.gpg
 ... ... ...And an accordingSchemadefinition:class Conf(Schema):
     sign_id: str = ''  # signing id for the CI
     sign_pw: str = ''  # signing pw for the CIWould accept those arguments as cli arguments, or if omitted, would traverse through the.password-storeand decrypt the
foundsign_id.gpgandsign_pw.gpgplacing the values found in the configuration objectc.tocAdditional features via FireSee thePython Fire's Flagsdocumentation for nice additional features such as:# e.g. tester.py is our cli program
tester.py subcommand-foo -- --trace
tester.py -- --interactive
tester.py -- --completionTruncated error printingThis feature rose as an opinionated option, and I admit, it should be something the user could bypass. Even though I have used python for a few years professionally, I am still not satisfied with its error printing. When raising exceptions, Clima truncates the error lists and tries to provide a more readable version of the "first" point of failure. The whole traceback is written into a logfileexception_traceback.logto examine if the truncated output provides insufficient information.Note: When running the examples, theexception_traceback.logfile will be written inside theexamplesdirectoryError (full trace in exception.log):

traceback_example.py:7   ::  lumberjack()            :  self.bright_side_of_death()  =>
traceback_example.py:12  ::  bright_side_of_death()  :  return tuple()[0]            =>  IndexError

IndexError: tuple index out of rangeWays to run the script for the uninitiatedHere are some suggestions on how to wrap scripts using Clima.Linking executable script to ~/.local/binLet's say those lines were written in a file namedscript.py. Command line usage in a terminal would then be e.g.:python script.py foo
python script.py foo --a 42Adding this line inscript.py#!/usr/bin/env pythonand changing its execution permissions (mac, linux):chmod +x script.pyAllows for a shorter means of execution:./script.py fooNow this could be linked as an adhoc command for example:ln -s $PWD/script.py ~/.local/bin/my_commandPackaging a module (pip ready)For a pip-installable package, one couldpackage this as a runnable command-
publish in the public or one's private pypi etc - and then approach the convenience factor shown at first.pip install my_tool
my_command foo -hTo publish with poetry is quite straight forward. First create an account in pypi.org and then:cd <project directory>
poetry build
poetry publishYou can useversionto bump up versions:poetry version patchBuilding/Installing from sourceThis repo is based onpoetry.git clone https://github.com/d3rp/clima.git 
cd clima
poetry install --no-devThe--no-devis for to install the running environment without development tooling.tocLong description and backgroundYou can write your subcommands as a class encapsulating the "business logic". Clima encapsulates the command-line arguments as a container, mapping its attributes as you declare them in a simple schema class.In other words, you can use this to wrap your scripts as command line commands without resorting to Bash or maintaining argument parsing in python. It removes the need to duplicate comments to have--helpremember what the arguments were and what they did. Sprinkling some decorator magic offers a typical user experience of a CLI program (e.g. argument parsing and validation, --help, subcommands, ...).The premise behind Clima is that a simple script usually uses a script wide global configuration throughout the user code, or in other words, a context for the program accessed in different parts of the code. Clima populates that context, or configuration, with given arguments falling back on defaults in the code and some further complimentary options. Those are then made accessible via a globalcvariable, or container, that can be tossed around the code base with minimal additional effort.
With almost no additional effort, Clima can provide autocompletion in IDEs (as attributes). This feature helps when extending your code as the autocomplete kicks in after typingc.offering those fields in your "schema" as attributes.Why another cli framework?Clima does not try to cater as a feature-complete CLI framework like the ones listed below. It is a package to help with boilerplate to get quick but reusable tools for your workflow.Other options for a full-featured CLI experience:docoptfirecleoclicktyperDependenciesdotenvgnugpg - this is pass through though. If it's not installed, the feature is not in use.fire -python-firefrom google does the cli wrapping / forked and included
into the repo - I wanted to have the version 0.1.x formatting and help output with few hacks of my owntoc