mainsequence 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mainsequence/__init__.py +0 -0
- mainsequence/__main__.py +9 -0
- mainsequence/cli/__init__.py +1 -0
- mainsequence/cli/api.py +157 -0
- mainsequence/cli/cli.py +442 -0
- mainsequence/cli/config.py +78 -0
- mainsequence/cli/ssh_utils.py +126 -0
- mainsequence/client/__init__.py +17 -0
- mainsequence/client/base.py +431 -0
- mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- mainsequence/client/data_sources_interfaces/duckdb.py +1468 -0
- mainsequence/client/data_sources_interfaces/timescale.py +479 -0
- mainsequence/client/models_helpers.py +113 -0
- mainsequence/client/models_report_studio.py +412 -0
- mainsequence/client/models_tdag.py +2276 -0
- mainsequence/client/models_vam.py +1983 -0
- mainsequence/client/utils.py +387 -0
- mainsequence/dashboards/__init__.py +0 -0
- mainsequence/dashboards/streamlit/__init__.py +0 -0
- mainsequence/dashboards/streamlit/assets/config.toml +12 -0
- mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
- mainsequence/dashboards/streamlit/assets/logo.png +0 -0
- mainsequence/dashboards/streamlit/core/__init__.py +0 -0
- mainsequence/dashboards/streamlit/core/theme.py +212 -0
- mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
- mainsequence/dashboards/streamlit/scaffold.py +220 -0
- mainsequence/instrumentation/__init__.py +7 -0
- mainsequence/instrumentation/utils.py +101 -0
- mainsequence/instruments/__init__.py +1 -0
- mainsequence/instruments/data_interface/__init__.py +10 -0
- mainsequence/instruments/data_interface/data_interface.py +361 -0
- mainsequence/instruments/instruments/__init__.py +3 -0
- mainsequence/instruments/instruments/base_instrument.py +85 -0
- mainsequence/instruments/instruments/bond.py +447 -0
- mainsequence/instruments/instruments/european_option.py +74 -0
- mainsequence/instruments/instruments/interest_rate_swap.py +217 -0
- mainsequence/instruments/instruments/json_codec.py +585 -0
- mainsequence/instruments/instruments/knockout_fx_option.py +146 -0
- mainsequence/instruments/instruments/position.py +475 -0
- mainsequence/instruments/instruments/ql_fields.py +239 -0
- mainsequence/instruments/instruments/vanilla_fx_option.py +107 -0
- mainsequence/instruments/pricing_models/__init__.py +0 -0
- mainsequence/instruments/pricing_models/black_scholes.py +49 -0
- mainsequence/instruments/pricing_models/bond_pricer.py +182 -0
- mainsequence/instruments/pricing_models/fx_option_pricer.py +90 -0
- mainsequence/instruments/pricing_models/indices.py +350 -0
- mainsequence/instruments/pricing_models/knockout_fx_pricer.py +209 -0
- mainsequence/instruments/pricing_models/swap_pricer.py +502 -0
- mainsequence/instruments/settings.py +175 -0
- mainsequence/instruments/utils.py +29 -0
- mainsequence/logconf.py +284 -0
- mainsequence/reportbuilder/__init__.py +0 -0
- mainsequence/reportbuilder/__main__.py +0 -0
- mainsequence/reportbuilder/examples/ms_template_report.py +706 -0
- mainsequence/reportbuilder/model.py +713 -0
- mainsequence/reportbuilder/slide_templates.py +532 -0
- mainsequence/tdag/__init__.py +8 -0
- mainsequence/tdag/__main__.py +0 -0
- mainsequence/tdag/config.py +129 -0
- mainsequence/tdag/data_nodes/__init__.py +12 -0
- mainsequence/tdag/data_nodes/build_operations.py +751 -0
- mainsequence/tdag/data_nodes/data_nodes.py +1292 -0
- mainsequence/tdag/data_nodes/persist_managers.py +812 -0
- mainsequence/tdag/data_nodes/run_operations.py +543 -0
- mainsequence/tdag/data_nodes/utils.py +24 -0
- mainsequence/tdag/future_registry.py +25 -0
- mainsequence/tdag/utils.py +40 -0
- mainsequence/virtualfundbuilder/__init__.py +45 -0
- mainsequence/virtualfundbuilder/__main__.py +235 -0
- mainsequence/virtualfundbuilder/agent_interface.py +77 -0
- mainsequence/virtualfundbuilder/config_handling.py +86 -0
- mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
- mainsequence/virtualfundbuilder/contrib/apps/__init__.py +8 -0
- mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +164 -0
- mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +292 -0
- mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +107 -0
- mainsequence/virtualfundbuilder/contrib/apps/news_app.py +437 -0
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +91 -0
- mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +95 -0
- mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +45 -0
- mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +40 -0
- mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +147 -0
- mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +77 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +5 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +61 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +149 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +310 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +78 -0
- mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +269 -0
- mainsequence/virtualfundbuilder/contrib/prices/__init__.py +1 -0
- mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +810 -0
- mainsequence/virtualfundbuilder/contrib/prices/utils.py +11 -0
- mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +1 -0
- mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +313 -0
- mainsequence/virtualfundbuilder/data_nodes.py +637 -0
- mainsequence/virtualfundbuilder/enums.py +23 -0
- mainsequence/virtualfundbuilder/models.py +282 -0
- mainsequence/virtualfundbuilder/notebook_handling.py +42 -0
- mainsequence/virtualfundbuilder/portfolio_interface.py +272 -0
- mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
- mainsequence/virtualfundbuilder/resource_factory/app_factory.py +170 -0
- mainsequence/virtualfundbuilder/resource_factory/base_factory.py +238 -0
- mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +101 -0
- mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +183 -0
- mainsequence/virtualfundbuilder/utils.py +381 -0
- mainsequence-2.0.0.dist-info/METADATA +105 -0
- mainsequence-2.0.0.dist-info/RECORD +110 -0
- mainsequence-2.0.0.dist-info/WHEEL +5 -0
- mainsequence-2.0.0.dist-info/licenses/LICENSE +40 -0
- mainsequence-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, Callable, Mapping, MutableMapping, Optional, Tuple, Union
|
5
|
+
|
6
|
+
import streamlit as st
|
7
|
+
|
8
|
+
from mainsequence.dashboards.streamlit.core.theme import inject_css_for_dark_accents, override_spinners
|
9
|
+
from importlib.resources import files as _pkg_files
|
10
|
+
import sys
|
11
|
+
import os
|
12
|
+
|
13
|
+
def _detect_app_dir() -> Path:
|
14
|
+
"""
|
15
|
+
Best-effort detection of the directory that contains the running Streamlit app.
|
16
|
+
Priority:
|
17
|
+
1) sys.modules['__main__'].__file__ (how Streamlit executes scripts)
|
18
|
+
2) Streamlit script run context (main_script_path) if available
|
19
|
+
3) env override MS_APP_DIR / STREAMLIT_APP_DIR
|
20
|
+
4) fallback: Path.cwd()
|
21
|
+
"""
|
22
|
+
# 1) __main__.__file__
|
23
|
+
try:
|
24
|
+
main_mod = sys.modules.get("__main__")
|
25
|
+
if main_mod and getattr(main_mod, "__file__", None):
|
26
|
+
return Path(main_mod.__file__).resolve().parent
|
27
|
+
except Exception:
|
28
|
+
pass
|
29
|
+
|
30
|
+
# 2) Streamlit runtime (private API; guarded)
|
31
|
+
try:
|
32
|
+
from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
|
33
|
+
ctx = get_script_run_ctx()
|
34
|
+
if ctx and getattr(ctx, "main_script_path", None):
|
35
|
+
return Path(ctx.main_script_path).resolve().parent
|
36
|
+
except Exception:
|
37
|
+
pass
|
38
|
+
|
39
|
+
# 3) env override
|
40
|
+
for var in ("MS_APP_DIR", "STREAMLIT_APP_DIR"):
|
41
|
+
p = os.environ.get(var)
|
42
|
+
if p:
|
43
|
+
try:
|
44
|
+
return Path(p).resolve()
|
45
|
+
except Exception:
|
46
|
+
pass
|
47
|
+
|
48
|
+
# 4) fallback
|
49
|
+
return Path.cwd()
|
50
|
+
|
51
|
+
|
52
|
+
def _bootstrap_theme_from_package(
|
53
|
+
package: str = "mainsequence.dashboards.streamlit",
|
54
|
+
resource: str = "assets/config.toml", # keep this in assets/ (dot-dirs often excluded from wheels)
|
55
|
+
target_root: Path | None = None,
|
56
|
+
) -> Path | None:
|
57
|
+
"""
|
58
|
+
Ensure there's a streamlit/config.toml next to the app script.
|
59
|
+
If missing, copy the packaged default once and rerun so theme applies.
|
60
|
+
Returns the path to the config file (or None if not created).
|
61
|
+
"""
|
62
|
+
# Read default theme from the package (if present)
|
63
|
+
|
64
|
+
try:
|
65
|
+
src = _pkg_files(package).joinpath(resource)
|
66
|
+
|
67
|
+
if not src.is_file():
|
68
|
+
return None # packaged file not present
|
69
|
+
default_toml = src.read_text(encoding="utf-8")
|
70
|
+
except Exception:
|
71
|
+
|
72
|
+
return None # no packaged theme; nothing to do
|
73
|
+
|
74
|
+
app_dir = target_root or _detect_app_dir()
|
75
|
+
cfg_dir = app_dir / ".streamlit"
|
76
|
+
cfg_file = cfg_dir / "config.toml"
|
77
|
+
|
78
|
+
if not cfg_file.exists():
|
79
|
+
try:
|
80
|
+
cfg_dir.mkdir(parents=True, exist_ok=True)
|
81
|
+
cfg_file.write_text(default_toml, encoding="utf-8")
|
82
|
+
# Avoid infinite loop: only rerun once
|
83
|
+
if not st.session_state.get("_ms_theme_bootstrapped"):
|
84
|
+
st.session_state["_ms_theme_bootstrapped"] = True
|
85
|
+
st.rerun()
|
86
|
+
except Exception:
|
87
|
+
# If we cannot write, just skip silently to avoid breaking the app
|
88
|
+
return None
|
89
|
+
|
90
|
+
return cfg_file
|
91
|
+
|
92
|
+
# --- App configuration contract (provided by the example app) -----------------
|
93
|
+
|
94
|
+
HeaderFn = Callable[[Any], None]
|
95
|
+
RouteFn = Callable[[Mapping[str, Any]], str]
|
96
|
+
ContextFn = Callable[[MutableMapping[str, Any]], Any]
|
97
|
+
InitSessionFn = Callable[[MutableMapping[str, Any]], None]
|
98
|
+
NotFoundFn = Callable[[], None]
|
99
|
+
|
100
|
+
@dataclass
|
101
|
+
class PageConfig:
|
102
|
+
title: str
|
103
|
+
build_context: Optional[ContextFn] =None # required
|
104
|
+
|
105
|
+
render_header: Optional[HeaderFn] = None # if None, minimal header
|
106
|
+
init_session: Optional[InitSessionFn] = None # set defaults in session_state
|
107
|
+
|
108
|
+
# Optional overrides; if None, scaffold uses its bundled defaults.
|
109
|
+
logo_path: Optional[Union[str, Path]] = None
|
110
|
+
page_icon_path: Optional[Union[str, Path]] = None
|
111
|
+
|
112
|
+
use_wide_layout: bool = True
|
113
|
+
hide_streamlit_multipage_nav: bool = False
|
114
|
+
inject_theme_css: bool = True
|
115
|
+
|
116
|
+
# --- Internal helpers ---------------------------------------------------------
|
117
|
+
|
118
|
+
_HIDE_NATIVE_NAV = """
|
119
|
+
<style>[data-testid='stSidebarNav']{display:none!important}</style>
|
120
|
+
"""
|
121
|
+
|
122
|
+
def _hide_sidebar() -> None:
|
123
|
+
st.markdown("""
|
124
|
+
<style>
|
125
|
+
[data-testid="stSidebar"]{display:none!important;}
|
126
|
+
[data-testid="stSidebarCollapseControl"]{display:none!important;}
|
127
|
+
</style>
|
128
|
+
""", unsafe_allow_html=True)
|
129
|
+
|
130
|
+
def _minimal_header(title: str) -> None:
|
131
|
+
st.title(title)
|
132
|
+
|
133
|
+
def _resolve_assets(explicit_logo: Optional[Union[str, Path]],
|
134
|
+
explicit_icon: Optional[Union[str, Path]]) -> Tuple[Optional[str], Union[str, None], Optional[str]]:
|
135
|
+
"""
|
136
|
+
Returns a tuple:
|
137
|
+
(logo_path_for_st_logo, page_icon_for_set_page_config, icon_path_for_st_logo_param)
|
138
|
+
|
139
|
+
- If no overrides are provided, uses scaffold defaults:
|
140
|
+
mainsequence.dashboards.streamlit/assets/logo.png
|
141
|
+
mainsequence.dashboards.streamlit/assets/favicon.png
|
142
|
+
- If favicon file is missing, falls back to emoji "📊" for set_page_config.
|
143
|
+
- st.logo() will only receive icon_image if a real file exists.
|
144
|
+
"""
|
145
|
+
base_assets = Path(__file__).resolve().parent / "assets"
|
146
|
+
default_logo = base_assets / "logo.png"
|
147
|
+
default_favicon = base_assets / "favicon.png"
|
148
|
+
|
149
|
+
# Pick explicit override or default paths
|
150
|
+
logo_path = Path(explicit_logo) if explicit_logo else default_logo
|
151
|
+
icon_path = Path(explicit_icon) if explicit_icon else default_favicon
|
152
|
+
|
153
|
+
# Effective values
|
154
|
+
logo_for_logo_api: Optional[str] = str(logo_path) if logo_path.exists() else None
|
155
|
+
icon_for_page_config: Union[str, None]
|
156
|
+
icon_for_logo_param: Optional[str]
|
157
|
+
|
158
|
+
if icon_path.exists():
|
159
|
+
icon_for_page_config = str(icon_path)
|
160
|
+
icon_for_logo_param = str(icon_path)
|
161
|
+
else:
|
162
|
+
# Streamlit allows emoji for set_page_config, but st.logo needs a file path.
|
163
|
+
icon_for_page_config = "📊"
|
164
|
+
icon_for_logo_param = None
|
165
|
+
|
166
|
+
return logo_for_logo_api, icon_for_page_config, icon_for_logo_param
|
167
|
+
|
168
|
+
# --- Public entrypoint --------------------------------------------------------
|
169
|
+
|
170
|
+
# scaffold.py
|
171
|
+
def run_page(cfg: PageConfig):
|
172
|
+
"""
|
173
|
+
Initialize page-wide look & feel, theme, context, and header.
|
174
|
+
Call this at the top of *every* Streamlit page (Home + pages/*).
|
175
|
+
Returns a context object (whatever build_context returns).
|
176
|
+
"""
|
177
|
+
# 1) Page config should be the first Streamlit call
|
178
|
+
_logo, _page_icon, _icon_for_logo = _resolve_assets(cfg.logo_path, cfg.page_icon_path)
|
179
|
+
st.set_page_config(
|
180
|
+
page_title=cfg.title,
|
181
|
+
page_icon=_page_icon,
|
182
|
+
layout="wide" if cfg.use_wide_layout else "centered",
|
183
|
+
)
|
184
|
+
|
185
|
+
# 2) Optional: logo + CSS tweaks
|
186
|
+
if _logo:
|
187
|
+
st.logo(_logo, icon_image=_icon_for_logo)
|
188
|
+
if cfg.inject_theme_css:
|
189
|
+
inject_css_for_dark_accents()
|
190
|
+
|
191
|
+
# 3) Spinners (pure CSS)
|
192
|
+
override_spinners()
|
193
|
+
|
194
|
+
# 4) Do NOT hide the native nav unless explicitly asked
|
195
|
+
if cfg.hide_streamlit_multipage_nav:
|
196
|
+
st.markdown(_HIDE_NATIVE_NAV, unsafe_allow_html=True)
|
197
|
+
|
198
|
+
# 5) Session + context
|
199
|
+
if cfg.init_session:
|
200
|
+
cfg.init_session(st.session_state)
|
201
|
+
|
202
|
+
ctx={}
|
203
|
+
if cfg.build_context:
|
204
|
+
ctx = cfg.build_context(st.session_state)
|
205
|
+
|
206
|
+
# 6) Header
|
207
|
+
if cfg.render_header:
|
208
|
+
cfg.render_header(ctx)
|
209
|
+
else:
|
210
|
+
_minimal_header(cfg.title)
|
211
|
+
|
212
|
+
# 7) Create .streamlit/config.toml on first run (reruns once if created)
|
213
|
+
from pathlib import Path
|
214
|
+
print(Path.cwd())
|
215
|
+
_bootstrap_theme_from_package()
|
216
|
+
|
217
|
+
return ctx
|
218
|
+
|
219
|
+
|
220
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from opentelemetry import trace
|
4
|
+
from opentelemetry.trace import (
|
5
|
+
INVALID_SPAN,
|
6
|
+
INVALID_SPAN_CONTEXT,
|
7
|
+
get_current_span,
|
8
|
+
get_tracer_provider,
|
9
|
+
set_tracer_provider,
|
10
|
+
get_tracer,
|
11
|
+
Status,
|
12
|
+
SpanKind,
|
13
|
+
StatusCode,
|
14
|
+
|
15
|
+
)
|
16
|
+
from opentelemetry.sdk.trace import (
|
17
|
+
TracerProvider,
|
18
|
+
|
19
|
+
)
|
20
|
+
from opentelemetry.sdk.trace.export import (
|
21
|
+
BatchSpanProcessor,
|
22
|
+
ConsoleSpanExporter
|
23
|
+
)
|
24
|
+
from opentelemetry.trace.propagation.tracecontext import \
|
25
|
+
TraceContextTextMapPropagator
|
26
|
+
from typing import Union
|
27
|
+
import structlog
|
28
|
+
|
29
|
+
def is_port_in_use(port: int,agent_host:str) -> bool:
|
30
|
+
import socket
|
31
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
32
|
+
return s.connect_ex((agent_host, port)) == 0
|
33
|
+
|
34
|
+
class TracerInstrumentator():
|
35
|
+
__doc__ = f"""
|
36
|
+
Main instrumentator class controlls building and exporting of traces
|
37
|
+
"""
|
38
|
+
|
39
|
+
def build_tracer(self) -> TraceContextTextMapPropagator:
|
40
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
41
|
+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
42
|
+
resource = Resource(attributes={SERVICE_NAME: "tdag"})
|
43
|
+
set_tracer_provider(TracerProvider(resource=resource))
|
44
|
+
|
45
|
+
end_point = os.environ.get("OTLP_ENDPOINT")
|
46
|
+
|
47
|
+
if end_point is not None:
|
48
|
+
otlp_exporter = OTLPSpanExporter(endpoint=end_point)
|
49
|
+
if is_port_in_use(4317,agent_host=self.agent_host)== True:
|
50
|
+
get_tracer_provider().add_span_processor(BatchSpanProcessor(otlp_exporter))
|
51
|
+
else:
|
52
|
+
get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
53
|
+
tracer = get_tracer("tdag")
|
54
|
+
return tracer
|
55
|
+
|
56
|
+
def get_current_trace_id(self):
|
57
|
+
current_span = get_current_span()
|
58
|
+
return format(current_span.context.trace_id, "032x")
|
59
|
+
|
60
|
+
def get_telemetry_carrier(self):
|
61
|
+
prop = TraceContextTextMapPropagator()
|
62
|
+
telemetry_carrier = {}
|
63
|
+
prop.inject(carrier=telemetry_carrier)
|
64
|
+
return telemetry_carrier
|
65
|
+
|
66
|
+
def append_attribute_to_current_span(self,attribute_key,attribute_value):
|
67
|
+
current_span = get_current_span()
|
68
|
+
current_span.set_attribute(attribute_key, attribute_value)
|
69
|
+
|
70
|
+
def add_otel_trace_context(logger, method_name, event_dict):
|
71
|
+
"""
|
72
|
+
Enrich log records with OpenTelemetry trace context (trace_id, span_id).
|
73
|
+
"""
|
74
|
+
span = trace.get_current_span()
|
75
|
+
if not span.is_recording():
|
76
|
+
event_dict["span"] = None
|
77
|
+
return event_dict
|
78
|
+
|
79
|
+
ctx = span.get_span_context()
|
80
|
+
parent = getattr(span, "parent", None)
|
81
|
+
|
82
|
+
event_dict["span"] = {
|
83
|
+
"span_id": format(ctx.span_id, "016x"),
|
84
|
+
"trace_id": format(ctx.trace_id, "032x"),
|
85
|
+
"parent_span_id": None if not parent else format(parent.span_id, "016x"),
|
86
|
+
}
|
87
|
+
|
88
|
+
return event_dict
|
89
|
+
|
90
|
+
class OTelJSONRenderer(structlog.processors.JSONRenderer):
|
91
|
+
"""
|
92
|
+
A custom JSON renderer that injects OTel trace/span fields
|
93
|
+
immediately before serializing to JSON.
|
94
|
+
"""
|
95
|
+
def __call__(self, logger, method_name, event_dict):
|
96
|
+
# 1) Grab the current active span from OpenTelemetry
|
97
|
+
event_dict=add_otel_trace_context(logger,method_name,event_dict)
|
98
|
+
|
99
|
+
# 3) Now call the base JSONRenderer to produce final JSON
|
100
|
+
return super().__call__(logger, method_name, event_dict)
|
101
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
from .instruments import *
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from .data_interface import DateInfo, MockDataInterface, MSInterface
|
2
|
+
from mainsequence.instruments import settings
|
3
|
+
|
4
|
+
def _make_backend():
|
5
|
+
if getattr(settings, "data", None) and getattr(settings.data, "backend", "mock") == "mainsequence":
|
6
|
+
return MSInterface()
|
7
|
+
return MockDataInterface()
|
8
|
+
|
9
|
+
# export a single, uniform instance
|
10
|
+
data_interface = _make_backend()
|