Basic infrastructure for writing scripts in Python

Overview

Base Script

Build Status PyPI version

Python is an excellent language that makes writing scripts very straightforward. Over the course of writing many scripts, we realized that we were doing some things over and over like creating a logger and accepting command line arguments. Base script is a very simple abstraction that takes care of setting up logging and other basics so you can focus on your application specific logic.

Here are some facilities that Base Script offers:

  • Logging
  • Accepting command-line arguments using argparse

Installation

pip install basescript

Usage

Here is a simple example to get started

Hello World

helloworld.py

from basescript import BaseScript

class HelloWorld(BaseScript):
    def run(self):
        print "Hello world"

if __name__ == '__main__':
    HelloWorld().start()

NOTE: all examples showcased here are available under the examples directory

Run the above by doing:

python helloworld.py run

Run script with log level set to DEBUG

python helloworld.py --log-level DEBUG run

Run script with custom log file

python helloworld.py --log-level DEBUG --log mylog run

Command line args, Using the logger

The following is a more involved example

adder.py

from basescript import BaseScript

class Adder(BaseScript):
    # The following specifies the script description so that it be used
    # as a part of the usage doc when --help option is used during running.
    DESC = 'Adds numbers'

    def __init__(self):
        super(Adder, self).__init__()
        self.a = 10
        self.b = 20

    def define_args(self, parser):
        parser.add_argument('c', type=int, help='Number to add')

    def run(self):
        self.log.info("Starting run of script ...")

        print self.a + self.b + self.args.c

        self.log.info("Script is done")

if __name__ == '__main__':
    Adder().start()

Run the script as follows and observe the usage information shown. Note how the description appears along with the c argument.

python adder.py --help
usage: adder.py [-h] [--name NAME] [--log-level LOG_LEVEL]
                [--log-format {json,pretty}] [--log-file LOG_FILE] [--quiet]
                [--metric-grouping-interval METRIC_GROUPING_INTERVAL]
                [--debug]
                {run} ...

Adds numbers

optional arguments:
  -h, --help            show this help message and exit
  --name NAME           Name to identify this instance
  --log-level LOG_LEVEL
                        Logging level as picked from the logging module
  --log-format {json,pretty}
                        Force the format of the logs. By default, if the
                        command is from a terminal, print colorful logs.
                        Otherwise print json.
  --log-file LOG_FILE   Writes logs to log file if specified, default: None
  --quiet               if true, does not print logs to stderr, default: False
  --metric-grouping-interval METRIC_GROUPING_INTERVAL
                        To group metrics based on time interval ex:10 i.e;(10
                        sec)
  --debug               To run the code in debug mode

commands:
  {run}
python adder.py run --help
usage: adder.py run [-h] c

positional arguments:
  c           Number to add

optional arguments:
  -h, --help  show this help message and exit

Run the script now to see the intended output

python adder.py run 30
60

Run the same with info and higher level logs enabled

python adder.py --log-level INFO 30
2016-04-10 13:48:27,356 INFO Starting run of script ...
60
2016-04-10 13:48:27,356 INFO Script is done

--log-level accepts all the values shown at https://docs.python.org/2/library/logging.html#logging-levels.

log is a log object created using python's standard logging module. You can read more about it at https://docs.python.org/2/library/logging.html.

Sub commands

When we have multiple functionalities then there is a way to call or execute each functionality with different sub command.

For example if we have add and subtract in the same script then we can call each functionality with different sub command.

calc.py

from basescript import BaseScript

class Calc(BaseScript):
    A = 10

    def add(self):
        print(self.A + self.args.b)

    def sub(self):
        print(self.A - self.args.b)

    def define_subcommands(self, subcommands):
        super(Calc, self).define_subcommands(subcommands)

        add_cmd = subcommands.add_parser("add", help="Adds number")
        add_cmd.set_defaults(func=self.add)
        add_cmd.add_argument('--b', type=int, help="Number to add")

        sub_cmd = subcommands.add_parser("sub", help="Subtracts number")
        sub_cmd.set_defaults(func=self.sub)
        sub_cmd.add_argument('--b', type=int, help="Number to subtract")

if __name__ == '__main__':
    Calc().start()

Run

$ python3 calc.py add --b 4
14
$ python3 calc.py sub --b 4
6

Metric-Grouping

When writing a Metric using self.log, you can specify type=metric. If this is done, a background thread will automatically group multiple metrics into one by averaging values (to prevent writing too many log lines). test.py

from basescript import BaseScript
import time
import random

class Stats(BaseScript):
    def __init__(self):
        super(Stats, self).__init__()

    def run(self):
        ts = time.time()
        while True:
            # Metric Format.
            self.log.info("stats", time_duration=(time.time()-ts), type="metric", random_number=random.randint(1, 50))

if __name__ == '__main__':
    Stats().start()

Run the command to see the output.

python test.py --metric-grouping-interval 5 run
Comments
  • #Feature: Auto json conversion using pipe(|).

    #Feature: Auto json conversion using pipe(|).

    Read-Me:

    • As per discussion with @prashanthellina while running the Basescript code intially it will write pretty format logs to the console. screen shot 2018-04-11 at 3 15 36 pm
    • If we specify pipe(|) after the command it must print json format to the console. ex: python test.py run | jq -C . | less -R
    Type: Enhancement inprogress-status meta-team moveto-inprogress-eod 
    opened by ghost 24
  • Basescript command with subcommand to produce template for writing new script

    Basescript command with subcommand to produce template for writing new script

    Additional requirement:

    We should consider generating an entire project hierarchy project tree layout (with template README.md, setup.py, travis config etc)

    This will make it easy for the user to make it easy to make the script pip installable and have command installed to standard bin location.

    medium-impact inprogress-status low-priority about-halfday-effort feature-request-type meta-team moveto-inprogress-3m 
    opened by prashanthellina 15
  • Bump pyyaml from 5.1.1 to 5.4

    Bump pyyaml from 5.1.1 to 5.4

    Bumps pyyaml from 5.1.1 to 5.4.

    Changelog

    Sourced from pyyaml's changelog.

    5.4 (2021-01-19)

    5.3.1 (2020-03-18)

    • yaml/pyyaml#386 -- Prevents arbitrary code execution during python/object/new constructor

    5.3 (2020-01-06)

    5.2 (2019-12-02)

    • Repair incompatibilities introduced with 5.1. The default Loader was changed, but several methods like add_constructor still used the old default yaml/pyyaml#279 -- A more flexible fix for custom tag constructors yaml/pyyaml#287 -- Change default loader for yaml.add_constructor yaml/pyyaml#305 -- Change default loader for add_implicit_resolver, add_path_resolver
    • Make FullLoader safer by removing python/object/apply from the default FullLoader yaml/pyyaml#347 -- Move constructor for object/apply to UnsafeConstructor
    • Fix bug introduced in 5.1 where quoting went wrong on systems with sys.maxunicode <= 0xffff yaml/pyyaml#276 -- Fix logic for quoting special characters
    • Other PRs: yaml/pyyaml#280 -- Update CHANGES for 5.1

    5.1.2 (2019-07-30)

    • Re-release of 5.1 with regenerated Cython sources to build properly for Python 3.8b2+
    Commits
    • 58d0cb7 5.4 release
    • a60f7a1 Fix compatibility with Jython
    • ee98abd Run CI on PR base branch changes
    • ddf2033 constructor.timezone: _copy & deepcopy
    • fc914d5 Avoid repeatedly appending to yaml_implicit_resolvers
    • a001f27 Fix for CVE-2020-14343
    • fe15062 Add 3.9 to appveyor file for completeness sake
    • 1e1c7fb Add a newline character to end of pyproject.toml
    • 0b6b7d6 Start sentences and phrases for capital letters
    • c976915 Shell code improvements
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    inprogress-status moveto-inprogress-1m dependencies 
    opened by dependabot[bot] 7
  • Add support for env file

    Add support for env file

    • User should be able to provide an env variable that will be appended to every logline

    refer: https://github.com/nudjur/infra/issues/307

    requirements

    • Add a command-line argument to accept the env file
    • env file will be a dictionary in YAML configuration
    • whenever a signal is received it should read the env variables from the <>.yaml file and update the log line
    inprogress-status moveto-inprogress-1h 
    opened by rajinish01 6
  • TypeError: init_logger() got an unexpected keyword argument 'pre_hooks'

    TypeError: init_logger() got an unexpected keyword argument 'pre_hooks'

    • we got the below issue when ran the sample hello_world.py which is in README
    • TypeError: init_logger() got an unexpected keyword argument 'pre_hooks' image
    • basescript version(0.3.3) image
    bug-type inprogress-status 
    opened by prasannababuAddagiri 6
  • Unittest does not run properly

    Unittest does not run properly

    Hi there,

    First, thank you for your package ;)

    I'm unfortunately running on an issue. I've written unit tests, and trying to run them, I'm getting this error :

    python -m unittest discover -s tests -p "*_tests.py"
    usage: python -m unittest [-h] [--name NAME] [--log-level LOG_LEVEL]
                              [--log-format {json,pretty}] [--log-file LOG_FILE]
                              [--quiet]
                              [--metric-grouping-interval METRIC_GROUPING_INTERVAL]
                              [--debug] [--minimal]
                              {run} ...
    python -m unittest: error: argument commands: invalid choice: 'discover' (choose from 'run')
    E
    ======================================================================
    ERROR: test_MY_SCRIPT_MY_FUNCTION (MY_SCRIPT_tests.MYSCRIPTSTests)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 1787, in parse_known_args
        namespace, args = self._parse_known_args(args, namespace)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 1975, in _parse_known_args
        positionals_end_index = consume_positionals(start_index)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 1952, in consume_positionals
        take_action(action, args)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 1845, in take_action
        argument_values = self._get_values(action, argument_strings)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 2386, in _get_values
        self._check_value(action, value[0])
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 2433, in _check_value
        raise ArgumentError(action, msg % args)
    argparse.ArgumentError: argument commands: invalid choice: 'discover' (choose from 'run')
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/my/home/path/tests/MY_SCRIPT_tests.py", line 13, in test_MY_SCRIPT_MY_FUNCTION
        MY_SCRIPT = StatsGtMatrix()
      File "/my/home/path/scripts/MY_SCRIPT.py", line 84, in __init__
        super(StatsGtMatrix, self).__init__()
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/site-packages/basescript/basescript.py", line 29, in __init__
        self.args = self.parser.parse_args(args=args)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 1755, in parse_args
        args, argv = self.parse_known_args(args, namespace)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 1794, in parse_known_args
        self.error(str(err))
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 2508, in error
        self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
      File "/miniconda/path/envs/bioinfo_sdk/lib/python3.7/argparse.py", line 2495, in exit
        _sys.exit(status)
    SystemExit: 2
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.005s
    
    FAILED (errors=1)
    

    Here is a sample from my unittest file :

    import unittest
    from unittest.mock import MagicMock
    
    from scripts.my_script import MyScript, ClassA, ClassB
    
    class MyScriptTests(unittest.TestCase):
    
        _fake_A_classes = [ClassA('ped1'), ClassA('ped2')]
    
        _fake_B_class = ClassB('ped1','A','A', False, False)
    
        def test_my_script_my_function(self):
            my_script = MyScript()
            my_script.A_classes = \
            MagicMock(
                return_value=self._fake_A_classes
            )
    
            returned_ped = my_script.my_function(self._fake_B_class)
    
            self.assertEqual('ped1', returned_ped.name)
    

    and from my script :

    ...
    class MyScript(BaseScript):
        # The following specifies the script description so that it be used
        # as a part of the usage doc when --help option is used during running.
        DESC = """
        Usage
        """
    
        def __init__(self):
            #this set class attribute
            super(MyScript, self).__init__()
            self.custom_array = []
    
        def define_args(self, parser):
            parser.add_argument('arg_one', type=str, help='input file path')
    
        def my_function(self, class_a: ClassA):
        # function I'm trying to test
            return class_a
    ...
    

    It's not my real Classes, path, functions name.

    I'm wondering how to run unittest for a basescript script ?

    Any help would be appreciated.

    Best regards

    next-status 
    opened by tomraulet 5
  • Document: Logging guidelines

    Document: Logging guidelines

    Basescript supports structured logging based on structlog (https://www.structlog.org/en/stable/why.html). However, the documentation here does not list the capabilities and the best practices for logging (eg: every log is an event and the string should look like an identifier; values most not be encoded into this string but instead logged a kv pairs etc)

    ready-status meta-team moveto-inprogress-3m 
    opened by prashanthellina 5
  • #Feature: Explore click module in basescript.

    #Feature: Explore click module in basescript.

    Read-me:

    • The new click module read command line arguments with more features we have to understand more about it and then decide to explore it or not. Refer: http://click.pocoo.org/5/why/#
    ready-status meta-team moveto-inprogress-3m 
    opened by ghost 5
  • Can we have a

    Can we have a "logged_metric" type

    If we specify the type as "metric" basescript does grouping and also drops the "__fields", I want something wich will mention the type as "metric" and but not drop the "__fields".

    opened by supriyopaul 5
  • references #106, on receiveding SIGUSR1, reopening log file to enable…

    references #106, on receiveding SIGUSR1, reopening log file to enable…

    … external log rotation tools to work

    This fix ensures that when an external log rotation tool such as logrotate renames the current log file, it can inform the basescript based process by sending a SIGUSR1 signal. Upon receipt, basescript will now close the currently open log file and reopen it. This ensures that newly written log lines after the file move don't continue to go into the renamed file but instead to a file with the original file name.

    opened by prashanthellina 4
  • Multiprocessing with basescript

    Multiprocessing with basescript

    Multiprocessing is not working with basescript (method is not getting called). If I remove basescript then it is working.

    Multiprocessing with basescript

    from multiprocessing import Pool
    
    from basescript import BaseScript
    
    class VocabGen(BaseScript):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
        def process_single_file(self):
            processes = []
            pool = Pool()
            for i in range(10):
                p = pool.apply_async(self.sleep_two_secs)
                processes.append(p)
                start = i 
    
            for p in processes:
                p.wait()
    
        def sleep_two_secs(self):
            print("Came inside")
    
        def run(self):
            self.process_single_file()
    
    if __name__ == '__main__':
        VocabGen().start()
    

    RUN: python3 base.py run Output: Empty output. But exepected is "Came inside" should get print 10 times.

    Multiprocessing without basescript

    from multiprocessing import Pool
    
    class VocabGen():
        def __init__(self, *args, **kwargs):
            pass
    
        def process_single_file(self):
            processes = []
            pool = Pool()
            for i in range(10):
                p = pool.apply_async(self.sleep_two_secs)
                processes.append(p)
                start = i 
    
            for p in processes:
                p.wait()
    
        def sleep_two_secs(self):
            print("Came inside")
    
        def run(self):
            self.process_single_file()
    
    if __name__ == '__main__':
        v = VocabGen()
        v.run()
    

    RUN: python3 test.py Output: "Came inside" is getting print for 10 times.

    inprogress-status moveto-inprogress-1m 
    opened by RamanjaneyuluIdavalapati 4
Releases(0.3.9)
Owner
Deep Compute, LLC
Deep Compute, LLC
Python3 Interface to numa Linux library

py-libnuma is python3 interface to numa Linux library so that you can set task affinity and memory affinity in python level for your process which can help you to improve your code's performence.

Dalong 13 Nov 10, 2022
My solutions for Advent of Code 2021 🌟🎄

🌟 Advent of Code 2021 🎄 My solutions for Advent of Code 2021. About · What is Advent of Code? · Contents · Usage · Table of puzzles (TODO: add final

Amanda P. Pinha 2 Dec 05, 2022
The git for the Python Story Utility Package library.

PSUP, The Python Story Utility Package Module. PSUP helps making stories or games with options, diverging paths, different endings and so on. You can

Enoki 6 Nov 27, 2022
Projects using the Tkinter module in Python!

Tkinter projects This repository includes some Tkinter projects made by me. All of these are simple to understand. I create apps with good functionali

Amey 0 Sep 24, 2021
Get a list of content on your Netflix My List that is expiring in the next month or two.

Netflix My List Expiring Movies Annoyed at Netflix for taking away your movies? Now you don't have to be! Installation instructions Install selenium C

24 Aug 06, 2022
This is the repo for Uncertainty Quantification 360 Toolkit.

UQ360 The Uncertainty Quantification 360 (UQ360) toolkit is an open-source Python package that provides a diverse set of algorithms to quantify uncert

International Business Machines 207 Dec 30, 2022
Dapp / Forge traces enhancer

traces-explorer Dapp / Forge traces enhancer Usage traces.py and pattern_* files should be in the same directory make test traces.txt py traces.

1 Feb 02, 2022
Datargsing is a data management and manipulation Python library

Datargsing What is It? Datargsing is a data management and manipulation Python library which is currently in deving Why this library is good? This Pyt

CHOSSY Lucas 10 Oct 24, 2022
Attempt at a Windows version of the plotman Chia Plot Manager system

windows plotman: an attempt to get plotman to work on windows THIS IS A BETA. Not ready for production use just yet. Almost, but not quite there yet.

59 May 11, 2022
Play tic-tac-toe in PowerPoint

The presentation has around 6,000 slides representing every possible game state (and some impossible ones, since I didn't check for wins or ties). You play by clicking on the squares, which are hyper

Jesse Li 3 Dec 18, 2021
This repository contains code for building education startup.

Learning Management System Overview It's the code for EssayBrain, a tool for teacher that automatically grades and validates essays. In order to valid

Shyam Das Shrestha 1 Nov 21, 2021
Information about a signed UEFI Shell that can be used when Secure Boot is enabled.

SignedUEFIShell During our research of the BootHole vulnerability last year, we tried to find as many signed bootloaders as we could. We searched all

Mickey 61 Jan 03, 2023
An ultra fast cross-platform multiple screenshots module in pure Python using ctypes.

Python MSS from mss import mss # The simplest use, save a screen shot of the 1st monitor with mss() as sct: sct.shot() An ultra fast cross-platfo

Mickaël Schoentgen 799 Dec 30, 2022
A Python module for decorators, wrappers and monkey patching.

wrapt The aim of the wrapt module is to provide a transparent object proxy for Python, which can be used as the basis for the construction of function

Graham Dumpleton 1.8k Jan 06, 2023
An open source recipe book from the awesome staff of Clinical Genomics

meatballs An open source recipe book from the awesome staff of Clinical Genomics.

Clinical Genomics 2 Dec 07, 2021
This application is made solely for entertainment purposes

Timepass This application is made solely for entertainment purposes helps you find things to do when you're bored ! tells jokes guaranteed to bring on

Omkar Pramod Hankare 2 Nov 24, 2021
Python implementation of an automatic parallel parking system in a virtual environment, including path planning, path tracking, and parallel parking

Automatic Parallel Parking: Path Planning, Path Tracking & Control This repository contains a python implementation of an automatic parallel parking s

134 Jan 09, 2023
TeamFleming is a multicultural group of 20 young bioinformatics enthusiasts participating in the 2021 HackBio Virtual Summer Internship

💻 Welcome to Team Fleming's Repo! #TeamFleming is a multicultural group of 20 young bioinformatics enthusiasts participating in the 2021 HackBio Virt

3 Aug 08, 2021
This is a Fava extension to display a grouped portfolio view in Fava for a set of Beancount accounts.

Fava Portfolio Summary This is a Fava extension to display a grouped portfolio view in Fava for a set of Beancount accounts. It can also calculate MWR

18 Dec 26, 2022