Compare commits

...

32 Commits

Author SHA1 Message Date
2a98575790 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-24 17:11:14 +02:00
ed51bc483d corrections in log 2021-04-24 17:11:06 +02:00
52209a5ebd Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-24 14:36:02 +02:00
0442e772c2 support str value 2021-04-24 14:35:51 +02:00
b0edfb7b01 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-24 14:16:11 +02:00
27031dbf0e log_connexion 2021-04-24 14:15:54 +02:00
cb4dde1dc4 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-24 12:56:52 +02:00
9ebe79d533 special connexion for log (do not rollback if error) 2021-04-24 12:56:44 +02:00
c740ec3fe3 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-24 10:12:51 +02:00
56b1f12a4a Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-16 09:34:21 +02:00
4fc3e74bbd Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-13 10:31:24 +02:00
83d74c2b06 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-04-12 15:11:54 +02:00
6a27b002ff Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2021-03-27 10:59:19 +01:00
e2d73932c0 add sdnotify dependency 2020-11-14 19:11:57 +01:00
980a119ef9 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-11-14 19:01:34 +01:00
b9da2ce686 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-11-14 08:12:50 +01:00
941261c830 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-10-14 18:30:13 +02:00
98c77bf719 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-09-20 21:33:15 +02:00
1b9d87fa53 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-09-19 10:33:34 +02:00
0e988d7040 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-09-19 09:20:04 +02:00
be97d757d9 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-09-16 17:38:04 +02:00
19d90fd9bc Delete changelog 2020-09-16 10:39:37 +02:00
5653de1e99 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-09-16 08:16:33 +02:00
399bfb9ab6 add sql in risotto package 2020-09-06 09:47:14 +02:00
234b82b459 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-08-12 15:09:49 +02:00
c9e0bcbbfe update dependencies 2020-08-12 15:09:20 +02:00
47e4976f54 install python3-risotto 2020-08-12 11:09:45 +02:00
dd33ea5b8f add dh-python build dependency 2020-08-12 10:59:26 +02:00
689df4ec23 Merge branch 'develop' into dist/risotto/risotto-2.8.0/develop 2020-08-12 10:48:59 +02:00
223fb9aaf3 separate risotto and python3-risotto package 2020-08-12 10:48:12 +02:00
bed27a1e58 separate risotto and python3-risotto package 2020-08-12 10:44:59 +02:00
40eff91684 ajout de la dependance vers asyncpg 2020-08-12 08:41:15 +02:00
9 changed files with 254 additions and 202 deletions

5
debian/changelog vendored
View File

@ -1,5 +0,0 @@
risotto (0.1) unstable; urgency=low
* first version
-- Cadoles <contact@cadoles.com> Fri, 20 Mar 2020 15:18:25 +0100

14
debian/control vendored
View File

@ -2,13 +2,23 @@ Source: risotto
Section: admin Section: admin
Priority: extra Priority: extra
Maintainer: Cadoles <contact@cadoles.com> Maintainer: Cadoles <contact@cadoles.com>
Build-depends: debhelper (>=11), python3-all, python3-setuptools Build-depends: debhelper (>=11), python3-all, python3-setuptools, dh-python
Standards-Version: 3.9.4 Standards-Version: 3.9.4
Homepage: https://forge.cadoles.com/Infra/risotto Homepage: https://forge.cadoles.com/Infra/risotto
Package: python3-risotto
Architecture: any
Pre-Depends: dpkg, python3, ${misc:Pre-Depends}
Depends: ${python:Depends}, ${misc:Depends},
python3-asyncpg,
python3-rougail,
python3-aiohttp,
python3-sdnotify
Description: configuration manager libraries
Package: risotto Package: risotto
Architecture: any Architecture: any
Pre-Depends: dpkg, python3, ${misc:Pre-Depends} Pre-Depends: dpkg, python3, ${misc:Pre-Depends}
Depends: ${python:Depends}, ${misc:Depends} Depends: ${python:Depends}, ${misc:Depends}, python3-risotto
Description: configuration manager Description: configuration manager

2
debian/risotto.install vendored Normal file
View File

@ -0,0 +1,2 @@
script/risotto-server usr/bin/
sql/risotto.sql usr/share/eole/db/eole-risotto/gen/

0
script/risotto-server Normal file → Executable file
View File

View File

@ -1,12 +1,12 @@
CREATE TABLE RisottoLog( CREATE TABLE RisottoLog(
LogId SERIAL PRIMARY KEY, LogId SERIAL PRIMARY KEY,
ContextId INTEGER,
Msg VARCHAR(255) NOT NULL, Msg VARCHAR(255) NOT NULL,
URI VARCHAR(255), URI VARCHAR(255),
URIS VARCHAR(255), URIS VARCHAR(255),
UserLogin VARCHAR(100) NOT NULL, UserLogin VARCHAR(100) NOT NULL,
Level VARCHAR(10) NOT NULL, Level VARCHAR(10) NOT NULL,
ContextId INTEGER, Kwargs JSON,
Data JSON,
Returns JSON, Returns JSON,
StartDate timestamp DEFAULT current_timestamp, StartDate timestamp DEFAULT current_timestamp,
StopDate timestamp StopDate timestamp

View File

@ -48,8 +48,8 @@ class CallDispatcher:
except AttributeError: except AttributeError:
err = _(f'function {module_name}.{function_name} return the unknown parameter "{key}" for the uri "{risotto_context.version}.{risotto_context.message}"') err = _(f'function {module_name}.{function_name} return the unknown parameter "{key}" for the uri "{risotto_context.version}.{risotto_context.message}"')
raise CallError(err) raise CallError(err)
except ValueError: except ValueError as err:
err = _(f'function {module_name}.{function_name} return the parameter "{key}" with an unvalid value "{value}" for the uri "{risotto_context.version}.{risotto_context.message}"') err = _(f'function {module_name}.{function_name} return the invalid parameter "{key}" for the uri "{risotto_context.version}.{risotto_context.message}": {err}')
raise CallError(err) raise CallError(err)
await config.property.read_only() await config.property.read_only()
mandatories = await config.value.mandatory() mandatories = await config.value.mandatory()
@ -89,6 +89,7 @@ class CallDispatcher:
if hasattr(old_risotto_context, 'connection'): if hasattr(old_risotto_context, 'connection'):
# do not start a new database connection # do not start a new database connection
risotto_context.connection = old_risotto_context.connection risotto_context.connection = old_risotto_context.connection
risotto_context.log_connection = old_risotto_context.log_connection
await log.start(risotto_context, await log.start(risotto_context,
kwargs, kwargs,
info_msg, info_msg,
@ -108,73 +109,83 @@ class CallDispatcher:
config_arguments, config_arguments,
function_obj, function_obj,
) )
except CallError as err: await log.success(risotto_context,
ret,
)
except Exception as err:
await log.failed(risotto_context, await log.failed(risotto_context,
str(err), str(err),
) )
raise err from err raise CallError(err) from err
else: else:
try: error = None
async with self.pool.acquire() as connection: async with self.pool.acquire() as log_connection:
await connection.set_type_codec( await log_connection.set_type_codec(
'json', 'json',
encoder=dumps, encoder=dumps,
decoder=loads, decoder=loads,
schema='pg_catalog' schema='pg_catalog'
) )
risotto_context.connection = connection risotto_context.log_connection = log_connection
async with connection.transaction(): try:
try: async with self.pool.acquire() as connection:
await log.start(risotto_context, await connection.set_type_codec(
kwargs, 'json',
info_msg, encoder=dumps,
) decoder=loads,
await self.check_message_type(risotto_context, schema='pg_catalog'
kwargs, )
) risotto_context.connection = connection
config_arguments = await self.load_kwargs_to_config(risotto_context, async with connection.transaction():
f'{version}.{message}', try:
kwargs, await log.start(risotto_context,
check_role, kwargs,
internal, info_msg,
) )
ret = await self.launch(risotto_context, await self.check_message_type(risotto_context,
kwargs, kwargs,
config_arguments, )
function_obj, config_arguments = await self.load_kwargs_to_config(risotto_context,
) f'{version}.{message}',
# log the success kwargs,
await log.success(risotto_context, check_role,
ret, internal,
) )
if not internal and isinstance(ret, dict): ret = await self.launch(risotto_context,
ret['context_id'] = risotto_context.context_id kwargs,
except CallError as err: config_arguments,
if get_config()['global']['debug']: function_obj,
print_exc() )
await log.failed(risotto_context, # log the success
str(err), await log.success(risotto_context,
) ret,
raise err from err )
except CallError as err: if not internal and isinstance(ret, dict):
raise err from err ret['context_id'] = risotto_context.context_id
except Exception as err: except CallError as err:
# if there is a problem with arguments, just send an error and do nothing if get_config()['global']['debug']:
if get_config()['global']['debug']: print_exc()
print_exc() await log.failed(risotto_context,
async with self.pool.acquire() as connection: str(err),
await connection.set_type_codec( )
'json', raise err from err
encoder=dumps, except CallError as err:
decoder=loads, error = err
schema='pg_catalog' except Exception as err:
) # if there is a problem with arguments, just send an error and do nothing
risotto_context.connection = connection if get_config()['global']['debug']:
async with connection.transaction(): print_exc()
await log.failed(risotto_context, await log.failed(risotto_context,
str(err), str(err),
) )
raise err from err error = err
if error:
if not internal:
err = CallError(str(error))
err.context_id = risotto_context.context_id
else:
err = error
raise err from error
return ret return ret
@ -248,58 +259,66 @@ class PublishDispatcher:
False, False,
False, False,
) )
async with self.pool.acquire() as connection: for function_obj in self.messages[version][message]['functions']:
await connection.set_type_codec( async with self.pool.acquire() as log_connection:
'json', await log_connection.set_type_codec(
encoder=dumps, 'json',
decoder=loads, encoder=dumps,
schema='pg_catalog' decoder=loads,
) schema='pg_catalog'
risotto_context.connection = connection )
for function_obj in self.messages[version][message]['functions']: risotto_context.log_connection = log_connection
function_name = function_obj['function'].__name__ async with self.pool.acquire() as connection:
info_msg = _(f"call function {function_obj['full_module_name']}.{function_name}") await connection.set_type_codec(
try: 'json',
async with connection.transaction(): encoder=dumps,
try: decoder=loads,
await log.start(risotto_context, schema='pg_catalog'
kwargs, )
info_msg, risotto_context.connection = connection
) function_name = function_obj['function'].__name__
await self.check_message_type(risotto_context, info_msg = _(f"call function {function_obj['full_module_name']}.{function_name}")
kwargs, try:
)
await self.launch(risotto_context,
kwargs,
config_arguments,
function_obj,
)
# log the success
await log.success(risotto_context)
except CallError as err:
if get_config()['global']['debug']:
print_exc()
await log.failed(risotto_context,
str(err),
)
except CallError:
pass
except Exception as err:
# if there is a problem with arguments, log and do nothing
if get_config()['global']['debug']:
print_exc()
async with self.pool.acquire() as connection:
await connection.set_type_codec(
'json',
encoder=dumps,
decoder=loads,
schema='pg_catalog'
)
risotto_context.connection = connection
async with connection.transaction(): async with connection.transaction():
await log.failed(risotto_context, try:
str(err), await log.start(risotto_context,
) kwargs,
info_msg,
)
await self.check_message_type(risotto_context,
kwargs,
)
await self.launch(risotto_context,
kwargs,
config_arguments,
function_obj,
)
# log the success
await log.success(risotto_context)
except CallError as err:
if get_config()['global']['debug']:
print_exc()
await log.failed(risotto_context,
str(err),
)
except CallError:
pass
except Exception as err:
# if there is a problem with arguments, log and do nothing
if get_config()['global']['debug']:
print_exc()
async with self.pool.acquire() as connection:
await connection.set_type_codec(
'json',
encoder=dumps,
decoder=loads,
schema='pg_catalog'
)
risotto_context.connection = connection
async with connection.transaction():
await log.failed(risotto_context,
str(err),
)
class Dispatcher(register.RegisterDispatcher, class Dispatcher(register.RegisterDispatcher,

View File

@ -101,15 +101,31 @@ async def handle(request):
internal=False, internal=False,
**kwargs, **kwargs,
) )
except NotAllowedError as err:
raise HTTPNotFound(reason=str(err))
except CallError as err:
raise HTTPBadRequest(reason=str(err).replace('\n', ' '))
except Exception as err: except Exception as err:
if get_config()['global']['debug']: context_id = None
print_exc() if isinstance(err, NotAllowedError):
raise HTTPInternalServerError(reason=str(err)) error_type = HTTPNotFound
return Response(text=dumps({'response': text}), elif isinstance(err, CallError):
error_type = HTTPBadRequest
context_id = err.context_id
else:
if get_config()['global']['debug']:
print_exc()
error_type = HTTPInternalServerError
response = {'type': 'error',
'reason': str(err).replace('\n', ' '),
}
if context_id is not None:
response['context_id'] = context_id
err = dumps({'response': response,
'type': 'error',
})
raise error_type(text=err,
content_type='application/json',
)
return Response(text=dumps({'response': text,
'type': 'success',
}),
content_type='application/json', content_type='application/json',
) )

View File

@ -13,24 +13,24 @@ class Logger:
""" """
async def insert(self, async def insert(self,
msg: str, msg: str,
uri: str,
uris: str,
risotto_context: Context, risotto_context: Context,
level: str, level: str,
data: Any=None, kwargs: Any=None,
start: bool=False, start: bool=False,
) -> None: ) -> None:
uri = self._get_last_uri(risotto_context)
uris = " ".join(risotto_context.paths)
insert = 'INSERT INTO RisottoLog(Msg, URI, URIS, UserLogin, Level' insert = 'INSERT INTO RisottoLog(Msg, URI, URIS, UserLogin, Level'
values = 'VALUES($1,$2,$3,$4,$5' values = 'VALUES($1,$2,$3,$4,$5'
args = [msg, uri, uris, risotto_context.username, level] args = [msg, uri, uris, risotto_context.username, level]
if data: if kwargs:
insert += ', Data' insert += ', Kwargs'
values += ',$6' values += ',$6'
args.append(dumps(data)) args.append(dumps(kwargs))
context_id = risotto_context.context_id context_id = risotto_context.context_id
if context_id is not None: if context_id is not None:
insert += ', ContextId' insert += ', ContextId'
if data: if kwargs:
values += ',$7' values += ',$7'
else: else:
values += ',$6' values += ',$6'
@ -38,7 +38,8 @@ class Logger:
sql = insert + ') ' + values + ') RETURNING LogId' sql = insert + ') ' + values + ') RETURNING LogId'
try: try:
log_id = await risotto_context.connection.fetchval(sql, *args) async with risotto_context.log_connection.transaction():
log_id = await risotto_context.log_connection.fetchval(sql, *args)
if context_id is None and start: if context_id is None and start:
risotto_context.context_id = log_id risotto_context.context_id = log_id
if start: if start:
@ -51,7 +52,7 @@ class Logger:
context_id: int, context_id: int,
uri: Optional[str], uri: Optional[str],
) -> list: ) -> list:
sql = '''SELECT Msg as msg, URI as uri_name, URIS as uris, UserLogin as user_login, Level as level, Data as data, StartDate as start_date, StopDate as stop_date sql = '''SELECT Msg as msg, URI as uri_name, URIS as uris, UserLogin as user_login, Level as level, Kwargs as kwargs, Returns as returns, StartDate as start_date, StopDate as stop_date
FROM RisottoLog FROM RisottoLog
WHERE UserLogin = $1 AND (LogId = $2 OR ContextId = $2) WHERE UserLogin = $1 AND (LogId = $2 OR ContextId = $2)
''' '''
@ -60,18 +61,23 @@ class Logger:
sql += ' AND URI = $3' sql += ' AND URI = $3'
args.append(uri) args.append(uri)
ret = [] ret = []
for row in await risotto_context.connection.fetch(*args): async with risotto_context.log_connection.transaction():
d = {} for row in await risotto_context.log_connection.fetch(*args):
for key, value in row.items(): d = {}
if key == 'data': for key, value in row.items():
if not value: if key in ['kwargs', 'returns']:
value = {} if isinstance(value, dict):
else: pass
value = loads(value) elif not value:
elif key in ['start_date', 'stop_date']: value = {}
value = str(value) else:
d[key] = value value = loads(value)
ret.append(d) if key == 'uris':
value = value.split(' ')
elif key in ['start_date', 'stop_date']:
value = str(value)
d[key] = value
ret.append(d)
return ret return ret
def _get_last_uri(self, def _get_last_uri(self,
@ -109,8 +115,6 @@ class Logger:
paths_msg = self._get_message_paths(risotto_context) paths_msg = self._get_message_paths(risotto_context)
print(_(f'{risotto_context.username}: ERROR: {error} ({paths_msg} with arguments "{arguments}": {msg})')) print(_(f'{risotto_context.username}: ERROR: {error} ({paths_msg} with arguments "{arguments}": {msg})'))
await self.insert(msg, await self.insert(msg,
self._get_last_uri(risotto_context),
paths_msg,
risotto_context, risotto_context,
'Error', 'Error',
arguments, arguments,
@ -127,8 +131,6 @@ class Logger:
if get_config()['global']['debug']: if get_config()['global']['debug']:
print(_(f'{risotto_context.username}: INFO:{paths_msg}: {msg}')) print(_(f'{risotto_context.username}: INFO:{paths_msg}: {msg}'))
await self.insert(msg, await self.insert(msg,
self._get_last_uri(risotto_context),
paths_msg,
risotto_context, risotto_context,
'Info', 'Info',
arguments, arguments,
@ -141,10 +143,12 @@ class Logger:
) -> None: ) -> None:
paths_msg = self._get_message_paths(risotto_context) paths_msg = self._get_message_paths(risotto_context)
if get_config()['global']['debug']: if get_config()['global']['debug']:
print(_(f'{risotto_context.username}: START:{paths_msg}: {msg}')) if risotto_context.context_id != None:
context = f'({risotto_context.context_id})'
else:
context = ''
print(_(f'{risotto_context.username}: START{context}:{paths_msg}: {msg}'))
await self.insert(msg, await self.insert(msg,
self._get_last_uri(risotto_context),
paths_msg,
risotto_context, risotto_context,
'Start', 'Start',
arguments, arguments,
@ -157,7 +161,7 @@ class Logger:
) -> None: ) -> None:
if get_config()['global']['debug']: if get_config()['global']['debug']:
paths_msg = self._get_message_paths(risotto_context) paths_msg = self._get_message_paths(risotto_context)
print(_(f'{risotto_context.username}: SUCCESS:{paths_msg}({risotto_context.context_id})')) print(_(f'{risotto_context.username}: SUCCESS({risotto_context.context_id}):{paths_msg}'))
sql = """UPDATE RisottoLog sql = """UPDATE RisottoLog
SET StopDate = $2, SET StopDate = $2,
Level = 'SUCCESS' Level = 'SUCCESS'
@ -169,10 +173,11 @@ class Logger:
args.append(dumps(returns)) args.append(dumps(returns))
sql += """WHERE LogId = $1 sql += """WHERE LogId = $1
""" """
await risotto_context.connection.execute(sql, async with risotto_context.log_connection.transaction():
risotto_context.start_id, await risotto_context.log_connection.execute(sql,
*args, risotto_context.start_id,
) *args,
)
async def failed(self, async def failed(self,
risotto_context: Context, risotto_context: Context,
@ -180,18 +185,23 @@ class Logger:
) -> None: ) -> None:
if get_config()['global']['debug']: if get_config()['global']['debug']:
paths_msg = self._get_message_paths(risotto_context) paths_msg = self._get_message_paths(risotto_context)
print(_(f'{risotto_context.username}: FAILED:{paths_msg}({risotto_context.context_id}): err')) if risotto_context.context_id != None:
context = f'({risotto_context.context_id})'
else:
context = ''
print(_(f'{risotto_context.username}: FAILED({risotto_context.context_id}):{paths_msg}: {err}'))
sql = """UPDATE RisottoLog sql = """UPDATE RisottoLog
SET StopDate = $2, SET StopDate = $2,
Level = 'FAILED', Level = 'FAILED',
Msg = $3 Msg = $3
WHERE LogId = $1 WHERE LogId = $1
""" """
await risotto_context.connection.execute(sql, async with risotto_context.log_connection.transaction():
risotto_context.start_id, await risotto_context.log_connection.execute(sql,
datetime.now(), risotto_context.start_id,
err, datetime.now(),
) err,
)
async def info(self, async def info(self,
risotto_context, risotto_context,
@ -200,8 +210,6 @@ class Logger:
if get_config()['global']['debug']: if get_config()['global']['debug']:
print(msg) print(msg)
await self.insert(msg, await self.insert(msg,
'',
None,
risotto_context, risotto_context,
'Info', 'Info',
) )

View File

@ -297,35 +297,37 @@ class RegisterDispatcher:
truncate: bool=False, truncate: bool=False,
) -> None: ) -> None:
internal_user = get_config()['global']['internal_user'] internal_user = get_config()['global']['internal_user']
async with self.pool.acquire() as connection: async with self.pool.acquire() as log_connection:
await connection.set_type_codec( async with self.pool.acquire() as connection:
'json', await connection.set_type_codec(
encoder=dumps, 'json',
decoder=loads, encoder=dumps,
schema='pg_catalog' decoder=loads,
) schema='pg_catalog'
if truncate: )
if truncate:
async with connection.transaction():
await connection.execute('TRUNCATE InfraServer, InfraSite, InfraZone, Log, ProviderDeployment, ProviderFactoryCluster, ProviderFactoryClusterNode, SettingApplicationservice, SettingApplicationServiceDependency, SettingRelease, SettingServer, SettingServermodel, SettingSource, UserRole, UserRoleURI, UserURI, UserUser, InfraServermodel, ProviderZone, ProviderServer, ProviderSource, ProviderApplicationservice, ProviderServermodel')
async with connection.transaction(): async with connection.transaction():
await connection.execute('TRUNCATE InfraServer, InfraSite, InfraZone, Log, ProviderDeployment, ProviderFactoryCluster, ProviderFactoryClusterNode, SettingApplicationservice, SettingApplicationServiceDependency, SettingRelease, SettingServer, SettingServermodel, SettingSource, UserRole, UserRoleURI, UserURI, UserUser, InfraServermodel, ProviderZone, ProviderServer, ProviderSource, ProviderApplicationservice, ProviderServermodel') for submodule_name, module in self.injected_self.items():
async with connection.transaction(): risotto_context = Context()
for submodule_name, module in self.injected_self.items(): risotto_context.username = internal_user
risotto_context = Context() risotto_context.paths.append(f'internal.{submodule_name}.on_join')
risotto_context.username = internal_user risotto_context.type = None
risotto_context.paths.append(f'internal.{submodule_name}.on_join') risotto_context.log_connection = log_connection
risotto_context.type = None risotto_context.connection = connection
risotto_context.connection = connection risotto_context.module = submodule_name.split('.', 1)[0]
risotto_context.module = submodule_name.split('.', 1)[0] info_msg = _(f'in function risotto_{submodule_name}.on_join')
info_msg = _(f'in function risotto_{submodule_name}.on_join') await log.info_msg(risotto_context,
await log.info_msg(risotto_context, None,
None, info_msg)
info_msg) try:
try: await module.on_join(risotto_context)
await module.on_join(risotto_context) except Exception as err:
except Exception as err: if get_config()['global']['debug']:
if get_config()['global']['debug']: print_exc()
print_exc() msg = _(f'on_join returns an error in module {submodule_name}: {err}')
msg = _(f'on_join returns an error in module {submodule_name}: {err}') await log.error_msg(risotto_context, {}, msg)
await log.error_msg(risotto_context, {}, msg)
async def load(self): async def load(self):
# valid function's arguments # valid function's arguments