robotcode-repl-server 0.100.2__tar.gz → 0.101.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: robotcode-repl-server
3
- Version: 0.100.2
3
+ Version: 0.101.0
4
4
  Summary: RobotCode REPL Server for Robot Framework
5
5
  Project-URL: Homepage, https://robotcode.io
6
6
  Project-URL: Donate, https://opencollective.com/robotcode
@@ -24,8 +24,8 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
24
24
  Classifier: Topic :: Utilities
25
25
  Classifier: Typing :: Typed
26
26
  Requires-Python: >=3.8
27
- Requires-Dist: robotcode-jsonrpc2==0.100.2
28
- Requires-Dist: robotcode-runner==0.100.2
27
+ Requires-Dist: robotcode-jsonrpc2==0.101.0
28
+ Requires-Dist: robotcode-runner==0.101.0
29
29
  Description-Content-Type: text/markdown
30
30
 
31
31
  # robotcode-repl-server
@@ -27,8 +27,8 @@ classifiers = [
27
27
  ]
28
28
  dynamic = ["version"]
29
29
  dependencies = [
30
- "robotcode-jsonrpc2==0.100.2",
31
- "robotcode-runner==0.100.2"
30
+ "robotcode-jsonrpc2==0.101.0",
31
+ "robotcode-runner==0.101.0"
32
32
  ]
33
33
 
34
34
  [project.entry-points.robotcode]
@@ -0,0 +1 @@
1
+ __version__ = "0.101.0"
@@ -1,16 +1,17 @@
1
+ import textwrap
1
2
  from dataclasses import dataclass, field
2
- from datetime import datetime
3
+ from datetime import datetime, timedelta
3
4
  from pathlib import Path
4
5
  from threading import Event
5
6
  from typing import TYPE_CHECKING, Iterator, List, Optional, Protocol, Union, runtime_checkable
6
- from uuid import uuid4
7
7
 
8
8
  from robot.running import Keyword
9
+ from robot.utils.markuputils import html_format
10
+ from robot.utils.robottime import elapsed_time_to_string
9
11
 
10
12
  from robotcode.core.utils.dataclasses import as_json
11
13
  from robotcode.repl.base_interpreter import BaseInterpreter, is_true
12
-
13
- from .html_writer import Element, create_keyword_html, create_message_html
14
+ from robotcode.robot.utils import get_robot_version
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from robot import result, running
@@ -76,13 +77,28 @@ class KeywordResultData(ResultData):
76
77
  message: str
77
78
  start_time: Optional[str]
78
79
  end_time: Optional[str]
79
- elapsed_time: Optional[float]
80
+ elapsed_time: Optional[str]
80
81
 
81
82
  items: List[ResultData] = field(default_factory=list)
82
83
 
83
84
  node_type: str = "keyword"
84
85
 
85
86
 
87
+ if get_robot_version() < (7, 0):
88
+
89
+ def make_elapsed_time_str(elapsed_time: Union[timedelta, int, float, None]) -> Optional[str]:
90
+ if elapsed_time is None:
91
+ return None
92
+ return str(elapsed_time_to_string(elapsed_time))
93
+
94
+ else:
95
+
96
+ def make_elapsed_time_str(elapsed_time: Union[timedelta, int, float, None]) -> Optional[str]:
97
+ if elapsed_time is None:
98
+ return None
99
+ return str(elapsed_time_to_string(elapsed_time, seconds=True))
100
+
101
+
86
102
  class Interpreter(BaseInterpreter):
87
103
  def __init__(
88
104
  self,
@@ -93,45 +109,24 @@ class Interpreter(BaseInterpreter):
93
109
  self.has_input = Event()
94
110
  self.executed = Event()
95
111
  self._code: List[str] = []
96
- self._html_result: Optional[Element] = None
97
- self._result_stack: List[Element] = []
98
- self._output_stack: List[Element] = []
99
- self._shadow_marker: Optional[str] = None
100
112
  self._success: Optional[bool] = None
101
113
  self._result_data: Optional[ResultData] = None
102
114
  self._result_data_stack: List[ResultData] = []
103
115
  self.collect_messages: bool = False
104
116
  self._has_shutdown = False
117
+ self._cell_errors: List[str] = []
105
118
 
106
119
  def shutdown(self) -> None:
107
120
  self._code = []
108
121
  self._has_shutdown = True
109
122
  self.has_input.set()
110
- # self.executed.set()
111
123
 
112
124
  def execute(self, source: str) -> ExecutionResult:
113
- self._result_stack = []
114
125
  self._result_data_stack = []
115
126
 
116
127
  self._success = None
117
128
  try:
118
- self._shadow_marker = str(uuid4())
119
-
120
- html_result = Element("div", classes=["robot-results"])
121
-
122
- with html_result.tag(
123
- "div", attributes={"data-shadow-marker": self._shadow_marker}, styles={"display": "none"}
124
- ):
125
- pass
126
-
127
- outer_test: Optional[Element] = None
128
- with html_result.tag("div", classes=["result_body"]) as body:
129
- with body.tag("div", classes=["test"]) as test:
130
- with test.tag("div", classes=["children"], styles={"display": "block"}):
131
- pass
132
- outer_test = test
133
-
134
- self._html_result = outer_test
129
+ self._cell_errors = []
135
130
  self._result_data = RootResultData()
136
131
 
137
132
  self.executed.clear()
@@ -148,12 +143,18 @@ class Interpreter(BaseInterpreter):
148
143
  ExecutionOutput(
149
144
  "x-application/robotframework-repl-log", as_json(self._result_data, compact=True)
150
145
  ),
151
- ExecutionOutput(
152
- "x-application/robotframework-repl-html", html_result.as_str(only_children=True)
146
+ *(
147
+ [ExecutionOutput("application/vnd.code.notebook.stderr", "\n".join(self._cell_errors))]
148
+ if self._cell_errors
149
+ else []
153
150
  ),
154
151
  ]
155
152
  if self._success is not None
156
- else []
153
+ else (
154
+ [ExecutionOutput("application/vnd.code.notebook.stderr", "\n".join(self._cell_errors))]
155
+ if self._cell_errors
156
+ else []
157
+ )
157
158
  ),
158
159
  )
159
160
  except BaseException as e:
@@ -164,7 +165,12 @@ class Interpreter(BaseInterpreter):
164
165
  s = self._code.pop(0)
165
166
  test, errors = self.get_test_body_from_string(s)
166
167
  if errors:
167
- raise CellInputError(errors)
168
+ self._cell_errors.append(
169
+ "CellInputError: " + ("\n" + textwrap.indent("\n".join(errors), " "))
170
+ if len(errors) > 1
171
+ else errors[0]
172
+ )
173
+ # raise CellInputError(errors)
168
174
 
169
175
  for kw in test.body:
170
176
  yield kw
@@ -183,29 +189,6 @@ class Interpreter(BaseInterpreter):
183
189
  )
184
190
  )
185
191
 
186
- if level in ("DEBUG", "TRACE"):
187
- return
188
-
189
- if self._html_result is None:
190
- return
191
-
192
- items = next(
193
- (
194
- i
195
- for i in self._html_result.children
196
- if isinstance(i, Element) and i.tag_name == "div" and i.classes is not None and "children" in i.classes
197
- ),
198
- None,
199
- )
200
- if items is None:
201
- items = Element("div", classes=["children"])
202
- self._html_result.add_element(items)
203
-
204
- id = f"message-{len(items.children)}"
205
-
206
- message_data = create_message_html(id, message, level, html, timestamp, shadow_root_id=self._shadow_marker)
207
- items.add_element(message_data)
208
-
209
192
  def message(
210
193
  self, message: str, level: str, html: Union[str, bool] = False, timestamp: Union[datetime, str, None] = None
211
194
  ) -> None:
@@ -232,7 +215,7 @@ class Interpreter(BaseInterpreter):
232
215
  name=result.name if getattr(result, "name", None) else "",
233
216
  owner=result.owner if getattr(result, "owner", None) else "",
234
217
  source_name=result.source_name if getattr(result, "source_name", None) else "",
235
- doc=result.doc if getattr(result, "doc", None) else "",
218
+ doc=html_format(result.doc) if getattr(result, "doc", None) else "",
236
219
  args=list(result.args) if getattr(result, "args", None) else [],
237
220
  assign=list(result.assign) if getattr(result, "assign", None) else [],
238
221
  tags=list(result.tags) if getattr(result, "tags", None) else [],
@@ -242,43 +225,29 @@ class Interpreter(BaseInterpreter):
242
225
  message=result.message,
243
226
  start_time=result.starttime,
244
227
  end_time=result.endtime,
245
- elapsed_time=result.elapsedtime,
228
+ elapsed_time=(
229
+ make_elapsed_time_str(result.elapsedtime)
230
+ if get_robot_version() < (7, 0)
231
+ else make_elapsed_time_str(result.elapsed_time)
232
+ ),
246
233
  )
247
234
  if self._result_data is not None and isinstance(self._result_data, ResultDataWithChildren):
248
235
  self._result_data.items.append(kw_data)
249
236
  self._result_data_stack.append(self._result_data)
250
237
  self._result_data = kw_data
251
238
 
252
- if self._html_result is not None:
253
- self._result_stack.append(self._html_result)
254
- kw = self.create_keyword_html_element(result)
255
-
256
- children = next(
257
- (
258
- i
259
- for i in self._html_result.children
260
- if isinstance(i, Element)
261
- and i.tag_name == "div"
262
- and i.classes is not None
263
- and "children" in i.classes
264
- ),
265
- None,
266
- )
267
-
268
- if children is None:
269
- self._html_result.add_element(kw)
270
- else:
271
- children.add_element(kw)
272
-
273
- self._html_result = kw
274
-
275
239
  def end_keyword(self, data: "running.Keyword", result: "result.Keyword") -> None:
276
240
  if data.type in ["IF/ELSE ROOT", "TRY/EXCEPT ROOT"]:
277
241
  return
242
+
278
243
  if self._result_data is not None:
279
244
  if isinstance(self._result_data, KeywordResultData):
280
245
  self._result_data.end_time = result.endtime
281
- self._result_data.elapsed_time = result.elapsedtime
246
+ self._result_data.elapsed_time = (
247
+ make_elapsed_time_str(result.elapsedtime)
248
+ if get_robot_version() < (7, 0)
249
+ else make_elapsed_time_str(result.elapsed_time)
250
+ )
282
251
  self._result_data.status = result.status
283
252
  self._result_data.message = result.message
284
253
 
@@ -289,69 +258,6 @@ class Interpreter(BaseInterpreter):
289
258
  elif result.status == "PASS" and self.last_result is not False:
290
259
  self._success = True
291
260
 
292
- if self._html_result is not None and isinstance(self._html_result, Element):
293
- kw = self.create_keyword_html_element(result)
294
-
295
- old_children = next(
296
- (
297
- i
298
- for i in self._html_result.children
299
- if isinstance(i, Element)
300
- and i.tag_name == "div"
301
- and i.classes is not None
302
- and "children" in i.classes
303
- ),
304
- None,
305
- )
306
- if old_children is not None:
307
- new_children = next(
308
- (
309
- i
310
- for i in kw.children
311
- if isinstance(i, Element)
312
- and i.tag_name == "div"
313
- and i.classes is not None
314
- and "children" in i.classes
315
- ),
316
- None,
317
- )
318
- if new_children is None:
319
- new_children = Element("div", classes=["children"])
320
- self._html_result.add_element(new_children)
321
-
322
- for old_child in old_children.children:
323
- if not (
324
- isinstance(old_child, Element)
325
- and old_child.tag_name == "table"
326
- and old_child.classes is not None
327
- and "metadata" in old_child.classes
328
- ):
329
- new_children.add_element(old_child)
330
-
331
- self._html_result.children = kw.children
332
-
333
- self._html_result = self._result_stack.pop()
334
-
335
- def create_keyword_html_element(self, result: "result.Keyword") -> Element:
336
- return create_keyword_html(
337
- id=result.id,
338
- name=result.name if getattr(result, "name", None) else None,
339
- owner=result.owner if getattr(result, "owner", None) else None,
340
- source_name=result.source_name if getattr(result, "source_name", None) else None,
341
- doc=result.doc if getattr(result, "doc", None) else None,
342
- args=result.args if getattr(result, "args", None) else (),
343
- assign=result.assign if getattr(result, "assign", None) else (),
344
- tags=result.tags if getattr(result, "tags", None) else (),
345
- timeout=result.timeout if getattr(result, "timeout", None) else None,
346
- type=result.type,
347
- status=result.status,
348
- message=result.message,
349
- start_time=result.starttime,
350
- end_time=result.endtime,
351
- elapsed_time=result.elapsedtime,
352
- shadow_root_id=self._shadow_marker,
353
- )
354
-
355
261
  def run_input(self) -> None:
356
262
  self.has_input.wait()
357
263
  if self._has_shutdown:
@@ -1 +0,0 @@
1
- __version__ = "0.100.2"
@@ -1,324 +0,0 @@
1
- from contextlib import contextmanager
2
- from datetime import datetime, timedelta
3
- from typing import Any, Dict, Generator, List, Optional, Sequence, TypeVar, Union
4
-
5
- from robot.utils.robottime import elapsed_time_to_string
6
-
7
- from robotcode.repl.base_interpreter import is_true
8
- from robotcode.robot.utils import get_robot_version
9
-
10
-
11
- class ElementDataBase:
12
- def as_str(self, indent: int = 0) -> str:
13
- raise NotImplementedError
14
-
15
- def __str__(self) -> str:
16
- return self.as_str(0)
17
-
18
-
19
- _T = TypeVar("_T", bound=ElementDataBase)
20
-
21
-
22
- class TextElement(ElementDataBase):
23
- def __init__(self, text: str) -> None:
24
- self.text = text
25
-
26
- def as_str(self, indent: int = 0) -> str:
27
- return f"{' '*indent}{self.text}"
28
-
29
-
30
- class RawElement(ElementDataBase):
31
- def __init__(self, text: str) -> None:
32
- self.text = text
33
-
34
- def as_str(self, indent: int = 0) -> str:
35
- return f"{' '*indent}{self.text}"
36
-
37
-
38
- class Element(ElementDataBase):
39
- def __init__(
40
- self,
41
- tag_name: str,
42
- text: Optional[str] = None,
43
- classes: Optional[List[str]] = None,
44
- styles: Optional[Dict[str, str]] = None,
45
- attributes: Optional[Dict[str, str]] = None,
46
- **kwargs: Any,
47
- ) -> None:
48
- self.tag_name = tag_name
49
- self.classes = classes
50
- self.styles = styles
51
- if attributes is None:
52
- attributes = {}
53
- attributes.update(kwargs)
54
- self.attributes = attributes
55
- self.children: List[ElementDataBase] = []
56
-
57
- if text is not None:
58
- self.add_element(TextElement(text))
59
-
60
- def add_element(self, child: _T) -> _T:
61
- self.children.append(child)
62
- return child
63
-
64
- @contextmanager
65
- def tag(
66
- self,
67
- tag_name: str,
68
- *,
69
- text: Optional[str] = None,
70
- classes: Optional[List[str]] = None,
71
- styles: Optional[Dict[str, str]] = None,
72
- attributes: Optional[Dict[str, str]] = None,
73
- **kwargs: Any,
74
- ) -> Generator["Element", None, None]:
75
- element = Element(tag_name, text=text, classes=classes, styles=styles, attributes=attributes, **kwargs)
76
-
77
- yield element
78
-
79
- self.add_element(element)
80
-
81
- def add_raw(self, text: Any) -> None:
82
- self.add_element(RawElement(str(text)))
83
-
84
- def add_text(self, text: Any) -> None:
85
- self.add_element(TextElement(str(text)))
86
-
87
- def _build_attributes(self) -> str:
88
- result = []
89
- if self.classes:
90
- result.append(f'class="{" ".join(s for s in self.classes if s)}"')
91
- if self.styles:
92
- result.append(f'style="{"; ".join(f"{k}: {v}" for k, v in self.styles.items() if v)}"')
93
- if self.attributes:
94
- result.extend(f'{k}="{v}"' for k, v in self.attributes.items())
95
-
96
- return " ".join(result)
97
-
98
- NON_BREAKABLE_TAGS = {"span", "a", "b", "i", "u", "strong", "em", "code", "pre", "tt", "samp", "kbd", "var", "td"}
99
-
100
- def as_str(self, indent: int = 0, *, only_children: bool = False) -> str:
101
- if not only_children:
102
- attributes = self._build_attributes()
103
-
104
- if attributes:
105
- start_tag = f"<{self.tag_name} {attributes}>"
106
- else:
107
- start_tag = f"<{self.tag_name}>"
108
-
109
- end_tag = f"</{self.tag_name}>"
110
-
111
- result = " " * indent + start_tag
112
- else:
113
- result = ""
114
- end_tag = None
115
-
116
- if self.children:
117
- result += "\n" if self.tag_name not in self.NON_BREAKABLE_TAGS else ""
118
- for child in self.children:
119
- result += child.as_str((indent + 1) if self.tag_name not in self.NON_BREAKABLE_TAGS else 0) + (
120
- "\n" if self.tag_name not in self.NON_BREAKABLE_TAGS else ""
121
- )
122
- result += (" " * indent) if self.tag_name not in self.NON_BREAKABLE_TAGS else ""
123
-
124
- if end_tag:
125
- result += end_tag
126
-
127
- return result
128
-
129
-
130
- def create_keyword_html(
131
- id: Optional[str] = None,
132
- name: Optional[str] = "",
133
- owner: Optional[str] = None,
134
- source_name: Optional[str] = None,
135
- doc: Optional[str] = "",
136
- args: Sequence[str] = (),
137
- assign: Sequence[str] = (),
138
- tags: Sequence[str] = (),
139
- timeout: Optional[str] = None,
140
- type: str = "KEYWORD",
141
- status: str = "FAIL",
142
- message: str = "",
143
- start_time: Union[datetime, str, None] = None,
144
- end_time: Union[datetime, str, None] = None,
145
- elapsed_time: Union[timedelta, int, float, None] = None,
146
- shadow_root_id: Optional[str] = None,
147
- ) -> Element:
148
- result = Element("div", classes=["keyword"], id=id)
149
-
150
- elapsed_time_str = (
151
- (
152
- elapsed_time_to_string(elapsed_time)
153
- if get_robot_version() < (7, 0)
154
- else elapsed_time_to_string(elapsed_time, seconds=True)
155
- )
156
- if elapsed_time is not None
157
- else ""
158
- )
159
-
160
- with result.tag(
161
- "div",
162
- classes=["element-header", "closed" if status not in ["FAIL"] else ""],
163
- onclick=f"toggleKeyword('{id}', '{shadow_root_id}')",
164
- ) as e_element_header:
165
- with e_element_header.tag(
166
- "div",
167
- classes=["element-header-left"],
168
- title=f"{type.upper()} {owner}.{name} [{status}]",
169
- ) as e_header_left:
170
- if elapsed_time is not None:
171
- with e_header_left.tag("span", classes=["elapsed"]) as e_elapsed:
172
- e_elapsed.add_text(elapsed_time_str)
173
- with e_header_left.tag("span", classes=["label", status.lower()]) as e_label:
174
- e_label.add_text(str(type).upper())
175
- with e_header_left.tag("span", classes=["assign"]) as e_assign:
176
- e_assign.add_text(" ".join(assign))
177
- with e_header_left.tag("span", classes=["name"]) as e_name:
178
- with e_name.tag("span", classes=["parent-name"]) as parent_name:
179
- parent_name.add_text((owner + " . ") if owner else "")
180
- e_name.add_text(name)
181
- e_header_left.add_raw("&nbsp;")
182
- with e_header_left.tag("span", classes=["arg"]) as args_tag:
183
- args_tag.add_text(" ".join(args))
184
- with e_element_header.tag("div", classes=["element-header-right"]) as e_header_right:
185
- with e_header_right.tag(
186
- "div",
187
- classes=["expand"],
188
- title="Expand all",
189
- onclick=f"expandAll(event, '{id}', '{shadow_root_id}')",
190
- ):
191
- pass
192
- with e_header_right.tag(
193
- "div",
194
- classes=["collapse"],
195
- title="Collapse all",
196
- onclick=f"collapseAll(event, '{id}', '{shadow_root_id}')",
197
- ):
198
- pass
199
- with e_header_right.tag(
200
- "div",
201
- classes=["link"],
202
- title="Highlight this item",
203
- onclick=f"makeElementVisible(event, '{id}', '{shadow_root_id}')",
204
- ):
205
- pass
206
- with e_element_header.tag("div", classes=["element-header-toggle"], title="Toggle visibility"):
207
- pass
208
-
209
- with result.tag(
210
- "div", classes=["children", "populated"], styles={"display": "none" if status not in ["FAIL"] else "block"}
211
- ) as e_children:
212
- with e_children.tag("table", classes=["metadata", "keyword-metadata"]) as e_body:
213
- if doc:
214
- with e_body.tag("tr") as tr:
215
- with tr.tag("th", text="Documentation:"):
216
- pass
217
- with tr.tag("td", classes=["doc"]) as td:
218
- td.add_text(doc)
219
- if tags:
220
- with e_body.tag("tr") as tr:
221
- with tr.tag("th", text="Tags:"):
222
- pass
223
- with tr.tag("td", classes=["tags"]) as td:
224
- td.add_text(", ".join(tags))
225
- if timeout:
226
- with e_body.tag("tr") as tr:
227
- with tr.tag("th", text="Timeout:"):
228
- pass
229
- with tr.tag("td", classes=["timeout"]) as td:
230
- td.add_text(timeout)
231
- if source_name:
232
- with e_body.tag("tr") as tr:
233
- with tr.tag("th", text="Source:"):
234
- pass
235
- with tr.tag("td", classes=["source"]) as td:
236
- td.add_text(source_name)
237
- with e_body.tag("tr") as tr:
238
- with tr.tag("th", text="Start / End / Elapsed:"):
239
- pass
240
- with tr.tag("td", classes=["message"]) as td:
241
- td.add_text(str(start_time) + " / " + str(end_time) + " / " + elapsed_time_str)
242
- if message:
243
- with e_body.tag("tr") as tr:
244
- with tr.tag("th", text="Message:"):
245
- pass
246
- with tr.tag("td", classes=["message"]) as td:
247
- td.add_text(message)
248
-
249
- return result
250
-
251
-
252
- def create_message_html(
253
- id: str,
254
- message: str,
255
- level: str,
256
- html: Union[str, bool] = False,
257
- timestamp: Union[datetime, str, None] = None,
258
- shadow_root_id: Optional[str] = None,
259
- ) -> Element:
260
- result = Element("table", classes=["messages", f"{level.lower()}-message"], id=id)
261
-
262
- with result.tag("tr", classes=["message-row"]) as tr:
263
- with tr.tag("td", classes=["time"]) as td:
264
- if isinstance(timestamp, datetime):
265
- td.add_text(timestamp.strftime("%H:%M:%S"))
266
- else:
267
- td.add_text(timestamp)
268
- with tr.tag("td", classes=["level", level.lower()]) as td:
269
- with td.tag("span", classes=["label", level.lower()]) as sp:
270
- sp.add_text(level.upper())
271
- with tr.tag("td", classes=["message"]) as td:
272
- if is_true(html):
273
- td.add_raw(message)
274
- else:
275
- td.add_text(message)
276
- with tr.tag(
277
- "td",
278
- classes=["select-message"],
279
- onclick=f"selectMessage('{id}', '{shadow_root_id}')",
280
- title="Select message text",
281
- ) as td:
282
- with td.tag("div"):
283
- pass
284
-
285
- return result
286
-
287
-
288
- if __name__ == "__main__":
289
- # div = Element("div", id="main", classes=["test", "test2"], styles={"color": "red", "font-size": "12px"})
290
- # p = div.add_element(Element("p", text="Hello, world!"))
291
- # p.add_element(Element("span", text="This is a test"))
292
- # print(div)
293
-
294
- # body = Element("body")
295
- # with body.tag("div", id="main", classes=["test", "test2"], styles={"color": "red"}) as div:
296
- # with div.tag("p") as p:
297
- # p.add_text("Hello, world!")
298
- # with p.tag("span") as span:
299
- # span.add_text("This is a test")
300
- # with span.tag("br"):
301
- # pass
302
- # span.add_text("This is a test")
303
- # with span.tag("br", id="test"):
304
- # pass
305
- # print(body)
306
-
307
- # r = create_keyword_html(
308
- # id="ts-1-2-3-4-5-6",
309
- # name="Test Keyword",
310
- # owner="Test Library",
311
- # doc="This is a test keyword",
312
- # args=("arg1", "arg2"),
313
- # assign=("${var1}", "${var2}"),
314
- # tags=("tag1", "tag2"),
315
- # timeout="10s",
316
- # type="KEYWORD",
317
- # status="PASS",
318
- # message="This is a test message",
319
- # start_time=datetime.now(timezone.utc),
320
- # end_time=datetime.now(timezone.utc),
321
- # elapsed_time=timedelta(seconds=5),
322
- # )
323
- r = create_message_html("ts-1-2-3-4-5-6", "This is a test message", "INFO", timestamp="2021-10-10 12:00:00")
324
- print(r)