robotframework-events 0.1.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.
@@ -0,0 +1,248 @@
1
+ """Typed events of format v1, and the JSON Schema that describes one line of an events file.
2
+
3
+ The listener emits plain dicts, which is faster; these classes are for programs that read
4
+ events. :func:`from_dict` turns a decoded line into an event, :meth:`Event.to_dict` turns it
5
+ back. Unknown fields are ignored and unknown event types become :class:`UnknownEvent`, so
6
+ readers keep working when newer writers add to the format.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass, fields
14
+ from importlib import resources
15
+ from typing import Any, ClassVar
16
+
17
+ FORMAT_VERSION = 1
18
+ SCHEMA_FILE = "events-v1.schema.json"
19
+
20
+ Stats = dict[str, int]
21
+ """``{"total": n, "passed": n, "failed": n, "skipped": n}``"""
22
+
23
+
24
+ @dataclass(slots=True, kw_only=True)
25
+ class Event:
26
+ """Fields that every event has. ``type`` is the class attribute ``TYPE``."""
27
+
28
+ TYPE: ClassVar[str] = ""
29
+
30
+ v: int = FORMAT_VERSION
31
+ seq: int
32
+ t: float
33
+
34
+ @property
35
+ def type(self) -> str:
36
+ return self.TYPE
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ """The event as written to a file: envelope first, fields that are None left out."""
40
+ data: dict[str, Any] = {"v": self.v, "seq": self.seq, "t": self.t, "type": self.type}
41
+ for f in fields(self):
42
+ if f.name not in _ENVELOPE:
43
+ value = getattr(self, f.name)
44
+ if value is not None:
45
+ data[f.name] = value
46
+ return data
47
+
48
+
49
+ @dataclass(slots=True, kw_only=True)
50
+ class RunStarted(Event):
51
+ """``python``, ``pid`` and ``cwd`` are missing in events converted from a result file."""
52
+
53
+ TYPE: ClassVar[str] = "run.started"
54
+
55
+ run: str
56
+ rf: str
57
+ python: str | None = None
58
+ pid: int | None = None
59
+ cwd: str | None = None
60
+
61
+
62
+ @dataclass(slots=True, kw_only=True)
63
+ class RunEnded(Event):
64
+ TYPE: ClassVar[str] = "run.ended"
65
+
66
+ status: str
67
+ stats: Stats
68
+
69
+
70
+ @dataclass(slots=True, kw_only=True)
71
+ class SuiteStarted(Event):
72
+ TYPE: ClassVar[str] = "suite.started"
73
+
74
+ id: str
75
+ parent: str | None = None
76
+ name: str
77
+ longname: str
78
+ source: str | None = None
79
+ tests: int
80
+
81
+
82
+ @dataclass(slots=True, kw_only=True)
83
+ class SuiteEnded(Event):
84
+ TYPE: ClassVar[str] = "suite.ended"
85
+
86
+ id: str
87
+ status: str
88
+ message: str | None = None
89
+ elapsed: float
90
+ stats: Stats
91
+
92
+
93
+ @dataclass(slots=True, kw_only=True)
94
+ class TestStarted(Event):
95
+ TYPE: ClassVar[str] = "test.started"
96
+ __test__: ClassVar[bool] = False # not a pytest test class
97
+
98
+ id: str
99
+ parent: str
100
+ name: str
101
+ longname: str
102
+ source: str | None = None
103
+ lineno: int | None = None
104
+ tags: list[str] | None = None
105
+
106
+
107
+ @dataclass(slots=True, kw_only=True)
108
+ class TestEnded(Event):
109
+ TYPE: ClassVar[str] = "test.ended"
110
+ __test__: ClassVar[bool] = False
111
+
112
+ id: str
113
+ status: str
114
+ message: str | None = None
115
+ elapsed: float
116
+
117
+
118
+ @dataclass(slots=True, kw_only=True)
119
+ class StepStarted(Event):
120
+ """A keyword, setup, teardown or control structure. Which fields are set depends on ``kind``.
121
+
122
+ ``source`` is left out when it equals the parent's source. ``assign`` is a list of variables
123
+ for keywords and FOR, a ``{variable: value}`` dict for ITERATION and a string for EXCEPT.
124
+ """
125
+
126
+ TYPE: ClassVar[str] = "step.started"
127
+
128
+ id: str
129
+ parent: str | None = None
130
+ kind: str
131
+ source: str | None = None
132
+ lineno: int | None = None
133
+ name: str | None = None
134
+ owner: str | None = None
135
+ args: list[str] | None = None
136
+ assign: list[str] | dict[str, str] | str | None = None
137
+ flatten: bool | None = None
138
+ flavor: str | None = None
139
+ values: list[str] | None = None
140
+ condition: str | None = None
141
+ limit: str | None = None
142
+ patterns: list[str] | None = None
143
+ scope: str | None = None
144
+
145
+
146
+ @dataclass(slots=True, kw_only=True)
147
+ class StepEnded(Event):
148
+ TYPE: ClassVar[str] = "step.ended"
149
+
150
+ id: str
151
+ status: str
152
+ message: str | None = None
153
+ elapsed: float
154
+
155
+
156
+ @dataclass(slots=True, kw_only=True)
157
+ class MessageLogged(Event):
158
+ TYPE: ClassVar[str] = "message.logged"
159
+
160
+ parent: str | None = None
161
+ level: str
162
+ text: str | None = None
163
+ truncated: bool | None = None
164
+ html: bool | None = None
165
+
166
+
167
+ @dataclass(slots=True, kw_only=True)
168
+ class StatusChanged(Event):
169
+ """A suite teardown changed the status of a suite or test that already ended."""
170
+
171
+ TYPE: ClassVar[str] = "status.changed"
172
+
173
+ id: str
174
+ item: str
175
+ status: str
176
+ message: str | None = None
177
+
178
+
179
+ @dataclass(slots=True, kw_only=True)
180
+ class ResultFileCreated(Event):
181
+ TYPE: ClassVar[str] = "resultfile.created"
182
+
183
+ kind: str
184
+ path: str
185
+
186
+
187
+ @dataclass(slots=True, kw_only=True)
188
+ class UnknownEvent(Event):
189
+ """An event type this version does not know. ``data`` holds the fields besides the envelope."""
190
+
191
+ name: str
192
+ data: dict[str, Any]
193
+
194
+ @property
195
+ def type(self) -> str:
196
+ return self.name
197
+
198
+ def to_dict(self) -> dict[str, Any]:
199
+ return {"v": self.v, "seq": self.seq, "t": self.t, "type": self.name, **self.data}
200
+
201
+
202
+ EVENT_CLASSES: dict[str, type[Event]] = {
203
+ cls.TYPE: cls
204
+ for cls in (
205
+ RunStarted,
206
+ SuiteStarted,
207
+ TestStarted,
208
+ StepStarted,
209
+ MessageLogged,
210
+ StepEnded,
211
+ TestEnded,
212
+ SuiteEnded,
213
+ StatusChanged,
214
+ ResultFileCreated,
215
+ RunEnded,
216
+ )
217
+ }
218
+
219
+ _ENVELOPE = frozenset({"v", "seq", "t", "type"})
220
+ _FIELDS = {type_: frozenset(f.name for f in fields(cls)) - _ENVELOPE for type_, cls in EVENT_CLASSES.items()}
221
+
222
+
223
+ def from_dict(data: Mapping[str, Any]) -> Event:
224
+ """Builds the typed event for one decoded line.
225
+
226
+ Unknown fields are ignored; an unknown ``type`` gives an :class:`UnknownEvent`.
227
+ Raises ``ValueError`` when an envelope field or a required field is missing.
228
+ """
229
+ try:
230
+ envelope = {"v": data["v"], "seq": data["seq"], "t": data["t"]}
231
+ type_ = data["type"]
232
+ except KeyError as e:
233
+ raise ValueError(f"event without {e.args[0]!r}: {dict(data)}") from None
234
+ cls = EVENT_CLASSES.get(type_)
235
+ if cls is None:
236
+ return UnknownEvent(**envelope, name=type_, data={k: v for k, v in data.items() if k not in _ENVELOPE})
237
+ known = _FIELDS[type_]
238
+ try:
239
+ return cls(**envelope, **{k: v for k, v in data.items() if k in known})
240
+ except TypeError as e:
241
+ raise ValueError(f"invalid {type_} event: {e}") from None
242
+
243
+
244
+ def schema() -> dict[str, Any]:
245
+ """The JSON Schema (draft 2020-12) for one line of an events file, format v1. Returns a new dict."""
246
+ text = resources.files(__package__).joinpath(SCHEMA_FILE).read_text(encoding="utf-8")
247
+ result: dict[str, Any] = json.loads(text)
248
+ return result