This repository has been archived by the owner on Oct 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Manager class for applications using Task API
Stores application definitions and state (enabled/disabled). Uses EnvironmentManager to enforce that the appropriate environment is available and enabled before enabling an application. Definitions can be loaded from JSON files. Signed-off-by: Adam Wierzbicki <awierzbicki@golem.network>
- Loading branch information
Showing
4 changed files
with
199 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import json | ||
import logging | ||
from pathlib import Path | ||
from typing import Any, Dict, List, Iterator | ||
|
||
from dataclasses import dataclass, field | ||
from dataclasses_json import config, dataclass_json | ||
from marshmallow import fields as mm_fields | ||
|
||
from golem.task.envmanager import EnvironmentManager | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@dataclass_json | ||
@dataclass | ||
class AppDefinition: | ||
name: str | ||
requestor_env: str | ||
requestor_prereq: Dict[str, Any] = field(metadata=config( | ||
encoder=json.dumps, | ||
decoder=json.loads, | ||
mm_field=mm_fields.Dict(keys=mm_fields.Str()) | ||
)) | ||
max_benchmark_score: float | ||
version: str = '0.0' | ||
description: str = '' | ||
author: str = '' | ||
license: str = '' | ||
|
||
|
||
def load_app_from_json_file(json_file: Path) -> AppDefinition: | ||
""" Parse application definition from the given JSON file. Raise ValueError | ||
if the given file doesn't contain a valid definition. """ | ||
try: | ||
app_json = json_file.read_text(encoding='utf-8') | ||
# pylint: disable=no-member | ||
return AppDefinition.from_json(app_json) # type: ignore | ||
# pylint: enable=no-member | ||
except (OSError, ValueError, KeyError): | ||
msg = f"Error parsing app definition from file '{json_file}'." | ||
logger.exception(msg) | ||
raise ValueError(msg) | ||
|
||
|
||
def load_apps_from_dir(app_dir: Path) -> Iterator[AppDefinition]: | ||
""" Read every file in the given directory and attempt to parse it. Ignore | ||
files which don't contain valid app definitions. """ | ||
for json_file in app_dir.iterdir(): | ||
try: | ||
yield load_app_from_json_file(json_file) | ||
except ValueError: | ||
continue | ||
|
||
|
||
class AppManager: | ||
""" Manager class for applications using Task API. """ | ||
|
||
def __init__(self, env_manager: EnvironmentManager) -> None: | ||
self._env_manager = env_manager | ||
self._apps: Dict[str, AppDefinition] = {} | ||
self._state: Dict[str, bool] = {} | ||
|
||
def register_app(self, app: AppDefinition) -> None: | ||
""" Register an application in the manager. """ | ||
if app.name in self._apps: | ||
raise ValueError(f"Application '{app.name}' already registered.") | ||
self._apps[app.name] = app | ||
self._state[app.name] = False | ||
logger.info("Application '%s' registered.", app.name) | ||
|
||
def enabled(self, app_name: str) -> bool: | ||
""" Check if an application with the given name is registered in the | ||
manager and enabled. """ | ||
return app_name in self._state and self._state[app_name] | ||
|
||
def set_enabled(self, app_name: str, enabled: bool) -> None: | ||
""" Enable or disable an application. Raise an error if the application | ||
is not registered or the environment associated with the application | ||
is not available. """ | ||
if app_name not in self._apps: | ||
raise ValueError(f"Application '{app_name}' not registered.") | ||
env_id = self._apps[app_name].requestor_env | ||
if not self._env_manager.enabled(env_id): | ||
raise ValueError(f"Environment '{env_id}' not available.") | ||
self._state[app_name] = enabled | ||
logger.info( | ||
"Application '%s' %s.", | ||
app_name, | ||
'enabled' if enabled else 'disabled') | ||
|
||
def apps(self) -> List[AppDefinition]: | ||
""" Get all registered apps. """ | ||
return list(self._apps.values()) | ||
|
||
def app(self, app_name: str) -> AppDefinition: | ||
""" Get an app with given name (assuming it is registered). """ | ||
return self._apps[app_name] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
from unittest import TestCase | ||
from unittest.mock import Mock | ||
|
||
from golem.app_manager import ( | ||
AppDefinition, | ||
AppManager, | ||
load_app_from_json_file, | ||
load_apps_from_dir | ||
) | ||
from golem.task.envmanager import EnvironmentManager | ||
from golem.testutils import TempDirFixture | ||
|
||
APP_NAME = 'test_app' | ||
APP_DEF = AppDefinition( | ||
name=APP_NAME, | ||
requestor_env='test_env', | ||
requestor_prereq={ | ||
'key1': 'value', | ||
'key2': [1, 2, 3] | ||
}, | ||
max_benchmark_score=1.0 | ||
) | ||
|
||
|
||
class AppManagerTestBase(TestCase): | ||
|
||
def setUp(self): | ||
super().setUp() | ||
self.env_manager = Mock(spec_set=EnvironmentManager) | ||
self.app_manager = AppManager(self.env_manager) | ||
|
||
|
||
class TestRegisterApp(AppManagerTestBase): | ||
|
||
def test_register_app(self): | ||
self.app_manager.register_app(APP_DEF) | ||
self.assertEqual(self.app_manager.apps(), [APP_DEF]) | ||
self.assertEqual(self.app_manager.app(APP_NAME), APP_DEF) | ||
self.assertFalse(self.app_manager.enabled(APP_NAME)) | ||
|
||
def test_re_register(self): | ||
self.app_manager.register_app(APP_DEF) | ||
with self.assertRaises(ValueError): | ||
self.app_manager.register_app(APP_DEF) | ||
|
||
|
||
class TestSetEnabled(AppManagerTestBase): | ||
|
||
def test_app_not_registered(self): | ||
with self.assertRaises(ValueError): | ||
self.app_manager.set_enabled(APP_NAME, True) | ||
|
||
def test_env_not_enabled(self): | ||
self.app_manager.register_app(APP_DEF) | ||
self.env_manager.enabled.return_value = False | ||
with self.assertRaises(ValueError): | ||
self.app_manager.set_enabled(APP_NAME, True) | ||
|
||
def test_enable_disable(self): | ||
self.app_manager.register_app(APP_DEF) | ||
self.env_manager.enabled.return_value = True | ||
self.assertFalse(self.app_manager.enabled(APP_NAME)) | ||
self.app_manager.set_enabled(APP_NAME, True) | ||
self.assertTrue(self.app_manager.enabled(APP_NAME)) | ||
self.app_manager.set_enabled(APP_NAME, False) | ||
self.assertFalse(self.app_manager.enabled(APP_NAME)) | ||
|
||
|
||
class TestLoadAppFromJSONFile(TempDirFixture): | ||
|
||
def test_ok(self): | ||
json_file = self.new_path / 'test_app.json' | ||
json_file.write_text(APP_DEF.to_json(), encoding='utf-8') # noqa pylint: disable=no-member | ||
loaded_app = load_app_from_json_file(json_file) | ||
self.assertEqual(loaded_app, APP_DEF) | ||
|
||
def test_file_missing(self): | ||
json_file = self.new_path / 'test_app.json' | ||
with self.assertRaises(ValueError): | ||
load_app_from_json_file(json_file) | ||
|
||
def test_invalid_json(self): | ||
json_file = self.new_path / 'test_app.json' | ||
json_file.write_text('(╯°□°)╯︵ ┻━┻', encoding='utf-8') | ||
with self.assertRaises(ValueError): | ||
load_app_from_json_file(json_file) | ||
|
||
|
||
class TestLoadAppsFromDir(TempDirFixture): | ||
|
||
def test_register(self): | ||
app_file = self.new_path / 'test_app.json' | ||
bogus_file = self.new_path / 'bogus.json' | ||
app_file.write_text(APP_DEF.to_json(), encoding='utf-8') # noqa pylint: disable=no-member | ||
bogus_file.write_text('(╯°□°)╯︵ ┻━┻', encoding='utf-8') | ||
loaded_apps = list(load_apps_from_dir(self.new_path)) | ||
self.assertEqual(loaded_apps, [APP_DEF]) |