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,387 @@
|
|
1
|
+
import copy
|
2
|
+
from socket import socket
|
3
|
+
|
4
|
+
import psutil
|
5
|
+
import requests
|
6
|
+
import os
|
7
|
+
from requests.structures import CaseInsensitiveDict
|
8
|
+
import datetime
|
9
|
+
import time
|
10
|
+
import pytz
|
11
|
+
from typing import Union, Optional, TypedDict, Dict
|
12
|
+
from mainsequence.logconf import logger
|
13
|
+
from enum import Enum
|
14
|
+
|
15
|
+
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
|
16
|
+
|
17
|
+
|
18
|
+
class DataFrequency(str, Enum):
|
19
|
+
one_m = "1m"
|
20
|
+
five_m = "5m"
|
21
|
+
one_d = "1d"
|
22
|
+
one_w = "1w"
|
23
|
+
one_year = "1y"
|
24
|
+
one_month = "1mo"
|
25
|
+
one_quarter = "1q"
|
26
|
+
|
27
|
+
|
28
|
+
class DateInfo(TypedDict, total=False):
|
29
|
+
start_date: Optional[datetime.datetime]
|
30
|
+
start_date_operand: Optional[str]
|
31
|
+
end_date: Optional[datetime.datetime]
|
32
|
+
end_date_operand: Optional[str]
|
33
|
+
|
34
|
+
|
35
|
+
UniqueIdentifierRangeMap = Dict[str, DateInfo]
|
36
|
+
|
37
|
+
|
38
|
+
def request_to_datetime(string_date: str):
|
39
|
+
if "+" in string_date:
|
40
|
+
string_date = datetime.datetime.fromisoformat(string_date.replace("T", " ")).replace(tzinfo=pytz.utc)
|
41
|
+
return string_date
|
42
|
+
try:
|
43
|
+
date = datetime.datetime.strptime(string_date, DATE_FORMAT).replace(
|
44
|
+
tzinfo=pytz.utc)
|
45
|
+
except ValueError:
|
46
|
+
date = datetime.datetime.strptime(string_date, "%Y-%m-%dT%H:%M:%SZ").replace(
|
47
|
+
tzinfo=pytz.utc)
|
48
|
+
return date
|
49
|
+
|
50
|
+
|
51
|
+
class DoesNotExist(Exception):
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
class AuthLoaders:
|
56
|
+
@property
|
57
|
+
def auth_headers(self):
|
58
|
+
if not hasattr(self, "_auth_headers"):
|
59
|
+
self.refresh_headers()
|
60
|
+
return self._auth_headers
|
61
|
+
|
62
|
+
def refresh_headers(self):
|
63
|
+
logger.debug("Getting Auth Headers ASSETS_ORM")
|
64
|
+
self._auth_headers = get_authorization_headers()
|
65
|
+
|
66
|
+
|
67
|
+
def get_authorization_headers():
|
68
|
+
headers = get_rest_token_header()
|
69
|
+
return headers
|
70
|
+
|
71
|
+
|
72
|
+
def make_request(
|
73
|
+
s,
|
74
|
+
r_type: str,
|
75
|
+
url: str,
|
76
|
+
loaders: Union[AuthLoaders, None],
|
77
|
+
payload: Union[dict, None] = None,
|
78
|
+
time_out=None,
|
79
|
+
accept_gzip: bool = True
|
80
|
+
):
|
81
|
+
from requests.models import Response
|
82
|
+
|
83
|
+
TIMEOFF = 0.25
|
84
|
+
TRIES = int(15 // TIMEOFF)
|
85
|
+
timeout = 120 if time_out is None else time_out
|
86
|
+
payload = {} if payload is None else payload
|
87
|
+
|
88
|
+
def get_req(session):
|
89
|
+
if r_type == "GET":
|
90
|
+
return session.get
|
91
|
+
elif r_type == "POST":
|
92
|
+
return session.post
|
93
|
+
elif r_type == "PATCH":
|
94
|
+
return session.patch
|
95
|
+
elif r_type == "DELETE":
|
96
|
+
return session.delete
|
97
|
+
else:
|
98
|
+
raise NotImplementedError(f"Unsupported method: {r_type}")
|
99
|
+
|
100
|
+
# --- Prepare kwargs for requests call ---
|
101
|
+
request_kwargs = {}
|
102
|
+
if r_type in ("POST", "PATCH") and "files" in payload:
|
103
|
+
# We have file uploads → use multipart form data
|
104
|
+
request_kwargs["data"] = payload.get("json", {}) # form fields
|
105
|
+
request_kwargs["files"] = payload["files"] # actual files
|
106
|
+
s.headers.pop("Content-Type", None)
|
107
|
+
else:
|
108
|
+
# Fallback: no files, no json → just form fields
|
109
|
+
request_kwargs = payload
|
110
|
+
|
111
|
+
req = get_req(session=s)
|
112
|
+
keep_request = True
|
113
|
+
counter = 0
|
114
|
+
headers_refreshed = False
|
115
|
+
|
116
|
+
if accept_gzip:
|
117
|
+
# Don't clobber other headers; just ensure the key exists.
|
118
|
+
# Keep it simple: gzip covers Django's GZipMiddleware.
|
119
|
+
s.headers.setdefault("Accept-Encoding", "gzip")
|
120
|
+
|
121
|
+
# Now loop with retry logic
|
122
|
+
while keep_request:
|
123
|
+
try:
|
124
|
+
start_time = time.perf_counter()
|
125
|
+
logger.debug(f"Requesting {r_type} from {url}")
|
126
|
+
r = req(url, timeout=timeout, **request_kwargs)
|
127
|
+
duration = time.perf_counter() - start_time
|
128
|
+
logger.debug(f"{url} took {duration:.4f} seconds.")
|
129
|
+
|
130
|
+
if r.status_code in [403, 401] and not headers_refreshed:
|
131
|
+
logger.warning(f"Error {r.status_code} Refreshing headers")
|
132
|
+
loaders.refresh_headers()
|
133
|
+
s.headers.update(loaders.auth_headers)
|
134
|
+
req = get_req(session=s)
|
135
|
+
headers_refreshed = True
|
136
|
+
else:
|
137
|
+
keep_request = False
|
138
|
+
break
|
139
|
+
except requests.exceptions.ConnectionError as errc:
|
140
|
+
logger.exception(f"Error connecting {url}")
|
141
|
+
except TypeError as e:
|
142
|
+
logger.exception(f"Type error for {url} exception {e}")
|
143
|
+
raise e
|
144
|
+
except Exception as e:
|
145
|
+
logger.exception(f"Error connecting {url} exception {e}")
|
146
|
+
|
147
|
+
counter += 1
|
148
|
+
if counter >= TRIES:
|
149
|
+
keep_request = False
|
150
|
+
r = Response()
|
151
|
+
r.code = "expired"
|
152
|
+
r.error_type = "expired"
|
153
|
+
r.status_code = 500
|
154
|
+
break
|
155
|
+
|
156
|
+
logger.debug(f"Trying request again after {TIMEOFF}s "
|
157
|
+
f"- Counter: {counter}/{TRIES} - URL: {url}")
|
158
|
+
time.sleep(TIMEOFF)
|
159
|
+
return r
|
160
|
+
|
161
|
+
|
162
|
+
def build_session():
|
163
|
+
from requests.adapters import HTTPAdapter, Retry
|
164
|
+
s = requests.Session()
|
165
|
+
retries = Retry(total=2, backoff_factor=2, )
|
166
|
+
s.mount('http://', HTTPAdapter(max_retries=retries))
|
167
|
+
return s
|
168
|
+
|
169
|
+
|
170
|
+
def get_constants_tdag():
|
171
|
+
url = f"{os.getenv('TDAG_ENDPOINT')}/orm/api/ts_manager/api/constants"
|
172
|
+
loaders = AuthLoaders()
|
173
|
+
s = build_session()
|
174
|
+
s.headers.update(loaders.auth_headers)
|
175
|
+
r = make_request(s=s, loaders=loaders, r_type="GET", url=url)
|
176
|
+
return r.json()
|
177
|
+
|
178
|
+
|
179
|
+
def get_constants_vam():
|
180
|
+
url = f"{os.getenv('TDAG_ENDPOINT')}/orm/api/assets/api/constants"
|
181
|
+
loaders = AuthLoaders()
|
182
|
+
s = build_session()
|
183
|
+
s.headers.update(loaders.auth_headers)
|
184
|
+
r = make_request(s=s, loaders=loaders, r_type="GET", url=url)
|
185
|
+
return r.json()
|
186
|
+
|
187
|
+
|
188
|
+
def get_binance_constants():
|
189
|
+
url = f"{os.getenv('TDAG_ENDPOINT')}/orm/api/binance/constants"
|
190
|
+
loaders = AuthLoaders()
|
191
|
+
s = build_session()
|
192
|
+
s.headers.update(loaders.auth_headers)
|
193
|
+
r = make_request(s=s, loaders=loaders, r_type="GET", url=url)
|
194
|
+
return r.json()
|
195
|
+
|
196
|
+
|
197
|
+
class LazyConstants(dict):
|
198
|
+
"""
|
199
|
+
Class Method to load constants only once they are called. this minimizes the calls to the API
|
200
|
+
"""
|
201
|
+
|
202
|
+
def __init__(self, constant_type: str):
|
203
|
+
if constant_type == "tdag":
|
204
|
+
self.CONSTANTS_METHOD = get_constants_tdag
|
205
|
+
elif constant_type == "vam":
|
206
|
+
self.CONSTANTS_METHOD = get_constants_vam
|
207
|
+
elif constant_type == "binance":
|
208
|
+
self.CONSTANTS_METHOD = get_binance_constants
|
209
|
+
else:
|
210
|
+
raise NotImplementedError(f"{constant_type} not implemented")
|
211
|
+
self._initialized = False
|
212
|
+
|
213
|
+
def __getattr__(self, key):
|
214
|
+
if not self._initialized:
|
215
|
+
self._load_constants()
|
216
|
+
return self.__dict__[key]
|
217
|
+
|
218
|
+
def _load_constants(self):
|
219
|
+
# 1) call the method that returns your top-level dict
|
220
|
+
raw_data = self.CONSTANTS_METHOD()
|
221
|
+
# 2) Convert nested dicts to an "object" style
|
222
|
+
nested = self.to_attr_dict(raw_data)
|
223
|
+
# 3) Dump everything into self.__dict__ so it's dot-accessible
|
224
|
+
for k, v in nested.items():
|
225
|
+
self.__dict__[k] = v
|
226
|
+
self._initialized = True
|
227
|
+
|
228
|
+
def to_attr_dict(self, data):
|
229
|
+
"""
|
230
|
+
Recursively convert a Python dict into an object that allows dot-notation access.
|
231
|
+
Non-dict values (e.g., int, str, list) are returned as-is; dicts become _AttrDict.
|
232
|
+
"""
|
233
|
+
if not isinstance(data, dict):
|
234
|
+
return data
|
235
|
+
|
236
|
+
class _AttrDict(dict):
|
237
|
+
def __getattr__(self, name):
|
238
|
+
return self[name]
|
239
|
+
|
240
|
+
def __setattr__(self, name, value):
|
241
|
+
self[name] = value
|
242
|
+
|
243
|
+
out = _AttrDict()
|
244
|
+
for k, v in data.items():
|
245
|
+
out[k] = self.to_attr_dict(v) # recursively transform
|
246
|
+
return out
|
247
|
+
|
248
|
+
|
249
|
+
if 'TDAG_CONSTANTS' not in locals():
|
250
|
+
TDAG_CONSTANTS = LazyConstants("tdag")
|
251
|
+
|
252
|
+
if 'MARKETS_CONSTANTS' not in locals():
|
253
|
+
MARKETS_CONSTANTS = LazyConstants("vam")
|
254
|
+
|
255
|
+
if "BINANCE_CONSTANTS" not in locals():
|
256
|
+
BINANCE_CONSTANTS = LazyConstants("binance")
|
257
|
+
|
258
|
+
|
259
|
+
def get_rest_token_header():
|
260
|
+
headers = CaseInsensitiveDict()
|
261
|
+
headers["Content-Type"] = "application/json"
|
262
|
+
|
263
|
+
if os.getenv("MAINSEQUENCE_TOKEN"):
|
264
|
+
headers["Authorization"] = "Token " + os.getenv("MAINSEQUENCE_TOKEN")
|
265
|
+
return headers
|
266
|
+
else:
|
267
|
+
raise Exception("MAINSEQUENCE_TOKEN is not set in env")
|
268
|
+
|
269
|
+
|
270
|
+
def get_network_ip():
|
271
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
272
|
+
# Connect to a well-known external host (Google DNS) on port 80
|
273
|
+
s.connect(("8.8.8.8", 80))
|
274
|
+
# Get the local IP address used to make the connection
|
275
|
+
network_ip = s.getsockname()[0]
|
276
|
+
return network_ip
|
277
|
+
|
278
|
+
|
279
|
+
def is_process_running(pid: int) -> bool:
|
280
|
+
"""
|
281
|
+
Check if a process with the given PID is running.
|
282
|
+
|
283
|
+
Args:
|
284
|
+
pid (int): The process ID to check.
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
bool: True if the process is running, False otherwise.
|
288
|
+
"""
|
289
|
+
try:
|
290
|
+
# Check if the process with the given PID is running
|
291
|
+
process = psutil.Process(pid)
|
292
|
+
return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
|
293
|
+
except psutil.NoSuchProcess:
|
294
|
+
# Process with the given PID does not exist
|
295
|
+
return False
|
296
|
+
|
297
|
+
|
298
|
+
def set_types_in_table(df, column_types):
|
299
|
+
index_cols = [name for name in df.index.names if name is not None]
|
300
|
+
if index_cols:
|
301
|
+
df = df.reset_index()
|
302
|
+
|
303
|
+
for c, col_type in column_types.items():
|
304
|
+
if c in df.columns:
|
305
|
+
if col_type == "object":
|
306
|
+
df[c] = df[c].astype(str)
|
307
|
+
else:
|
308
|
+
df[c] = df[c].astype(col_type)
|
309
|
+
|
310
|
+
if index_cols:
|
311
|
+
df = df.set_index(index_cols)
|
312
|
+
return df
|
313
|
+
|
314
|
+
|
315
|
+
def serialize_to_json(kwargs):
|
316
|
+
new_data = {}
|
317
|
+
for key, value in kwargs.items():
|
318
|
+
new_value = copy.deepcopy(value)
|
319
|
+
if isinstance(value, datetime.datetime):
|
320
|
+
new_value = str(value)
|
321
|
+
|
322
|
+
new_data[key] = new_value
|
323
|
+
return new_data
|
324
|
+
|
325
|
+
|
326
|
+
import os
|
327
|
+
import pathlib
|
328
|
+
import shutil
|
329
|
+
import subprocess
|
330
|
+
import uuid
|
331
|
+
|
332
|
+
|
333
|
+
def _linux_machine_id() -> Optional[str]:
|
334
|
+
"""Return the OS machine‑id if readable (many distros make this 0644)."""
|
335
|
+
for p in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
|
336
|
+
path = pathlib.Path(p)
|
337
|
+
if path.is_file():
|
338
|
+
try:
|
339
|
+
return path.read_text().strip().lower()
|
340
|
+
except PermissionError:
|
341
|
+
continue
|
342
|
+
return None
|
343
|
+
|
344
|
+
|
345
|
+
def bios_uuid() -> str:
|
346
|
+
"""Best‑effort hardware/OS identifier that never returns None.
|
347
|
+
|
348
|
+
Order of preference
|
349
|
+
-------------------
|
350
|
+
1. `/sys/class/dmi/id/product_uuid` (kernel‑exported, no root)
|
351
|
+
2. `dmidecode -s system-uuid` (requires root *and* dmidecode)
|
352
|
+
3. `/etc/machine-id` or `/var/lib/dbus/machine-id`
|
353
|
+
4. `uuid.getnode()` (MAC address as 48‑bit int, zero‑padded hex)
|
354
|
+
|
355
|
+
The value is always lower‑case and stripped of whitespace.
|
356
|
+
"""
|
357
|
+
# Tier 1 – kernel DMI file
|
358
|
+
path = pathlib.Path("/sys/class/dmi/id/product_uuid")
|
359
|
+
if path.is_file():
|
360
|
+
try:
|
361
|
+
val = path.read_text().strip().lower()
|
362
|
+
if val:
|
363
|
+
return val
|
364
|
+
except PermissionError:
|
365
|
+
pass
|
366
|
+
|
367
|
+
# Tier 2 – dmidecode, but only if available *and* running as root
|
368
|
+
if shutil.which("dmidecode") and os.geteuid() == 0:
|
369
|
+
try:
|
370
|
+
out = subprocess.check_output(
|
371
|
+
["dmidecode", "-s", "system-uuid"],
|
372
|
+
text=True,
|
373
|
+
stderr=subprocess.DEVNULL,
|
374
|
+
)
|
375
|
+
val = out.splitlines()[0].strip().lower()
|
376
|
+
if val:
|
377
|
+
return val
|
378
|
+
except subprocess.SubprocessError:
|
379
|
+
pass
|
380
|
+
|
381
|
+
# Tier 3 – machine‑id
|
382
|
+
mid = _linux_machine_id()
|
383
|
+
if mid:
|
384
|
+
return mid
|
385
|
+
|
386
|
+
# Tier 4 – MAC address (uuid.getnode). Always available.
|
387
|
+
return f"{uuid.getnode():012x}"
|
File without changes
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
[theme]
|
3
|
+
base="dark" # "light" or "dark"
|
4
|
+
primaryColor="#5DADE2" # buttons, widgets
|
5
|
+
backgroundColor="#0E1216" # page background
|
6
|
+
secondaryBackgroundColor="#121721"
|
7
|
+
textColor="#EAECEE"
|
8
|
+
font="sans serif" # or "serif" / "monospace"
|
9
|
+
|
10
|
+
[server]
|
11
|
+
enableXsrfProtection = true
|
12
|
+
headless = true
|
Binary file
|
Binary file
|
File without changes
|
@@ -0,0 +1,212 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import streamlit as st
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
def inject_css_for_dark_accents():
|
7
|
+
st.markdown(
|
8
|
+
"""
|
9
|
+
<style>
|
10
|
+
/* Subtle tweaks; Streamlit theme itself comes from .streamlit/config.toml */
|
11
|
+
.stMetric > div { background: rgba(255,255,255,0.04); border-radius: 6px; padding: .5rem .75rem; }
|
12
|
+
div[data-testid="stMetricDelta"] svg { display: none; } /* clean deltas, hide the arrow */
|
13
|
+
</style>
|
14
|
+
""",
|
15
|
+
unsafe_allow_html=True
|
16
|
+
)
|
17
|
+
|
18
|
+
def explain_theming():
|
19
|
+
st.info(
|
20
|
+
"Theme colors come from `.streamlit/config.toml`. "
|
21
|
+
"You can’t switch Streamlit’s theme at runtime, but you can tune Plotly’s colors and inject light CSS."
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
# --- Load spinner frames ONCE from two levels above, files: image_1_base64.txt ... image_5_base64.txt ---
|
28
|
+
def _load_spinner_frames_for_this_template() -> list[str]:
|
29
|
+
base_dir = Path(__file__).resolve().parent.parent.parent.parent # two levels above this module
|
30
|
+
frames: list[str] = []
|
31
|
+
for i in range(1, 6):
|
32
|
+
p = base_dir / f"image_{i}_base64.txt"
|
33
|
+
if not p.exists():
|
34
|
+
raise FileNotFoundError(f"Missing spinner frame file: {p}")
|
35
|
+
frames.append(p.read_text(encoding="utf-8").strip())
|
36
|
+
return frames
|
37
|
+
|
38
|
+
|
39
|
+
_SPINNER_FRAMES_RAW = _load_spinner_frames_for_this_template()
|
40
|
+
|
41
|
+
|
42
|
+
# Expose constants for the function (keeps the code below simple)
|
43
|
+
IMAGE_1_B64, IMAGE_2_B64, IMAGE_3_B64, IMAGE_4_B64, IMAGE_5_B64 = _SPINNER_FRAMES_RAW
|
44
|
+
|
45
|
+
def override_spinners(
|
46
|
+
hide_deploy_button: bool = False,
|
47
|
+
*,
|
48
|
+
# Sizes
|
49
|
+
top_px: int = 35, # top-right toolbar & st.status icon base size
|
50
|
+
inline_px: int = 288, # animation size when centered
|
51
|
+
# Timing
|
52
|
+
duration_ms: int = 900,
|
53
|
+
# Toolbar nudges / spacing
|
54
|
+
toolbar_nudge_px: int = -3,
|
55
|
+
toolbar_gap_left_px: int = 2,
|
56
|
+
toolbar_left_offset_px: int = 0,
|
57
|
+
# Centered overlay styling
|
58
|
+
center_non_toolbar: bool = True, # << keep True to center inline + status
|
59
|
+
dim_backdrop: bool = True, # << set False to hide the dark veil
|
60
|
+
overlay_blur_px: float = 1.5,
|
61
|
+
overlay_opacity: float = 0.35,
|
62
|
+
overlay_z_index: int = 9990, # keep below toolbar; we also lift toolbar above
|
63
|
+
) -> None:
|
64
|
+
"""Override Streamlit spinners with a 4-frame animation.
|
65
|
+
- Toolbar spinner stays in the toolbar (top-right).
|
66
|
+
- All other spinners (inline + st.status icon) are centered on screen.
|
67
|
+
"""
|
68
|
+
|
69
|
+
def as_data_uri(s: str, mime="image/png") -> str:
|
70
|
+
s = s.strip()
|
71
|
+
return s if s.startswith("data:") else f"data:{mime};base64,{s}"
|
72
|
+
|
73
|
+
i1 = as_data_uri(IMAGE_1_B64)
|
74
|
+
i2 = as_data_uri(IMAGE_2_B64)
|
75
|
+
i3 = as_data_uri(IMAGE_3_B64)
|
76
|
+
i4 = as_data_uri(IMAGE_4_B64)
|
77
|
+
i5= as_data_uri(IMAGE_5_B64)
|
78
|
+
|
79
|
+
veil_bg = f"rgba(0,0,0,{overlay_opacity})"
|
80
|
+
|
81
|
+
st.markdown(f"""
|
82
|
+
<style>
|
83
|
+
/* ---- 4-frame animation ---- */
|
84
|
+
@keyframes st-fourframe {{
|
85
|
+
0%% {{ background-image:url("{i1}"); }}
|
86
|
+
20% {{ background-image:url("{i2}"); }}
|
87
|
+
40% {{ background-image:url("{i3}"); }}
|
88
|
+
60% {{ background-image:url("{i4}"); }}
|
89
|
+
80% {{ background-image:url("{i5}"); }}
|
90
|
+
100% {{ background-image:url("{i5}"); }}
|
91
|
+
}}
|
92
|
+
|
93
|
+
/* ---- CSS variables ---- */
|
94
|
+
:root {{
|
95
|
+
--st-spin-top:{top_px}px; /* toolbar/status base size */
|
96
|
+
--st-spin-inline:{inline_px}px; /* centered spinner size */
|
97
|
+
--st-spin-dur:{duration_ms}ms;
|
98
|
+
|
99
|
+
--st-spin-toolbar-nudge:{toolbar_nudge_px}px;
|
100
|
+
--st-spin-toolbar-gap:{toolbar_gap_left_px}px;
|
101
|
+
--st-spin-toolbar-left:{toolbar_left_offset_px}px;
|
102
|
+
|
103
|
+
--st-overlay-z:{overlay_z_index};
|
104
|
+
--st-overlay-bg:{veil_bg};
|
105
|
+
--st-overlay-blur:{overlay_blur_px}px;
|
106
|
+
}}
|
107
|
+
|
108
|
+
/* Lift toolbar above any overlay so Stop/Deploy remain clickable */
|
109
|
+
div[data-testid="stToolbar"],
|
110
|
+
[data-testid="stStatusWidget"] {{
|
111
|
+
position: relative;
|
112
|
+
z-index: calc(var(--st-overlay-z) + 5);
|
113
|
+
}}
|
114
|
+
|
115
|
+
/* =======================================================================
|
116
|
+
1) Top-right toolbar widget (kept in place, not centered)
|
117
|
+
======================================================================= */
|
118
|
+
[data-testid="stStatusWidget"] {{
|
119
|
+
position:relative;
|
120
|
+
padding-left: calc(var(--st-spin-top) + var(--st-spin-toolbar-gap));
|
121
|
+
}}
|
122
|
+
[data-testid="stStatusWidget"] svg,
|
123
|
+
[data-testid="stStatusWidget"] img {{ display:none !important; }}
|
124
|
+
[data-testid="stStatusWidget"]::before {{
|
125
|
+
content:"";
|
126
|
+
position:absolute;
|
127
|
+
left: var(--st-spin-toolbar-left);
|
128
|
+
top:50%;
|
129
|
+
transform:translateY(-50%) translateY(var(--st-spin-toolbar-nudge));
|
130
|
+
width:var(--st-spin-top);
|
131
|
+
height:var(--st-spin-top);
|
132
|
+
background:no-repeat center/contain;
|
133
|
+
animation:st-fourframe var(--st-spin-dur) linear infinite;
|
134
|
+
}}
|
135
|
+
|
136
|
+
/* Hide the entire toolbar if requested */
|
137
|
+
{"div[data-testid='stToolbar']{display:none !important;}" if hide_deploy_button else ""}
|
138
|
+
|
139
|
+
/* =======================================================================
|
140
|
+
2) Inline spinner (st.spinner) — centered overlay
|
141
|
+
======================================================================= */
|
142
|
+
[data-testid="stSpinner"] svg {{ display:none !important; }}
|
143
|
+
[data-testid="stSpinner"] {{
|
144
|
+
min-height: 0 !important; /* avoid layout jump, since we center globally */
|
145
|
+
}}
|
146
|
+
{ "[data-testid='stSpinner']::after { content:''; position:fixed; inset:0; background:var(--st-overlay-bg); backdrop-filter: blur(var(--st-overlay-blur)); z-index: var(--st-overlay-z); pointer-events: none; }" if dim_backdrop else "" }
|
147
|
+
[data-testid="stSpinner"]::before {{
|
148
|
+
content:"";
|
149
|
+
position: fixed;
|
150
|
+
left: 50%;
|
151
|
+
top: 50%;
|
152
|
+
transform: translate(-50%,-50%);
|
153
|
+
width: var(--st-spin-inline);
|
154
|
+
height: var(--st-spin-inline);
|
155
|
+
background:no-repeat center/contain;
|
156
|
+
animation:st-fourframe var(--st-spin-dur) linear infinite;
|
157
|
+
z-index: calc(var(--st-overlay-z) + 1);
|
158
|
+
}}
|
159
|
+
|
160
|
+
/* Center the spinner message below the animation (works in sidebar or main) */
|
161
|
+
[data-testid="stSpinner"] [data-testid="stSpinnerMessage"],
|
162
|
+
[data-testid="stSpinner"] > div > div:last-child,
|
163
|
+
[data-testid="stSpinner"] > div > div:only-child {{
|
164
|
+
position: fixed !important;
|
165
|
+
left: 50% !important;
|
166
|
+
top: calc(50% + var(--st-spin-inline) / 2 + 12px) !important;
|
167
|
+
transform: translateX(-50%) !important;
|
168
|
+
z-index: calc(var(--st-overlay-z) + 2) !important;
|
169
|
+
text-align: center !important;
|
170
|
+
margin: 0 !important;
|
171
|
+
padding: .25rem .75rem !important;
|
172
|
+
max-width: min(80vw, 900px) !important; /* keeps long text from stretching off-screen */
|
173
|
+
white-space: normal !important; /* use `nowrap` if you prefer single-line */
|
174
|
+
font-weight: 500 !important;
|
175
|
+
}}
|
176
|
+
|
177
|
+
/* Kill the tiny default glyph wrapper so you don't get a stray dot in the sidebar */
|
178
|
+
[data-testid="stSpinner"] > div > div:first-child {{
|
179
|
+
display: none !important;
|
180
|
+
}}
|
181
|
+
|
182
|
+
/* We still hide the default SVG everywhere */
|
183
|
+
[data-testid="stSpinner"] svg {{
|
184
|
+
display: none !important;
|
185
|
+
}}
|
186
|
+
|
187
|
+
/* =======================================================================
|
188
|
+
3) st.status(...) icon — centered overlay
|
189
|
+
======================================================================= */
|
190
|
+
[data-testid="stStatus"] [data-testid="stStatusIcon"] svg,
|
191
|
+
[data-testid="stStatus"] [data-testid="stStatusIcon"] img {{ display:none !important; }}
|
192
|
+
{"[data-testid='stStatus']::after { content:''; position:fixed; inset:0; background:var(--st-overlay-bg); backdrop-filter: blur(var(--st-overlay-blur)); z-index: var(--st-overlay-z); pointer-events: none; }" if dim_backdrop else ""}
|
193
|
+
[data-testid="stStatus"] [data-testid="stStatusIcon"]::before {{
|
194
|
+
content:"";
|
195
|
+
position: fixed;
|
196
|
+
left: 50%;
|
197
|
+
top: 50%;
|
198
|
+
transform: translate(-50%,-50%);
|
199
|
+
width: var(--st-spin-inline); /* use same size as inline */
|
200
|
+
height: var(--st-spin-inline);
|
201
|
+
background:no-repeat center/contain;
|
202
|
+
animation:st-fourframe var(--st-spin-dur) linear infinite;
|
203
|
+
z-index: calc(var(--st-overlay-z) + 1);
|
204
|
+
}}
|
205
|
+
|
206
|
+
/* Optional: allow 'esc' feel without blocking clicks — achieved via pointer-events:none above. */
|
207
|
+
</style>
|
208
|
+
""", unsafe_allow_html=True)
|
209
|
+
|
210
|
+
|
211
|
+
|
212
|
+
|
File without changes
|