balerin 0.1.4
A python package startup orchestrator. it can handle loading all packages of an application at startup time respecting package dependencies.
License: BSD-3-Clause
Requires Python: >=3.6
Keywords: python balerin package-manager packaging startup auto-importstartup-import syntax-error package-loader

# Balerin
## Your application will dance with Balerin!

Balerin is a python package startup orchestrator. it can handle loading all
packages of an application at startup time respecting package dependencies.

Loading all application packages at startup has many benefits:

- Revealing all syntax errors and invalid imports on startup, preventing runtime failure.
- Initializing all required objects at startup, providing better performance at runtime.
- Triggering all first level objects of a module (ex. decorators) without the need to 
  manually trigger them.
- Better code maintainability by letting you distribute the code on their suitable 
  packages and preventing the need for a centralized location for nonsense 
  imports (ex. models, hooks, managers, ...).
- Auto registration of celery tasks without the need to name all task modules a 
  specific name (usually tasks).
- Populating application level caches at startup to boost performance at runtime.

## Can I Use Balerin With?

Yes, you can. Balerin can be used in conjunction with all major frameworks 
([Flask](https://github.com/pallets/flask), [FastAPI](https://github.com/tiangolo/fastapi), 
 [Sanic](https://github.com/sanic-org/sanic), [AIOHTTP](https://github.com/aio-libs/aiohttp), 
 [Bottle](https://github.com/bottlepy/bottle), [Pyramid](https://github.com/Pylons/pyramid),
 [Tornado](https://github.com/tornadoweb/tornado), [Django](https://github.com/django/django))
and many more. You can also use it on bare python applications without a 
framework, it's all up to you.

### What about Pyrin?

It's a good question. [Pyrin](https://github.com/mononobi/pyrin) has builtin support for 
Balerin, so you can just use Pyrin without the need to prepare Balerin on your own.

## Installing

**Install using pip**:

**`pip install balerin`**

## Usage Example

**There are two ways to use Balerin in your project:**

- *`Basic`*: Loading all packages based on filesystem order.
  (the order could be changed between each run).
- *`Pro`*: Loading all packages respecting their dependencies on each other.
  (the order will be always the same on every run).

**Sample Project Structure:**

- root_dir
  - my_app
    - accounting
      - `__init__.py`
      - `api.py`
    - customers
      - `__init__.py`
      - `models.py`
    - `__init__.py`
    - `api.py`
    - `models.py`
  - `start.py`

### Basic Usage:

**`my_app/__init__.py:`**

```python
import os

from flask import Flask
from balerin import PackagingManager


app = Flask('my_app')
working_directory = os.path.abspath(os.getcwd())
root_package = os.path.join(working_directory, 'my_app')
balerin = PackagingManager(root_package, context=dict(important=True, app=app))
```

**`start.py:`**

```python
from my_app import balerin, app

balerin.load_components()
app.run()
```

### Pro Usage:

In order to load packages respecting their dependencies on each other, you must define 
a package class in `__init__.py` file of each package that has dependency on other packages.
The package class must be a subclass of Balerin's `Package` class:

**`my_app/accounting/__init__.py:`**

```python
from balerin import Package


class AccountingPackage(Package):
    # the name of the package.
    # example: `my_app.api`.
    # should get it from `__name__` for each package.
    NAME = __name__

    # list of all packages that this package is dependent
    # on them and should be loaded after all of them.
    # example: ['my_app.logging', 'my_app.api.public']
    # this can be left as an empty list if there is no dependencies.
    DEPENDS = ['my_app.customers']

    # specifies that this package is enabled and must be loaded.
    ENABLED = True

    # name of a module inside this package that should be loaded before all other modules.
    # for example: 'manager'
    # this can be left as None if there is no such file in this package needing early loading.
    COMPONENT_NAME = None
```

Now you can load your packages respecting their dependencies on each other, using 
the sample code in **`Basic Usage`** section.

### How to Choose Between Basic or Pro Usages:

In most cases, you don't need to use the `Pro Usage` style. unless your application 
architecture is based on `Dependency Injection` and `IoC` concepts. so, when in doubt, go 
with `Basic Usage`.

### Initialization Arguments of PackagingManager Class:

- `root`: it can be a single or multiple paths to different packages of your application.
          Balerin will load all sub-packages of each path respectively.
- `base_component`: specifies a module name which must be loaded before all other modules 
                    in each package if available. for example *manager*. this value could be 
                    overridden in each *Package* class using *COMPONENT_NAME* attribute.
- `verbose`: specifies that loading info should be printed on each step.
             defaults to True if not provided.
- `ignored_packages`: list of package names that must be ignored from loading. package names 
                      must be fully qualified. for example: *my_app.accounting.public*. 
                      notice that if a package that has sub-packages added to ignore list, 
                      all of its sub-packages will be ignored automatically even if not 
                      present in ignore list.
- `ignored_modules`: list of module names that must be ignored from loading. 
                     module names could be fully qualified or just the module name itself.
                     for example: *my_app.customers.models* or *models*.
                     notice that if only module name is provided, then all modules matching 
                     the provided name will be ignored from loading.
- `ignored_detector`: a function to be used to detect if a package or module should be ignored.
                      it must take two arguments, the first is the fully qualified name 
                      and the second is a boolean value indicating that the input is a module. 
                      it also should take optional keyword arguments as context. it should 
                      return a boolean value.
                      for example: `my_detector(name, is_module, **context)`
- `module_loader`: a function to be used to load custom attributes of a module. 
                   it should take two arguments, a name and a module instance.  
                   it also should take optional keyword arguments as context. 
                   the output will be ignored. 
                   for example: `my_loader(name, module, **context)`
- `context`: a dict containing all shared contexts to be used for example 
             inside `ignored_detector` and `module_loader` functions.

### PackagingManager Public Interface:

Once you create an object of `PackagingManager` class, you can call 
these methods on the created object:

- `load_components`: load all packages inside the root path.
- `load`: load a single module with provided name.
- `get_loaded_packages`: get a list of all loaded package names.
- `is_package_loaded`: get a value indicating that given package is loaded.
- `get_context`: get a dict of all shared contexts.

```python
import os

from balerin import PackagingManager


working_directory = os.path.abspath(os.getcwd())
root_package = os.path.join(working_directory, 'my_app')
balerin = PackagingManager(root_package, context=dict(important=True))

balerin.load_components()
balerin.load('my_app.accounting.api')
loaded_packages = balerin.get_loaded_packages()
is_package_loaded = balerin.is_package_loaded('my_app.accounting')
context = balerin.get_context()
```

## Hint

`Balerin` is an albanian word and means dancer.