Skip to content

Commit d25496a

Browse files
committed
some fixes
1 parent d4320b1 commit d25496a

File tree

6 files changed

+156
-58
lines changed

6 files changed

+156
-58
lines changed

cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ build-backend = "poetry.core.masonry.api"
3131
[tool.poetry.plugins."openbb_obbject_extension"]
3232
to_string = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:ext"
3333
{{ cookiecutter.obbject_name }} = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:class_ext"
34+
nonblocking_plugin = "{{ cookiecutter.package_name }}.obbject.{{ cookiecutter.obbject_name }}:nonblocking_plugin"

cookiecutter/openbb_cookiecutter/template/{{cookiecutter.project_tag}}/{{cookiecutter.package_name}}/obbject/{{cookiecutter.obbject_name}}/__init__.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
# pylint: disable=W0613,R0903
44

5-
import json
5+
import threading
6+
import time
67

78
from openbb_core.app.model.extension import Extension
89
from openbb_core.app.model.obbject import OBBject
@@ -40,3 +41,43 @@ def __init__(self, obbject: OBBject):
4041
def hello_world(self, **kwargs):
4142
"""Say hello from the OBBject extension."""
4243
print(f"Hello from the OBBject instance! \n\n{repr(self._obbject)}") # noqa
44+
45+
46+
nonblocking_plugin = Extension(
47+
name="nonblocking_plugin",
48+
description="An on-command-output plugin simulating an extensive task performed in a separate thread.",
49+
on_command_output=True, # Must be set as True
50+
command_output_paths=["/{{cookiecutter.router_name}}/candles"],
51+
immutable=True, # Set to `True` for parallel processing.
52+
results_only=False, # Use this as a flag to return only the "results" portion of the OBBject.
53+
)
54+
55+
56+
def _expensive_operation_worker(serialized_obbject: dict):
57+
"""Simulate a long-running task without blocking the caller."""
58+
working_copy = OBBject(**serialized_obbject)
59+
print("\nThis is the deserialized OBBject in the non-blocking thread.")
60+
print(working_copy.__repr__())
61+
for i in range(10):
62+
print(str(i) + " seconds remaining...")
63+
time.sleep(1)
64+
print("Expensive operation is now complete.")
65+
66+
67+
@nonblocking_plugin.obbject_accessor
68+
def empty_plugin_function(obbject): # This can also be an async function.
69+
"""Simulated on_commnd_output function that executes an expensive task
70+
in a non-blocking thread."""
71+
print(
72+
"Serializing the obbject and passing to a new thread.\n"
73+
f"Command executed: {obbject.extra['metadata']}\n"
74+
)
75+
print(
76+
"Simulating an expensive task that is non-blocking and allows the function to return."
77+
)
78+
threading.Thread(
79+
target=_expensive_operation_worker,
80+
args=(obbject.model_dump(),),
81+
name="empty-plugin-expensive-operation",
82+
daemon=False,
83+
).start()

openbb_platform/core/openbb_core/app/command_runner.py

Lines changed: 94 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Command runner module."""
22

33
# pylint: disable=R0903
4-
54
from collections.abc import Callable
65
from copy import deepcopy
76
from dataclasses import asdict, is_dataclass
@@ -15,6 +14,7 @@
1514
from openbb_core.app.extension_loader import ExtensionLoader
1615
from openbb_core.app.model.abstract.error import OpenBBError
1716
from openbb_core.app.model.abstract.warning import OpenBBWarning, cast_warning
17+
from openbb_core.app.model.extension import CachedAccessor
1818
from openbb_core.app.model.metadata import Metadata
1919
from openbb_core.app.model.obbject import OBBject
2020
from openbb_core.app.provider_interface import ExtraParams
@@ -448,13 +448,13 @@ async def run(
448448
raise OpenBBError(e) from e
449449
warn(str(e), OpenBBWarning)
450450

451-
try:
452-
cls._trigger_command_output_callbacks(route, obbject)
453-
454-
except Exception as e:
455-
if Env().DEBUG_MODE:
456-
raise OpenBBError(e) from e
457-
warn(str(e), OpenBBWarning)
451+
if isinstance(obbject, OBBject):
452+
try:
453+
cls._trigger_command_output_callbacks(route, obbject)
454+
except Exception as e:
455+
if Env().DEBUG_MODE:
456+
raise OpenBBError(e) from e
457+
warn(str(e), OpenBBWarning)
458458

459459
return obbject
460460

@@ -463,7 +463,8 @@ def _trigger_command_output_callbacks(cls, route: str, obbject: OBBject) -> None
463463
"""Trigger command output callbacks for extensions."""
464464
loader = ExtensionLoader()
465465
callbacks = loader.on_command_output_callbacks
466-
results_only = False
466+
if not callbacks:
467+
return
467468

468469
# For each extension registered for all routes or the specific route,
469470
# we call its accessor on the OBBject.
@@ -473,53 +474,93 @@ def _trigger_command_output_callbacks(cls, route: str, obbject: OBBject) -> None
473474
# mutates the OBBject so we can pass this information to the interface.
474475
# We also set the _results_only attribute to True if any extension
475476
# indicates that only results should be returned.
476-
if "*" in callbacks:
477-
for ext in callbacks["*"]:
478-
if ext.results_only is True:
479-
results_only = True
480-
if ext.immutable is True:
481-
if hasattr(obbject, ext.name):
482-
obbject_copy = deepcopy(obbject)
483-
accessor = getattr(obbject_copy, ext.name)
484-
if iscoroutinefunction(accessor):
485-
run_async(accessor)
486-
elif callable(accessor):
487-
accessor()
488-
elif ext.immutable is False:
489-
if ext.results_only is True:
490-
results_only = True
491-
if hasattr(obbject, ext.name):
492-
accessor = getattr(obbject, ext.name)
493-
if iscoroutinefunction(accessor):
494-
run_async(accessor)
495-
elif callable(accessor):
496-
accessor()
497-
setattr(obbject, "_extension_modified", True)
498-
499-
if route in callbacks:
500-
for ext in callbacks[route]:
477+
results_only = False
478+
executed_keys: set[str] = set()
479+
ordered_extensions: list = []
480+
all_on_command_output_exts: list = []
481+
482+
def _extension_key(ext) -> str:
483+
if key := getattr(ext, "identifier", None):
484+
return str(key)
485+
if path := getattr(ext, "import_path", None):
486+
return f"{path}:{getattr(ext, 'name', id(ext))}"
487+
return str(getattr(ext, "name", id(ext)))
488+
489+
def _clone_for_immutable(source: OBBject) -> OBBject | None:
490+
try:
491+
new_source = source.model_copy()
492+
new_source = OBBject.model_validate(source.model_dump())
493+
return source.model_validate(new_source)
494+
except Exception as e:
495+
warn(
496+
"Skipped immutable callback because the OBBject "
497+
f"could not be duplicated. {e}",
498+
OpenBBWarning,
499+
)
500+
return None
501+
502+
for ext_list in callbacks.values():
503+
all_on_command_output_exts.extend(ext_list)
504+
505+
for ext in callbacks.get("*", []):
506+
key = _extension_key(ext)
507+
if key not in executed_keys:
508+
executed_keys.add(key)
509+
ordered_extensions.append(ext)
510+
511+
for ext in callbacks.get(route, []):
512+
key = _extension_key(ext)
513+
if key not in executed_keys:
514+
executed_keys.add(key)
515+
ordered_extensions.append(ext)
516+
517+
try:
518+
for ext in ordered_extensions:
501519
if ext.results_only is True:
502520
results_only = True
503521

504-
if ext.immutable is True:
505-
if hasattr(obbject, ext.name):
506-
obbject_copy = deepcopy(obbject)
507-
accessor = getattr(obbject_copy, ext.name)
508-
if iscoroutinefunction(accessor):
509-
run_async(accessor)
510-
elif callable(accessor):
511-
accessor()
512-
elif ext.immutable is False and hasattr(obbject, ext.name):
513-
accessor = getattr(obbject, ext.name)
514-
if iscoroutinefunction(accessor):
515-
run_async(accessor)
516-
elif callable(accessor):
517-
accessor()
518-
setattr(obbject, "_extension_modified", True)
519-
520-
if results_only is True:
521-
setattr(obbject, "_results_only", True)
522-
setattr(obbject, "_extension_modified", True)
522+
if ext.command_output_paths and route not in ext.command_output_paths:
523+
continue
524+
525+
accessors = getattr(type(obbject), "accessors", set())
526+
if ext.name not in accessors:
527+
continue
528+
529+
descriptor = type(obbject).__dict__.get(ext.name)
530+
if not isinstance(descriptor, CachedAccessor):
531+
continue
532+
533+
factory = descriptor._accessor # type: ignore[attr-defined]
534+
535+
target = _clone_for_immutable(obbject) if ext.immutable else obbject
536+
537+
if target is None:
538+
continue
539+
540+
if iscoroutinefunction(factory):
541+
run_async(factory, target)
542+
else:
543+
result = factory(target)
544+
if callable(result):
545+
result()
546+
547+
if ext.immutable is False:
548+
object.__setattr__(obbject, "_extension_modified", True)
549+
550+
if results_only is True:
551+
object.__setattr__(obbject, "_results_only", True)
552+
object.__setattr__(obbject, "_extension_modified", True)
553+
554+
except Exception as e:
555+
raise OpenBBError(e) from e
556+
557+
for ext in all_on_command_output_exts:
558+
if ext.name in type(obbject).__dict__:
559+
object.__setattr__(
560+
obbject,
561+
ext.name,
562+
"Accessor is not callable outside of function execution.",
563+
)
523564

524565

525566
class CommandRunner:

openbb_platform/core/openbb_core/app/model/credentials.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def load(self) -> BaseModel:
164164
**self.format_credentials(additional), # type: ignore
165165
)
166166
model._env_defaults = env_overrides # type: ignore # pylint: disable=W0212
167+
model.origins = self.credentials
167168

168169
return model
169170

openbb_platform/core/tests/app/test_command_runner.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from openbb_core.app.model.abstract.warning import OpenBBWarning
1818
from openbb_core.app.model.command_context import CommandContext
19-
from openbb_core.app.model.extension import Extension
19+
from openbb_core.app.model.extension import CachedAccessor, Extension
2020
from openbb_core.app.model.obbject import OBBject
2121
from openbb_core.app.model.system_settings import SystemSettings
2222
from openbb_core.app.model.user_settings import UserSettings
@@ -495,7 +495,13 @@ def mut_accessor(self):
495495
if isinstance(getattr(self, "results", None), list):
496496
self.results.append("modified_by_mut")
497497

498-
monkeypatch.setattr(OBBject, ext.name, mut_accessor, raising=False)
498+
monkeypatch.setattr(
499+
"openbb_core.app.model.obbject.OBBject.accessors",
500+
OBBject.accessors | {ext.name},
501+
)
502+
monkeypatch.setattr(
503+
OBBject, ext.name, CachedAccessor(ext.name, mut_accessor), raising=False
504+
)
499505

500506
# register the extension only for "mock/route"
501507
fake_loader = SimpleNamespace(on_command_output_callbacks={"mock/route": [ext]})
@@ -535,7 +541,13 @@ def test_results_only_flag_sets_attribute_and_accessor_runs(monkeypatch):
535541
def ro_accessor(self):
536542
called["hit"] = True
537543

538-
monkeypatch.setattr(OBBject, ext.name, ro_accessor, raising=False)
544+
monkeypatch.setattr(
545+
"openbb_core.app.model.obbject.OBBject.accessors",
546+
OBBject.accessors | {ext.name},
547+
)
548+
monkeypatch.setattr(
549+
OBBject, ext.name, CachedAccessor(ext.name, ro_accessor), raising=False
550+
)
539551

540552
fake_loader = SimpleNamespace(on_command_output_callbacks={"*": [ext]})
541553
monkeypatch.setattr(

openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ def __init__(self, fig: go.Figure | None = None, **kwargs) -> None:
8787

8888
super().__init__()
8989
if fig:
90-
self.__dict__ = fig.__dict__
90+
self.__dict__ = (
91+
go.Figure(fig).__dict__ if isinstance(fig, dict) else fig.__dict__
92+
)
9193

9294
self._charting_settings: ChartingSettings | None = kwargs.pop(
9395
"charting_settings", None

0 commit comments

Comments
 (0)