Skip to content

Commit

Permalink
Refactor ServiceManager To ExtensionManager
Browse files Browse the repository at this point in the history
  • Loading branch information
k9ert committed Oct 26, 2022
1 parent e025339 commit 352cb8c
Show file tree
Hide file tree
Showing 12 changed files with 498 additions and 50 deletions.
8 changes: 4 additions & 4 deletions pyinstaller/hooks/hook-cryptoadvance.specter.services.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import logging
from cryptoadvance.specter.managers.service_manager import ServiceManager
from cryptoadvance.specter.managers.service_manager import ExtensionManager

logger = logging.getLogger(__name__)

# Collecting template and static files from the different services in src/cryptoadvance/specter/services
service_template_datas = [
(service_dir, "templates")
for service_dir in ServiceManager.get_service_x_dirs("templates")
for service_dir in ExtensionManager.get_service_x_dirs("templates")
]
service_static_datas = [
(service_dir, "static")
for service_dir in ServiceManager.get_service_x_dirs("static")
for service_dir in ExtensionManager.get_service_x_dirs("static")
]

# Collect Packages from the services, including service- and controller-classes
service_packages = ServiceManager.get_service_packages()
service_packages = ExtensionManager.get_service_packages()


datas = [*service_template_datas, *service_static_datas]
Expand Down
437 changes: 437 additions & 0 deletions src/cryptoadvance/specter/managers/service_manager.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .service_manager import ExtensionManager
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@
from flask import url_for
from flask.blueprints import Blueprint

from ..services.service import Service
from ..services import callbacks, ExtensionException
from ..util.reflection import (
from ...services.service import Service
from ...services import callbacks, ExtensionException
from ...util.reflection import (
_get_module_from_class,
get_classlist_of_type_clazz_from_modulelist,
get_package_dir_for_subclasses_of,
get_subclasses_for_clazz,
get_subclasses_for_clazz_in_cwd,
)
from ..util.reflection_fs import search_dirs_in_path
from ...util.reflection_fs import search_dirs_in_path

logger = logging.getLogger(__name__)


class ServiceManager:
class ExtensionManager:
"""Loads support for all Services it auto-discovers."""

def __init__(self, specter, devstatus_threshold):
Expand All @@ -44,7 +44,7 @@ def __init__(self, specter, devstatus_threshold):
logger.info("----> starting service discovery Static")
# How do we discover services? Two configs are relevant:
# * SERVICES_LOAD_FROM_CWD (boolean, CWD is current working directory)
# * EXTENSION_LIST (array of Fully Qualified module strings like ["cryptoadvance.specter.services.swan.service"])
# * EXTENSION_LIST (array of Fully Qualified module strings like ["cryptoadvance.specterext.swan.service"])
# Ensuring security (especially for the CWD) is NOT done here but
# in the corresponding (Production)Config
logger.debug(f"EXTENSION_LIST = {app.config.get('EXTENSION_LIST')}")
Expand Down Expand Up @@ -80,7 +80,7 @@ def __init__(self, specter, devstatus_threshold):
f"Service {clazz.__name__} not activated due to devstatus ( {self.devstatus_threshold} > {clazz.devstatus} )"
)
logger.info("----> finished service processing")
self.execute_ext_callbacks("afterServiceManagerInit")
self.execute_ext_callbacks("afterExtensionManagerInit")

@classmethod
def register_blueprint_for_ext(cls, clazz, ext):
Expand Down Expand Up @@ -359,7 +359,6 @@ def get_service_x_dirs(cls, x):
logger.debug(f"Initial arr:")
for element in arr:
logger.debug(element)
# /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/bitcoinreserve/templates
# /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/swan/templates

arr = [path for path in arr if path.is_dir()]
Expand Down
6 changes: 3 additions & 3 deletions src/cryptoadvance/specter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from cryptoadvance.specter.hwi_rpc import HWIBridge

from cryptoadvance.specter.liquid.rpc import LiquidRPC
from cryptoadvance.specter.managers.service_manager import ServiceManager
from cryptoadvance.specter.managers.service_manager import ExtensionManager
from cryptoadvance.specter.rpc import BitcoinRPC
from cryptoadvance.specter.services import callbacks
from cryptoadvance.specter.util.reflection import get_template_static_folder
Expand Down Expand Up @@ -147,8 +147,8 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
# HWI
specter.hwi = HWIBridge()

# ServiceManager will instantiate and register blueprints for extensions
specter.service_manager = ServiceManager(
# ExtensionManager will instantiate and register blueprints for extensions
specter.service_manager = ExtensionManager(
specter=specter, devstatus_threshold=app.config["SERVICES_DEVSTATUS_THRESHOLD"]
)

Expand Down
10 changes: 8 additions & 2 deletions src/cryptoadvance/specter/services/callbacks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
""" Here we have some constants getting an id for extension-points/callbacks. As camelcase
is used, we don't use CAPITAL letters to not loose the meaning of the camelcase.
These constants are expected as parameter to the ServiceManager.callback function
These constants are expected as parameter to the ExtensionManager.callback function
and it'll throw an exception if the constant does not exist.
There are some weak naming conventions:
Expand All @@ -14,7 +14,7 @@
"""
I don't know why we have this one. Doesn't seem to be used anywhere.
"""
afterServiceManagerInit = "afterServiceManagerInit"
afterExtensionManagerInit = "afterExtensionManagerInit"

"""
This one is called, after the init_app method has finished. The "run" method has not
Expand All @@ -35,6 +35,12 @@
"""
add_wallettabs = "add_wallettabs"

""" Endpoints might define their behaviour via a ViewModel. Those Models are passed here and
extensions can modify that behaviour via Modifying that model. Currently there is only:
cryptoadvance.specter.server_enpoints.welcome.welcome_vm.WelcomeVm
"""
adjust_view_model = "adjust_view_model"

"""
This one is called, whenever a file is persisted. To call external scripts in another
process, you better use the SPECTER_PERSISTENCE_CALLBACK Env Var or it's asynchronous cousin
Expand Down
2 changes: 1 addition & 1 deletion src/cryptoadvance/specter/services/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# All blueprint from Services are no longer loaded statically but dynamically when the service-class in initialized
# check cryptoadvance.specter.services.service_manager.Service for doing that and
# check cryptoadvance.specter.services/**/manifest for instances of Service-classes and
# check cryptoadvance.specter.services.service_manager.ServiceManager.services for initialisation of ServiceClasses
# check cryptoadvance.specter.services.service_manager.ExtensionManager.services for initialisation of ServiceClasses


def user_secret_decrypted_required(func):
Expand Down
28 changes: 20 additions & 8 deletions src/cryptoadvance/specter/specter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
RpcError,
get_default_datadir,
)
from .managers.service_manager import ServiceManager
from .managers.service_manager import ExtensionManager
from .services.service import devstatus_alpha, devstatus_beta, devstatus_prod
from .specter_error import ExtProcTimeoutException, SpecterError
from .tor_daemon import TorDaemonController
Expand All @@ -68,7 +68,9 @@ def __init__(
config={},
internal_bitcoind_version="",
checker_threads=True,
initialize=True,
):
"""Very basic Initialisation of the Specter Object. Make sure to call specter.initialize() shortly after"""
if data_folder.startswith("~"):
data_folder = os.path.expanduser(data_folder)
data_folder = os.path.abspath(data_folder)
Expand All @@ -78,30 +80,40 @@ def __init__(
os.makedirs(data_folder)

self.data_folder = data_folder

self._config = config
self._internal_bitcoind_version = internal_bitcoind_version
self._checker_threads = checker_threads
if initialize:
self.initialize()

def initialize(self):
"""Runs the checker_treads, instantiates all the managers and attach them to its attributes"""
self.user_manager = UserManager(
self
) # has to come before calling VersionChecker()

# version checker
# checks for new versions once per hour
logger.info("Instantiate VersionChecker")
self.version = VersionChecker(specter=self)
if checker_threads:
if self._checker_threads:
self.version.start()

self._config_manager = ConfigManager(self.data_folder, config)
logger.info("Instantiate ConfigManager")
self._config_manager = ConfigManager(self.data_folder, self._config)

self.internal_bitcoind_version = internal_bitcoind_version
self.internal_bitcoind_version = self._internal_bitcoind_version

# Migrating from Specter 1.3.1 and lower (prior to the node manager)
self.migrate_old_node_format()

logger.info("Instantiate NodeManager")
self.node_manager = NodeManager(
proxy_url=self.proxy_url,
only_tor=self.only_tor,
active_node=self.active_node_alias,
bitcoind_path=self.bitcoind_path,
internal_bitcoind_version=internal_bitcoind_version,
internal_bitcoind_version=self._internal_bitcoind_version,
data_folder=os.path.join(self.data_folder, "nodes"),
)

Expand Down Expand Up @@ -136,12 +148,12 @@ def __init__(

self.update_tor_controller()
self.checker = Checker(lambda: self.check(check_all=True), desc="health")
if checker_threads:
if self._checker_threads:
self.checker.start()
self.price_checker = Checker(
lambda: update_price(self, self.user), desc="price"
)
if self.price_check and self.price_provider and checker_threads:
if self.price_check and self.price_provider and self._checker_threads:
self.price_checker.start()

# Configuring the two different storages (Universal json-files)
Expand Down
4 changes: 2 additions & 2 deletions src/cryptoadvance/specterext/swan/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from flask_babel import lazy_gettext as _
from flask_login import current_user, login_required

from ...server_endpoints import flash
from ..controller import user_secret_decrypted_required
from cryptoadvance.specter.server_endpoints import flash
from cryptoadvance.specter.services.controller import user_secret_decrypted_required
from . import client as swan_client
from .client import SwanApiException
from .service import SwanService
Expand Down
31 changes: 12 additions & 19 deletions tests/test_managers_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,30 @@
import os
from unittest.mock import MagicMock
from flask import Flask
from cryptoadvance.specter.managers.service_manager import ServiceManager
from cryptoadvance.specter.managers.service_manager import ExtensionManager
from cryptoadvance.specter.services.callbacks import after_serverpy_init_app


def test_ServiceManager2(mock_specter, mock_flaskapp, caplog):
def test_ExtensionManager2(mock_specter, mock_flaskapp, caplog):
ctx = mock_flaskapp.app_context()
ctx.push()
sm = ServiceManager(mock_specter, "alpha")
sm = ExtensionManager(mock_specter, "alpha")
# We have passed the TestConfig which is (hopefully) not existing in the Swan Extension
# So the ServiceManager will move up the dependency tree of TestConfig until it finds
# So the ExtensionManager will move up the dependency tree of TestConfig until it finds
# a Config and will copy the keys into the flask-config
assert mock_flaskapp.config["SWAN_API_URL"] == "https://api.dev.swanbitcoin.com"
assert sm.services["bitcoinreserve"] != None
assert sm.services["swan"] != None

sm.execute_ext_callbacks(after_serverpy_init_app, scheduler=None)


@pytest.mark.skip(reason="The .buildenv directoy does not exist on the CI-Infra")
def test_ServiceManager_get_service_x_dirs(caplog):
def test_ExtensionManager_get_service_x_dirs(caplog):
caplog.set_level(logging.DEBUG)
try:
os.chdir("./pyinstaller")
# THis is usefull in the pytinstaller/specterd.spec
dirs = ServiceManager.get_service_x_dirs("templates")
dirs = ExtensionManager.get_service_x_dirs("templates")
# As the tests are executed in a development-environment (pip3 install -e .), the results we get back here
# are not the same than the one we would get back when really are building. Because in that case,
# the .buildenv would the environment and no symlinks would link to src/cryptoadavance...
Expand All @@ -43,24 +42,19 @@ def test_ServiceManager_get_service_x_dirs(caplog):
for path in dirs:
assert str(path).endswith("templates")

dirs = ServiceManager.get_service_x_dirs("static")
dirs = ExtensionManager.get_service_x_dirs("static")
assert f"{expected_folder}/cryptoadvance/specter/services/swan/static" in [
str(dir) for dir in dirs
]
assert (
f"{expected_folder}/cryptoadvance/specter/services/bitcoinreserve/static"
in [str(dir) for dir in dirs]
)
print(dirs)
assert len(dirs) >= 2
finally:
os.chdir("../")


def test_ServiceManager_get_service_packages():
packages = ServiceManager.get_service_packages()
assert "cryptoadvance.specter.services.swan.service" in packages
assert "cryptoadvance.specter.services.bitcoinreserve.service" in packages
def test_ExtensionManager_get_service_packages():
packages = ExtensionManager.get_service_packages()
assert "cryptoadvance.specterext.swan.service" in packages


@pytest.fixture
Expand All @@ -75,12 +69,11 @@ def mock_flaskapp(mock_specter):

flaskapp_mock = Flask(__name__)
flaskapp_mock.config["EXTENSION_LIST"] = [
"cryptoadvance.specter.services.swan.service",
"cryptoadvance.specter.services.bitcoinreserve.service",
"cryptoadvance.specterext.swan.service",
]
flaskapp_mock.config["ISOLATED_CLIENT_EXT_URL_PREFIX"] = "/spc/ext"
flaskapp_mock.config["EXT_URL_PREFIX"] = "/ext"
# The ServiceManager is a flask-aware component. It will load all the services
# The ExtensionManager is a flask-aware component. It will load all the services
# however, in order to configure them, he needs to know about the configuration
# of the specterApp.
flaskapp_mock.config[
Expand Down
6 changes: 3 additions & 3 deletions tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
ServiceEncryptedStorageError,
ServiceEncryptedStorageManager,
)
from cryptoadvance.specter.managers.service_manager import ServiceManager
from cryptoadvance.specter.managers.service_manager import ExtensionManager
from cryptoadvance.specter.user import User, hash_password


Expand All @@ -31,13 +31,13 @@ class FakeService(Service):


# @patch("cryptoadvance.specter.services.service_manager.app")
# def test_ServiceManager_loads_services(empty_data_folder, app):
# def test_ExtensionManager_loads_services(empty_data_folder, app):
# # app.config = MagicMock()
# # app.config.get.return_value = "prod"
# specter_mock = MagicMock()
# specter_mock.data_folder.return_value = empty_data_folder

# service_manager = ServiceManager(specter=specter_mock, devstatus_threshold="alpha")
# service_manager = ExtensionManager(specter=specter_mock, devstatus_threshold="alpha")
# services = service_manager.services
# assert "swan" in services

Expand Down

0 comments on commit 352cb8c

Please sign in to comment.