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
194 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,99 @@ | ||
import json | ||
import logging | ||
from pathlib import Path | ||
from typing import Any, Dict, List | ||
|
||
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 = '' | ||
|
||
|
||
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 not in self._apps: | ||
self._apps[app.name] = app | ||
self._state[app.name] = False | ||
logger.info("Application '%s' registered.", app.name) | ||
|
||
def register_app_from_json_file(self, json_file: Path) -> None: | ||
""" Parse application definition from the given JSON file and register | ||
the parsed definition in the manager. 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 | ||
app = AppDefinition.from_json(app_json) # type: ignore | ||
# pylint: enable=no-member | ||
self.register_app(app) | ||
except (OSError, ValueError, KeyError): | ||
msg = f"Error parsing app definition from file '{json_file}'." | ||
logger.exception(msg) | ||
raise ValueError(msg) | ||
|
||
def register_apps_from_dir(self, app_dir: Path) -> None: | ||
""" Read every file in the given directory and attempt to parse it. If | ||
the file contains a valid JSON representation of an app definition | ||
register the parsed definition in the manager. Ignore files which | ||
don't contain valid app definitions. """ | ||
for json_file in app_dir.iterdir(): | ||
try: | ||
self.register_app_from_json_file(json_file) | ||
except ValueError: | ||
continue | ||
|
||
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,91 @@ | ||
from unittest.mock import Mock | ||
|
||
from golem.app_manager import AppDefinition, AppManager | ||
from golem.task.envmanager import EnvironmentManager | ||
from golem.testutils import TempDirFixture | ||
|
||
|
||
class AppManagerTestBase(TempDirFixture): | ||
|
||
def setUp(self): | ||
super().setUp() | ||
self.env_manager = Mock(spec_set=EnvironmentManager) | ||
self.app_manager = AppManager(self.env_manager) | ||
|
||
@property | ||
def app_name(self): | ||
return 'test_app' | ||
|
||
@property | ||
def app_def(self): | ||
return AppDefinition( | ||
name=self.app_name, | ||
requestor_env='test_env', | ||
requestor_prereq={ | ||
'key1': 'value', | ||
'key2': [1, 2, 3] | ||
}, | ||
max_benchmark_score=1.0 | ||
) | ||
|
||
|
||
class TestRegisterApp(AppManagerTestBase): | ||
|
||
def test_register_app(self): | ||
self.app_manager.register_app(self.app_def) | ||
self.assertEqual(self.app_manager.apps(), [self.app_def]) | ||
self.assertEqual(self.app_manager.app(self.app_name), self.app_def) | ||
self.assertFalse(self.app_manager.enabled(self.app_name)) | ||
|
||
def test_re_register(self): | ||
self.app_manager.register_app(self.app_def) | ||
self.app_manager.register_app(self.app_def) | ||
self.assertEqual(self.app_manager.apps(), [self.app_def]) | ||
self.assertEqual(self.app_manager.app(self.app_name), self.app_def) | ||
self.assertFalse(self.app_manager.enabled(self.app_name)) | ||
|
||
|
||
class TestRegisterAppFromJSONFile(AppManagerTestBase): | ||
|
||
def test_ok(self): | ||
json_file = self.new_path / 'test_app.json' | ||
json_file.write_text(self.app_def.to_json(), encoding='utf-8') # noqa pylint: disable=no-member | ||
self.app_manager.register_app_from_json_file(json_file) | ||
self.assertEqual(self.app_manager.apps(), [self.app_def]) | ||
self.assertEqual(self.app_manager.app(self.app_name), self.app_def) | ||
self.assertFalse(self.app_manager.enabled(self.app_name)) | ||
|
||
def test_file_missing(self): | ||
json_file = self.new_path / 'test_app.json' | ||
with self.assertRaises(ValueError): | ||
self.app_manager.register_app_from_json_file(json_file) | ||
self.assertEqual(self.app_manager.apps(), []) | ||
|
||
def test_invalid_json(self): | ||
json_file = self.new_path / 'test_app.json' | ||
json_file.write_text('(╯°□°)╯︵ ┻━┻', encoding='utf-8') | ||
with self.assertRaises(ValueError): | ||
self.app_manager.register_app_from_json_file(json_file) | ||
self.assertEqual(self.app_manager.apps(), []) | ||
|
||
|
||
class TestRegisterAppsFromDir(AppManagerTestBase): | ||
|
||
def test_register(self): | ||
app_file = self.new_path / 'test_app.json' | ||
bogus_file = self.new_path / 'bogus.json' | ||
app_file.write_text(self.app_def.to_json(), encoding='utf-8') # noqa pylint: disable=no-member | ||
bogus_file.write_text('(╯°□°)╯︵ ┻━┻', encoding='utf-8') | ||
self.app_manager.register_apps_from_dir(self.new_path) | ||
self.assertEqual(self.app_manager.apps(), [self.app_def]) | ||
|
||
|
||
class TestSetEnabled(AppManagerTestBase): | ||
|
||
def test_enable_disable(self): | ||
self.app_manager.register_app(self.app_def) | ||
self.assertFalse(self.app_manager.enabled(self.app_name)) | ||
self.app_manager.set_enabled(self.app_name, True) | ||
self.assertTrue(self.app_manager.enabled(self.app_name)) | ||
self.app_manager.set_enabled(self.app_name, False) | ||
self.assertFalse(self.app_manager.enabled(self.app_name)) |