Application guide¶
This library is supposed to provide tools for constructing your asynchronous server application. See it for yourself in app - base application classes.
Application class is currently based on aiohttp app. Basically application start consists of two stages: loading Config files and initialization of the application Services.
To create an app you can use init_app() function. You can also start an aiohttp server by
using run_server().
from kaiju_tools import run_server, init_app
run_server(
init_app,
base_config_paths=['./settings/config.yml'],
base_env_paths=['./settings/env.base.json'],
default_env_paths=['./settings/env.local.json'],
)
Check out Basic application on how to construct your application from scratch and also RPC guide and RPC services HOWTO on how to implement and run RPC services.
Services¶
The main building block of the app is Service - a class which has specific functions in
the app and can interact with other such classes. Services are quasi-independent: they may require each other, some
of them may require initialization, but they can be arranged in different ways in the app configuration system.
To create a service you must inherit it from Service, register your class in the service
classes registry and write it in your app configuration file (see Config files).
from kaiju_tools import Service, SERVICE_CLASS_REGISTRY
class MyService(Service):
"""My service does something I'm not sure."""
SERVICE_CLASS_REGISTRY.register(MyService)
To create a service configuration you must create a ServiceSettings object in the
services section of your app .yml config file. The simplest config would look like this:
services:
- cls: MyService
However there are additional useful options for service configuration.
services:
- cls: MyService
name: my_service_with_different_name
enabled: True
required: False
loglevel: ERROR
settings:
some_value: 42
name - a custom name for a service, imagine you have two services of the same class but with different settings. You can set custom names to prevent conflict and configure service discovery by name for the dependent services (see Dependencies).
enabled - a disabled service won’t be initialized.
required - this option is different. The app will try to initialize the service, but if it fails, the app will continue anyway.
loglevel - override application log level for this service
settings - you can put __init__ parameters here
Contextable services¶
Sometimes a service may require intricate init or close logic. Use ContextableService
as a base to be able to implement init(). It is executed by the app after
all the services have been created.
There’s also close() method executed on app termination. You can use
it to close connection pool, cleanup local cache, send notifications, etc.
from kaiju_tools import ContextableService, SERVICE_CLASS_REGISTRY
class MyService(ContextableService):
_pool = None
async def init(self):
self._pool = await self._initialize_pool()
async def close(self):
if self._pool:
self._pool.close()
SERVICE_CLASS_REGISTRY.register(MyService)
Currently contextable services use aiohttp cleanup context to call init and close methods.
Dependencies¶
Where may be a case when your service need to access another service. There is a way to discover services across
the app using discover_service() method.
# will discover a service of class `MyService`
my_service = service.discover_service(None, cls=MyService)
# will discover a service of class `MyService` but only if its name is 'my_service'
my_service = service.discover_service('my_service', cls=MyService)
# will discover a service of class `MyService` but if not then `None` will be returned
my_service = service.discover_service(None, cls=MyService, required=False)
You can use it to get dependencies in your service __init__, however it may cause a problem if
your service is initialized before the dependency. To prevent this you can use
ContextableService as base and discover dependencies
in init().
class MyService(ContextableService):
def __init__(self, app, dep: MyDependency = None, logger):
ContextableService.__init__(self, app=app, logger=logger)
self._dep = dep
async def init(self):
self._dep = self.discover_service(self._dep, cls=MyDependency)
Internal scheduler¶
schedule_task() can be used to schedule in-app periodic tasks.
You can customize task execution behavior using ExecPolicy.
Use task.enabled = False to stop the task from scheduling.
from kaiju-tools import Scheduler, ContextableService
class MyService(ContextableService):
async def init(self):
_scheduler = self.discover_service(None, cls=Scheduler)
self._task = _scheduler.schedule_task(
self._reload_local_cache, interval=60, name=f'{self.service_name}._reload_local_cache',
policy=_scheduler.ExecPolicy.WAIT)
async def close(self):
self._task.enabled = False #: disable the task
async def _reload_local_cache(self) -> None:
...
The scheduler itself can be enabled in config in the services block.
services:
- cls: Scheduler
RPC Server¶
JSONRPCServer() is used to process requests. See RPC guide.
It can be enabled in config in the services section.
services:
- cls: JSONRPCServer
name: rpc
settings:
default_request_time: 60
max_request_time: 600
enable_permissions: True
request_logs: True
response_logs: False
Loggers¶
Library logger is a modified version of the standard logger. It has the same syntax but will automatically insert client request context (such as correlation id) in a log message. It also allows you to write extra data in plain keyword arguments.
For better log aggregation it is recommended to not format the log message itself, providing contextual data as extras.
self.logger.info('Log in completed.', user_id=user_id) # user_id will be available in extras
LoggingService provides a service for all application
logging. It allows logging setting to be configured in the project settings file.
Example configuration:
services:
- cls: LoggingService
settings:
loggers:
- name: "[main_name]"
handlers: True
handlers:
- cls: JSONHandler
name: default
See HandlerSettings and LoggerSettings for a description
of loggers and handlers config parameters.
There are two standard formatter classes.
TextFormatter provides better readability and can be used when you need to read
from the terminal. There’s also a standard handler for this formatter called TextHandler.
Example of standard output:
(time | level | correlation id | service | message | extra data)
12:18:40 | INFO | 135125a5 | app.rpc | rpc accepted | {'request': {'method': 'rpc.api', 'id': 0}}
DataFormatter provides encoded output with extended data. It may be used when
collecting application output from stdout and directing it to a log aggregator (logstash etc.).
There’s also a standard handler for this formatter called JSONHandler.
Example of output:
{"t":1682328051.3172748,"name":"app.rpc","lvl":"INFO","msg":"rpc accepted","cid":"20d35cfc","sid":"6f580b919f00704584a0453a883310be","t_max":1682328142,"data":{"request":{"method":"rpc.api","id":0}}}
Config files¶
Configuration is handled by ConfigLoader. There are two types of configuration files:
.yml which contain a project structure with all main settings and a description of the services, and .env.json
files containing variables for the yml files.
YML files¶
*.yml files contain the basic data structure of a ProjectSettings class.
An example of an application config file. Note that you can use templates here (see Templates guide).
packages:
- kaiju_db
app:
debug: "[app_debug]"
run:
host: "[run_host]"
port: "[run_port]"
main:
name: "[main_name]"
version: "1.0"
env: "[main_env:'dev']"
loglevel: "[main_loglevel:'INFO']"
etc:
additional_data: 123
services:
- cls: MyService
name: my_service
settings:
value: 42
packages contains a list of imported service packages, all service classes from the specified package will be registered automatically.
app contains aiohttp Application parameters.
run contains aiohttp run_app() arguments such as host and port.
main must contain
Appdata such as name, version, environment, loglevel etc.etc may contain any information and metadata, it’s not directly used by the app
services should contain a list of services to be initialized
Note that you can use multiple .yml files in your project: data of them will be merged. It’s a good idea to split a big list of services into a few files.
For example, if you have [‘settings/config.yml’, ‘settings/services.yml’] listed in base_config_paths of
run_server() function and you
specify additional config files in CLI:
python -m app -c settings/dev.yml -c settings/local.yml` then the files
will be merged in the same order as written:
['settings/config.yml', 'settings/services.yml', 'settings/dev.yml', 'settings/local.yml']
JSON files¶
*.env.json files contain a flat map of config parameters. These parameters are inserted in .yml files as template values.
{
"app_debug": true,
"main_name": "my_app",
"run_host": "localhost",
"run_port": 9999,
"main_env": "prod",
"main_loglevel": "DEBUG"
}
Note that you can use multiple .json files which will be merged, so you can have a main configuration and sub-configurations.
For example, if you have [‘settings/base.env.json’] listed in base_env_paths of
run_server() function and [‘settings/local.env.json] in default_env_paths, and you
specify additional env files in CLI:
python -m app -f settings/executor.env.json -f settings/local.executor.env.json
then the files will be merged in the same order as written and `default_env_paths` will be replaced.
['settings/base.env.json', 'settings/executor.env.json', 'settings/local.executor.env.json']
Note that you do not need to set all keys in each file. All existing keys will be merged into a single mapping.
Environment variables¶
The config loader will auto-load environment variables with same keys as in the .env files. A vlue is evaluated using python eval() function.
run_port=9999
python -m app
Note that the key must be present in your .env files.
CLI values¶
You can pass an environment value using -e <key>=<value> flag. A value is evaluated using python eval() function.
python -m app -e main_env=test
CLI values have maximum priority over all other config sources. Note that the key must be present in your .env files.
CLI arguments¶
You can see supported CLI arguments by running
python -m app --help
Loading order¶
A configuration is gathered and resolved in the following order:
.yml files from base_config_paths value of
run_server()function.yml files provided by CLI -c
.env.json files from base_env_paths value of
run_server()function.env.json files provided by CLI -f or by default_env_paths value of
run_server()when no CLI arguments providedlocal system environment variables
env values provided by CLI -e
A ProjectSettings object is produced containing full app configuration.
Basic application¶
See kaiju base application template as an example.
| /
| ├── app/
| │ ├── services/
| │ │ ├── __init__.py
| │ ├── application.py
| │ ├── commands.py
| │ ├── tables.py
| │ ├── views.py
| │ ├── __main__.py
| │ └── __init__.py
| ├── settings/
| │ ├── config.yml
| │ ├── services.yml
| │ ├── env.json
| │ └── env.local.json
| ├── docs/
| ├── fixtures/
| ├── tests/
| ├── README.rst
| └── requirements.txt
views.py may contain custom aiohttp web views.
commands.py may contain custom CLI commands.
Use application.py to customize the application class and initialize routes. This example is generally a good starting point.
from kaiju_tools import App
from kaiju_tools.app import init_app as init_app_base
from kaiju_tools.http import JSONRPCView
from app.tables import METADATA
from app.views import MyView
__all__ = ['init_app', 'URLS']
URLS = [
('*', '/public/rpc', JSONRPCView, 'json_rpc_view'),
('*', '/my/view', MyView, 'my_view')
] #: web app routes
def init_app(settings, **kws) -> App:
_dir = Path(__file__).parent
app = init_app_base(settings, attrs={'db_meta': METADATA}, **kws)
for method, reg, handler, name in URLS:
app.router.add_route(method, reg, handler)
app.router.add_route(method, reg + '/', handler, name=name)
return app
services sub-package should contain all of your app custom services. It’s a good idea to import all
your service classes into services/__init__.py to be able to register them later in the
SERVICE_CLASS_REGISTRY by a single command.
from kaiju_tools.app import COMMANDS, SERVICE_CLASS_REGISTRY
import app.commands
from app import services
__version__ = '1.0.0'
SERVICE_CLASS_REGISTRY.register_from_module(services)
COMMANDS.register_from_module(app.commands)
In you main file you should run the server specifying which config and env files are to be used as base.
from kaiju_tools.app import run_server
from app.application import init_app
if __name__ == '__main__':
run_server(
init_app,
base_config_paths=[
'./settings/config.yml',
'./settings/services/services.yml'
],
base_env_paths=['./settings/env.json'],
default_env_paths=['./settings/env.local.json'],
)