Skip to content

Commit

Permalink
Feature: Extensions - Making more than one Blueprint possible (crypto…
Browse files Browse the repository at this point in the history
…advance#1764)

* Making more than one Blueprint possible

* making many blueprints backwards compatible with one, reactivating all extension in the config

* bugfixes for hasty commit should fix testspyte

* fix tests and backwards-comp

* docs

* bugfix

* default blueprint anhd better error feedback

* documentation
  • Loading branch information
k9ert authored and ankur12-1610 committed Jul 12, 2022
1 parent 94424d3 commit 1a54219
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 49 deletions.
39 changes: 39 additions & 0 deletions docs/services/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,45 @@ If the extension has a UI (currently all of them have one), `has_blueprint` is T
`isolated_client` Should not be used yet. It is determining where in the url-path-tree the blueprint will be mounted. This might have an impact on whether the extension's frontend-client has access to the cookie used in specter. Check `config.py` for details.
`devstatus` is one of `devstatus_alpha`, `devstatus_beta` or `devstatus_prod` defined in `cryptoadvance.specter.services.service`. Each specter-instance will have a config-variable called `SERVICES_DEVSTATUS_THRESHOLD` (prod in Production and alpha in Development) and depending on that, the plugin will be available to the user.

## Frontend aspects

As stated, you can have your own frontend with a blueprint. If you only have one, it needs to have a `/` route in order to be linkable from the `choose your plugin` page.
If you create your extension with a blueprint, it'll create also a controller for you which, simplified, look like this:
```
rubberduck_endpoint = ScratchpadService.blueprint
def ext() -> ScratchpadService:
''' convenience for getting the extension-object'''
return app.specter.ext["rubberduck"]
def specter() -> Specter:
''' convenience for getting the specter-object'''
return app.specter
@rubberduck.route("/")
@login_required
@user_secret_decrypted_required
def index():
return render_template(
"rubberduck/index.jinja",
)
[...]
```
But you can also have more than one blueprint. Define them like this in your service-class:
```
blueprint_modules = {
"default" : "mynym.specterext.rubberduck.controller",
"ui" : "mynym.specterext.rubberduck.controller_ui"
}
```
You have to have a default-blueprint which has the above mentioned index-page.
In your controller, the endpoint needs to be specified like this:
```
ui = RubberduckService.blueprints["ui"]
```


## Data-Storage
Effort has been taken to provide `Service` data storage that is separate from existing data stores in order to keep those areas clean and simple. Where touchpoints are unavoidable, they are kept to the absolute bare minimum (e.g. `User.services` list, `Address.service_id` field).

Expand Down
131 changes: 83 additions & 48 deletions src/cryptoadvance/specter/managers/service_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,69 +102,104 @@ def __init__(self, specter, devstatus_threshold):
def register_blueprint_for_ext(cls, clazz, ext):
if not clazz.has_blueprint:
return
if hasattr(clazz, "blueprint_module"):
import_name = clazz.blueprint_module
controller_module = clazz.blueprint_module
if hasattr(clazz, "blueprint_modules"):
controller_modules = clazz.blueprint_modules
setattr(clazz, "blueprints", {})
elif hasattr(clazz, "blueprint_module"):
controller_modules = {"default": clazz.blueprint_module}
else:
# The import_name helps to locate the root_path for the blueprint
import_name = f"cryptoadvance.specter.services.{clazz.id}.service"
controller_module = f"cryptoadvance.specter.services.{clazz.id}.controller"
controller_modules = controller_modules = {
"default": f"cryptoadvance.specter.services.{clazz.id}.controller"
}

clazz.blueprint = Blueprint(
f"{clazz.id}_endpoint",
import_name,
template_folder=get_template_static_folder("templates"),
static_folder=get_template_static_folder("static"),
)
only_one_blueprint = len(controller_modules.items()) == 1

def inject_stuff():
"""Can be used in all jinja2 templates"""
return dict(specter=app.specter, service=ext)

clazz.blueprint.context_processor(inject_stuff)
if "default" not in controller_modules.keys():
raise SpecterError(
"You need at least one Blueprint, with the key 'default'. It will be used to link to your UI"
)

# Import the controller for this service
logger.info(f" Loading Controller {controller_module}")
controller_module = import_module(controller_module)
for bp_key, bp_value in controller_modules.items():
if bp_key == "":
raise SpecterError("Empty keys are not allowed in the blueprints map")
middple_part = "" if bp_key == "default" else f"{bp_key}_"
bp_name = f"{clazz.id}_{middple_part}endpoint"
logger.debug(
f" Creating blueprint with name {bp_name} (middle_part:{middple_part}:"
)
bp = Blueprint(
f"{clazz.id}_{middple_part}endpoint",
bp_value,
template_folder=get_template_static_folder("templates"),
static_folder=get_template_static_folder("static"),
)
if only_one_blueprint:
setattr(clazz, "blueprint", bp)
else:
clazz.blueprints[bp_key] = bp
bp.context_processor(inject_stuff)

# finally register the blueprint
if clazz.isolated_client:
ext_prefix = app.config["ISOLATED_CLIENT_EXT_URL_PREFIX"]
else:
ext_prefix = app.config["EXT_URL_PREFIX"]
# Import the controller for this service
logger.info(f" Loading Controller {bp_value}")

try:
if (
app.testing
and len([vf for vf in app.view_functions if vf.startswith(clazz.id)])
<= 1
): # the swan-static one
# Yet again that nasty workaround which has been described in the archblog.
# The easy variant can be found in server.py
# The good news is, that we'll only do that for testing
import importlib

logger.info("Reloading Extension controller")
importlib.reload(controller_module)
app.register_blueprint(
clazz.blueprint, url_prefix=f"{ext_prefix}/{clazz.id}"
)
else:
app.register_blueprint(
clazz.blueprint, url_prefix=f"{ext_prefix}/{clazz.id}"
)
logger.info(f" Mounted {clazz.id} to {ext_prefix}/{clazz.id}")
except AssertionError as e:
if str(e).startswith("A name collision"):
raise SpecterError(
try:
controller_module = import_module(bp_value)
except ModuleNotFoundError as e:
raise Exception(
f"""
There is a name collision for the {clazz.blueprint.name}. \n
This is probably because you're running in DevelopementConfig and configured
the extension at the same time in the EXTENSION_LIST which currently loks like this:
{app.config['EXTENSION_LIST']})
There was an issue finding a controller module:
{e}
That module was specified in the Service class of service {clazz.id}
check that specification in {clazz.__module__}
"""
)

# finally register the blueprint
if clazz.isolated_client:
ext_prefix = app.config["ISOLATED_CLIENT_EXT_URL_PREFIX"]
else:
ext_prefix = app.config["EXT_URL_PREFIX"]

try:
bp_postfix = "" if only_one_blueprint else f"/{bp_key}"
if (
app.testing
and len(
[vf for vf in app.view_functions if vf.startswith(clazz.id)]
)
<= 1
): # the swan-static one
# Yet again that nasty workaround which has been described in the archblog.
# The easy variant can be found in server.py
# The good news is, that we'll only do that for testing
import importlib

logger.info("Reloading Extension controller")
importlib.reload(controller_module)
app.register_blueprint(
bp, url_prefix=f"{ext_prefix}/{clazz.id}{bp_postfix}"
)
else:
app.register_blueprint(
bp, url_prefix=f"{ext_prefix}/{clazz.id}{bp_postfix}"
)
logger.info(f" Mounted {bp} to {ext_prefix}/{clazz.id}{bp_postfix}")
except AssertionError as e:
if str(e).startswith("A name collision"):
raise SpecterError(
f"""
There is a name collision for the {clazz.blueprint.name}. \n
This is probably because you're running in DevelopementConfig and configured
the extension at the same time in the EXTENSION_LIST which currently loks like this:
{app.config['EXTENSION_LIST']})
"""
)

@classmethod
def configure_service_for_module(cls, clazz):
"""searches for ConfigClasses in the module-Directory and merges its config in the global config"""
Expand Down
4 changes: 4 additions & 0 deletions src/cryptoadvance/specter/util/reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ def get_subclasses_for_clazz(clazz, package_dirs: List[str] = None):
* {orgname}.specterext.{module_name}.service
Maybe you did forget to do this:
$ pip3 install -e .
OR The Module has been found, but had issues finding Modules itself
{e}
"""
)
else:
Expand Down
Loading

0 comments on commit 1a54219

Please sign in to comment.