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,412 @@
|
|
1
|
+
|
2
|
+
from .base import BasePydanticModel, BaseObjectOrm, TDAG_ENDPOINT
|
3
|
+
from .utils import (is_process_running, get_network_ip,
|
4
|
+
TDAG_CONSTANTS,
|
5
|
+
DATE_FORMAT, AuthLoaders, make_request, set_types_in_table, request_to_datetime, serialize_to_json, bios_uuid)
|
6
|
+
from typing import Optional, List,Annotated, Union, Literal
|
7
|
+
from pydantic import constr, Field,conint, BaseModel
|
8
|
+
import datetime
|
9
|
+
|
10
|
+
|
11
|
+
# Define a reusable HexColor annotated type
|
12
|
+
HexColor = Annotated[
|
13
|
+
str,
|
14
|
+
Field(
|
15
|
+
pattern=r"^#[0-9A-Fa-f]{6}$",
|
16
|
+
description="HEX color in format #RRGGBB",
|
17
|
+
min_length=7,
|
18
|
+
max_length=7,
|
19
|
+
)
|
20
|
+
]
|
21
|
+
|
22
|
+
|
23
|
+
class Theme(BasePydanticModel,BaseObjectOrm):
|
24
|
+
id: int
|
25
|
+
theme_type: constr(max_length=15) = Field(
|
26
|
+
..., description="‘standard’ or ‘custom’"
|
27
|
+
)
|
28
|
+
name: constr(max_length=100)
|
29
|
+
created: datetime.datetime
|
30
|
+
updated: datetime.datetime
|
31
|
+
|
32
|
+
mode: constr(max_length=5) = Field(
|
33
|
+
..., description="‘light’ or ‘dark’"
|
34
|
+
)
|
35
|
+
editor_background: Optional[dict] = Field(
|
36
|
+
None, description="FK to Background.id"
|
37
|
+
)
|
38
|
+
|
39
|
+
font_family_headings: Optional[constr(max_length=100)] = Field(
|
40
|
+
"", description="--font-family-headings"
|
41
|
+
)
|
42
|
+
font_family_paragraphs: Optional[constr(max_length=100)] = Field(
|
43
|
+
"", description="--font-family-paragraphs"
|
44
|
+
)
|
45
|
+
|
46
|
+
font_size_h1: conint(ge=1) = Field(48, description="--font-size-h1 (px)")
|
47
|
+
font_size_h2: conint(ge=1) = Field(40, description="--font-size-h2 (px)")
|
48
|
+
font_size_h3: conint(ge=1) = Field(32, description="--font-size-h3 (px)")
|
49
|
+
font_size_h4: conint(ge=1) = Field(24, description="--font-size-h4 (px)")
|
50
|
+
font_size_h5: conint(ge=1) = Field(20, description="--font-size-h5 (px)")
|
51
|
+
font_size_h6: conint(ge=1) = Field(16, description="--font-size-h6 (px)")
|
52
|
+
font_size_p: conint(ge=1) = Field(14, description="--font-size-p (px)")
|
53
|
+
|
54
|
+
primary_color: HexColor = Field("#0d6efd")
|
55
|
+
secondary_color: HexColor = Field("#6c757d")
|
56
|
+
accent_color_1: HexColor= Field("#198754")
|
57
|
+
accent_color_2: HexColor = Field("#ffc107")
|
58
|
+
heading_color: HexColor = Field("#212529")
|
59
|
+
paragraph_color: HexColor= Field("#495057")
|
60
|
+
light_paragraph_color: HexColor = Field("#6c757d")
|
61
|
+
background_color: HexColor = Field("#ffffff")
|
62
|
+
|
63
|
+
title_column_width: constr(max_length=15) = Field(
|
64
|
+
"150px", description="--title-column-width"
|
65
|
+
)
|
66
|
+
chart_label_font_size: conint(ge=1) = Field(
|
67
|
+
12, description="--chart-label-font-size (px)"
|
68
|
+
)
|
69
|
+
|
70
|
+
chart_palette_sequential: List[HexColor] = Field(
|
71
|
+
default_factory=list,
|
72
|
+
description="List of 5 HEX colours for sequential palettes",
|
73
|
+
)
|
74
|
+
chart_palette_diverging: List[HexColor] = Field(
|
75
|
+
default_factory=list,
|
76
|
+
description="List of 5 HEX colours for diverging palettes",
|
77
|
+
)
|
78
|
+
chart_palette_categorical: List[HexColor] = Field(
|
79
|
+
default_factory=list,
|
80
|
+
description="List of 6 HEX colours for categorical palettes",
|
81
|
+
)
|
82
|
+
|
83
|
+
def set_plotly_theme(self):
|
84
|
+
import plotly.io as pio
|
85
|
+
import plotly.graph_objects as go
|
86
|
+
try:
|
87
|
+
import plotly.express as px
|
88
|
+
except ImportError:
|
89
|
+
px = None
|
90
|
+
|
91
|
+
# --------------------- derive colours and fonts --------------------------
|
92
|
+
base_font = dict(
|
93
|
+
family=self.font_family_paragraphs or "Inter, Arial, sans-serif",
|
94
|
+
size=self.font_size_p,
|
95
|
+
color=self.paragraph_color,
|
96
|
+
)
|
97
|
+
title_font = dict(
|
98
|
+
family=self.font_family_headings or base_font["family"],
|
99
|
+
size=self.font_size_h4,
|
100
|
+
color=self.heading_color,
|
101
|
+
)
|
102
|
+
|
103
|
+
# categorical palette
|
104
|
+
colorway = (
|
105
|
+
self.chart_palette_categorical[:]
|
106
|
+
if len(self.chart_palette_categorical) >= 3
|
107
|
+
else [
|
108
|
+
self.primary_color,
|
109
|
+
self.secondary_color,
|
110
|
+
self.accent_color_1,
|
111
|
+
self.accent_color_2,
|
112
|
+
self.heading_color,
|
113
|
+
self.paragraph_color,
|
114
|
+
]
|
115
|
+
)
|
116
|
+
|
117
|
+
sequential = (
|
118
|
+
self.chart_palette_sequential[:]
|
119
|
+
if len(self.chart_palette_sequential) >= 3
|
120
|
+
else colorway
|
121
|
+
)
|
122
|
+
diverging = (
|
123
|
+
self.chart_palette_diverging[:]
|
124
|
+
if len(self.chart_palette_diverging) >= 3
|
125
|
+
else colorway[::-1]
|
126
|
+
)
|
127
|
+
|
128
|
+
paper_bg = plot_bg = self.background_color
|
129
|
+
if self.mode.lower() == "dark" and paper_bg.lower() == "#ffffff":
|
130
|
+
paper_bg = plot_bg = "#111111"
|
131
|
+
|
132
|
+
# --------------------------- build template ------------------------------
|
133
|
+
template = go.layout.Template(
|
134
|
+
layout=dict(
|
135
|
+
font=base_font,
|
136
|
+
title_font=title_font,
|
137
|
+
paper_bgcolor=paper_bg,
|
138
|
+
plot_bgcolor=plot_bg,
|
139
|
+
colorway=colorway,
|
140
|
+
# ↓↓↓ **correct spot for continuous scales** ↓↓↓
|
141
|
+
colorscale=dict(
|
142
|
+
sequential=sequential,
|
143
|
+
diverging=diverging,
|
144
|
+
),
|
145
|
+
xaxis=dict(showgrid=False, zeroline=False),
|
146
|
+
yaxis=dict(showgrid=True, zeroline=False),
|
147
|
+
)
|
148
|
+
)
|
149
|
+
|
150
|
+
# ---------------------- register & activate ------------------------------
|
151
|
+
tpl_name = f"theme_{self.id}_{self.name}"
|
152
|
+
pio.templates[tpl_name] = template
|
153
|
+
pio.templates.default = tpl_name
|
154
|
+
if px:
|
155
|
+
px.defaults.template = tpl_name
|
156
|
+
px.defaults.color_discrete_sequence = colorway
|
157
|
+
px.defaults.color_continuous_scale = sequential
|
158
|
+
|
159
|
+
|
160
|
+
|
161
|
+
class Folder(BasePydanticModel, BaseObjectOrm):
|
162
|
+
id:Optional[int]=None
|
163
|
+
name:str
|
164
|
+
slug:str
|
165
|
+
|
166
|
+
|
167
|
+
@classmethod
|
168
|
+
def get_or_create(cls, *args, **kwargs):
|
169
|
+
url = f"{cls.get_object_url()}/get-or-create/"
|
170
|
+
payload = {"json": kwargs}
|
171
|
+
r = make_request(
|
172
|
+
s=cls.build_session(),
|
173
|
+
loaders=cls.LOADERS,
|
174
|
+
r_type="POST",
|
175
|
+
url=url,
|
176
|
+
payload=payload
|
177
|
+
)
|
178
|
+
if r.status_code not in [200, 201]:
|
179
|
+
raise Exception(f"Error appending creating: {r.text}")
|
180
|
+
# Return a new instance of AssetCategory built from the response JSON.
|
181
|
+
return cls(**r.json())
|
182
|
+
|
183
|
+
class Slide(BasePydanticModel,BaseObjectOrm):
|
184
|
+
id:Optional[int]=None
|
185
|
+
number: int = Field(
|
186
|
+
...,
|
187
|
+
ge=0,
|
188
|
+
description="Number of the slide in presentation order"
|
189
|
+
)
|
190
|
+
body: str = Field(
|
191
|
+
...,
|
192
|
+
description=(
|
193
|
+
"Tiptap rich-text document for the main content, serialized as JSON. "
|
194
|
+
"Must follow the Tiptap ‘doc’ schema, e.g.: "
|
195
|
+
'{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}'
|
196
|
+
),
|
197
|
+
example='{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}'
|
198
|
+
)
|
199
|
+
header: Optional[str] = Field(
|
200
|
+
None,
|
201
|
+
description=(
|
202
|
+
"Optional Tiptap rich-text document for the header, serialized as JSON. "
|
203
|
+
"If present, should follow the Tiptap ‘doc’ schema, e.g.: "
|
204
|
+
'{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}'
|
205
|
+
),
|
206
|
+
example='{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}'
|
207
|
+
)
|
208
|
+
footer: Optional[str] = Field(
|
209
|
+
None,
|
210
|
+
description=(
|
211
|
+
"Optional Tiptap rich-text document for the footer, serialized as JSON. "
|
212
|
+
"If present, should follow the Tiptap ‘doc’ schema, e.g.: "
|
213
|
+
'{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}'
|
214
|
+
),
|
215
|
+
example='{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":null}}]}'
|
216
|
+
)
|
217
|
+
created_at: datetime.datetime = Field(
|
218
|
+
default_factory=datetime.datetime.utcnow,
|
219
|
+
description="Timestamp when the record was created"
|
220
|
+
)
|
221
|
+
updated_at: datetime.datetime = Field(
|
222
|
+
default_factory=datetime.datetime.utcnow,
|
223
|
+
description="Timestamp when the record was last updated"
|
224
|
+
)
|
225
|
+
custom_format: Optional[dict] = Field(description="Extra configuration for different type of slides")
|
226
|
+
|
227
|
+
|
228
|
+
class Presentation(BasePydanticModel, BaseObjectOrm):
|
229
|
+
id: int
|
230
|
+
folder: int
|
231
|
+
title: str = Field(..., max_length=255)
|
232
|
+
description: Optional[str] = Field("", description="Optional text")
|
233
|
+
created_at: datetime.datetime
|
234
|
+
updated_at: datetime.datetime
|
235
|
+
theme: Union[int,Theme]
|
236
|
+
slides:List[Slide]
|
237
|
+
|
238
|
+
|
239
|
+
|
240
|
+
@classmethod
|
241
|
+
def get_or_create_by_title(cls, *args, **kwargs):
|
242
|
+
url = f"{cls.get_object_url()}/get_or_create_by_title/"
|
243
|
+
payload = {"json": kwargs}
|
244
|
+
r = make_request(
|
245
|
+
s=cls.build_session(),
|
246
|
+
loaders=cls.LOADERS,
|
247
|
+
r_type="POST",
|
248
|
+
url=url,
|
249
|
+
payload=payload
|
250
|
+
)
|
251
|
+
if r.status_code not in [200, 201]:
|
252
|
+
raise Exception(f"Error appending creating: {r.text}")
|
253
|
+
# Return a new instance of AssetCategory built from the response JSON.
|
254
|
+
return cls(**r.json())
|
255
|
+
|
256
|
+
|
257
|
+
def add_slide(self, *args, **kwargs)->Slide:
|
258
|
+
url = f"{self.get_object_url()}/{self.id}/add_slide/"
|
259
|
+
|
260
|
+
|
261
|
+
r = make_request(
|
262
|
+
s=self.build_session(),
|
263
|
+
loaders=self.LOADERS,
|
264
|
+
r_type="POST",
|
265
|
+
url=url,payload={"json":{}}
|
266
|
+
)
|
267
|
+
if r.status_code not in [ 201]:
|
268
|
+
raise Exception(f"Error appending creating: {r.text}")
|
269
|
+
# Return a new instance of AssetCategory built from the response JSON.
|
270
|
+
return Slide(**r.json())
|
271
|
+
|
272
|
+
|
273
|
+
##########
|
274
|
+
|
275
|
+
class TextNode(BaseModel):
|
276
|
+
type: Literal["text"] = Field(..., description="Inline text node")
|
277
|
+
text: str = Field(..., description="Text content")
|
278
|
+
|
279
|
+
|
280
|
+
class TextParagraphAttrs(BaseModel):
|
281
|
+
textAlign: Optional[Literal["left", "right", "center", "justify"]] = Field(
|
282
|
+
None, description="Text alignment within the block"
|
283
|
+
)
|
284
|
+
level: Optional[int] = Field(
|
285
|
+
None, ge=1, le=6,
|
286
|
+
description="Heading level (1–6); only set when `type` == 'heading'"
|
287
|
+
)
|
288
|
+
|
289
|
+
|
290
|
+
class TextParagraph(BaseModel):
|
291
|
+
"""
|
292
|
+
Tiptap 'paragraph' or 'heading' block with inline text content.
|
293
|
+
"""
|
294
|
+
type: Literal["paragraph", "heading"] = Field(
|
295
|
+
..., description="Block type: 'paragraph' or 'heading'"
|
296
|
+
)
|
297
|
+
attrs: TextParagraphAttrs = Field(
|
298
|
+
default_factory=TextParagraphAttrs,
|
299
|
+
description="Block attributes (alignment and optional heading level)"
|
300
|
+
)
|
301
|
+
content: List[TextNode] = Field(
|
302
|
+
..., description="Inline text nodes"
|
303
|
+
)
|
304
|
+
|
305
|
+
@classmethod
|
306
|
+
def paragraph(
|
307
|
+
cls,
|
308
|
+
text: str,
|
309
|
+
text_align: Optional[Literal["left", "right", "center", "justify"]] = None
|
310
|
+
) -> "TextParagraph":
|
311
|
+
"""
|
312
|
+
Build a simple paragraph block:
|
313
|
+
|
314
|
+
TextParagraph.paragraph("Hello world", text_align="center")
|
315
|
+
"""
|
316
|
+
return cls(
|
317
|
+
type="paragraph",
|
318
|
+
attrs=TextParagraphAttrs(textAlign=text_align),
|
319
|
+
content=[TextNode(type="text", text=text)]
|
320
|
+
)
|
321
|
+
|
322
|
+
@classmethod
|
323
|
+
def heading(
|
324
|
+
cls,
|
325
|
+
text: str,
|
326
|
+
level: int = 1,
|
327
|
+
text_align: Optional[Literal["left", "right", "center", "justify"]] = None
|
328
|
+
) -> "TextParagraph":
|
329
|
+
"""
|
330
|
+
Build a heading block of the given level (1–6):
|
331
|
+
|
332
|
+
TextParagraph.heading("Title", level=2, text_align="center")
|
333
|
+
"""
|
334
|
+
return cls(
|
335
|
+
type="heading",
|
336
|
+
attrs=TextParagraphAttrs(textAlign=text_align, level=level),
|
337
|
+
content=[TextNode(type="text", text=text)]
|
338
|
+
)
|
339
|
+
|
340
|
+
class Config:
|
341
|
+
json_schema_extra = {
|
342
|
+
"examples": {
|
343
|
+
"paragraph": {
|
344
|
+
"summary": "Basic paragraph",
|
345
|
+
"value": {
|
346
|
+
"type": "paragraph",
|
347
|
+
"attrs": {"textAlign": None},
|
348
|
+
"content": [{"type": "text", "text": "This is a body paragraph."}]
|
349
|
+
}
|
350
|
+
},
|
351
|
+
"heading": {
|
352
|
+
"summary": "Centered H1 heading",
|
353
|
+
"value": {
|
354
|
+
"type": "heading",
|
355
|
+
"attrs": {"textAlign": "center", "level": 1},
|
356
|
+
"content": [{"type": "text", "text": "This is a heading"}]
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
}
|
361
|
+
|
362
|
+
### App Nodes
|
363
|
+
class EndpointProps(BaseModel):
|
364
|
+
props: dict[str, int] = Field(
|
365
|
+
...,
|
366
|
+
description="Dictionary of props to send to the endpoint, e.g. {'id': 33}",
|
367
|
+
example={"id": 33}
|
368
|
+
)
|
369
|
+
url: str = Field(
|
370
|
+
...,
|
371
|
+
description="Relative or absolute URL for the API endpoint",
|
372
|
+
example="/orm/api/reports/run-function/"
|
373
|
+
)
|
374
|
+
|
375
|
+
class AppNodeAttrs(BaseModel):
|
376
|
+
endpointProps: EndpointProps = Field(
|
377
|
+
...,
|
378
|
+
description="Configuration for the endpoint call"
|
379
|
+
)
|
380
|
+
|
381
|
+
class AppNode(BaseModel):
|
382
|
+
"""
|
383
|
+
Represents a custom Tiptap node of type 'appNode' that invokes an API.
|
384
|
+
"""
|
385
|
+
type: Literal["appNode"] = Field(
|
386
|
+
"appNode",
|
387
|
+
description="Node type identifier"
|
388
|
+
)
|
389
|
+
attrs: AppNodeAttrs = Field(
|
390
|
+
...,
|
391
|
+
description="Node attributes"
|
392
|
+
)
|
393
|
+
|
394
|
+
class Config:
|
395
|
+
json_schema_extra = {
|
396
|
+
"example": {
|
397
|
+
"type": "appNode",
|
398
|
+
"attrs": {
|
399
|
+
"endpointProps": {
|
400
|
+
"props": {"id": 33},
|
401
|
+
"url": "/orm/api/reports/run-function/"
|
402
|
+
}
|
403
|
+
}
|
404
|
+
}
|
405
|
+
}
|
406
|
+
@classmethod
|
407
|
+
def make_app_node(cls,id: int, url: str = "/orm/api/reports/run-function/") -> "AppNode":
|
408
|
+
return cls(
|
409
|
+
attrs=AppNodeAttrs(
|
410
|
+
endpointProps=EndpointProps(props={"id": id}, url=url)
|
411
|
+
)
|
412
|
+
)
|