oakscriptpy 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,82 @@
1
+ """Indicator factory — creates reusable indicator instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable
7
+
8
+ from ._types import Bar
9
+
10
+
11
+ @dataclass
12
+ class IndicatorMetadataConfig:
13
+ title: str
14
+ short_title: str | None = None
15
+ overlay: bool = False
16
+ format: str = "price"
17
+ precision: int = 2
18
+
19
+
20
+ @dataclass
21
+ class IndicatorContext:
22
+ data: list[Bar]
23
+ open: list[float]
24
+ high: list[float]
25
+ low: list[float]
26
+ close: list[float]
27
+ volume: list[float]
28
+ time: list[int]
29
+ pane_index: int = 0
30
+
31
+
32
+ class IndicatorInstance:
33
+ def __init__(self, metadata: IndicatorMetadataConfig, setup: Callable[[IndicatorContext], None]) -> None:
34
+ self.metadata = metadata
35
+ self._setup = setup
36
+ self._pane_index = 0 if metadata.overlay else 1
37
+ self._input_values: dict[str, Any] = {}
38
+
39
+ @property
40
+ def pane_index(self) -> int:
41
+ return self._pane_index
42
+
43
+ def is_overlay(self) -> bool:
44
+ return self.metadata.overlay
45
+
46
+ def update_inputs(self, inputs: dict[str, Any]) -> None:
47
+ self._input_values.update(inputs)
48
+
49
+ def get_input_values(self) -> dict[str, Any]:
50
+ return dict(self._input_values)
51
+
52
+ def calculate(self, data: list[Bar]) -> None:
53
+ ctx = IndicatorContext(
54
+ data=data,
55
+ open=[b.open for b in data],
56
+ high=[b.high for b in data],
57
+ low=[b.low for b in data],
58
+ close=[b.close for b in data],
59
+ volume=[b.volume for b in data],
60
+ time=[b.time for b in data],
61
+ pane_index=self._pane_index,
62
+ )
63
+ self._setup(ctx)
64
+
65
+
66
+ def indicator(
67
+ metadata: IndicatorMetadataConfig,
68
+ setup: Callable[[IndicatorContext], None],
69
+ ) -> Callable[[], IndicatorInstance]:
70
+ """Create an indicator factory function."""
71
+ normalized = IndicatorMetadataConfig(
72
+ title=metadata.title,
73
+ short_title=metadata.short_title or metadata.title,
74
+ overlay=metadata.overlay,
75
+ format=metadata.format,
76
+ precision=metadata.precision,
77
+ )
78
+
79
+ def factory() -> IndicatorInstance:
80
+ return IndicatorInstance(normalized, setup)
81
+
82
+ return factory
oakscriptpy/input_.py ADDED
@@ -0,0 +1,38 @@
1
+ """Input helper — creates InputValue dataclass instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class InputValue:
11
+ type: str
12
+ default_value: Any
13
+ value: Any
14
+ title: str | None = None
15
+ min: float | None = None
16
+ max: float | None = None
17
+ step: float | None = None
18
+ options: list[str] | None = None
19
+
20
+
21
+ def int_(default_value: int, title: str | None = None, min: int | None = None, max: int | None = None, step: int = 1) -> InputValue:
22
+ return InputValue(type="int", default_value=default_value, value=default_value, title=title, min=float(min) if min is not None else None, max=float(max) if max is not None else None, step=float(step))
23
+
24
+
25
+ def float_(default_value: float, title: str | None = None, min: float | None = None, max: float | None = None, step: float = 0.1) -> InputValue:
26
+ return InputValue(type="float", default_value=default_value, value=default_value, title=title, min=min, max=max, step=step)
27
+
28
+
29
+ def source(default_value: str = "close", title: str | None = None) -> InputValue:
30
+ return InputValue(type="source", default_value=default_value, value=default_value, title=title, options=["open", "high", "low", "close", "hl2", "hlc3", "ohlc4", "hlcc4"])
31
+
32
+
33
+ def bool_(default_value: bool, title: str | None = None) -> InputValue:
34
+ return InputValue(type="bool", default_value=default_value, value=default_value, title=title)
35
+
36
+
37
+ def string(default_value: str, title: str | None = None, options: list[str] | None = None) -> InputValue:
38
+ return InputValue(type="string", default_value=default_value, value=default_value, title=title, options=options)
oakscriptpy/inputs.py ADDED
@@ -0,0 +1,170 @@
1
+ """Input functions — mirrors PineScript input.* with idempotent semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from .runtime import get_context, recalculate
9
+ from .runtime_types import InputConfig
10
+
11
+ _registered_inputs: dict[str, bool] = {}
12
+ _auto_recalculate_enabled: bool = False
13
+ _input_counter: int = 0
14
+
15
+
16
+ def enable_auto_recalculate() -> None:
17
+ global _auto_recalculate_enabled
18
+ _auto_recalculate_enabled = True
19
+
20
+
21
+ def disable_auto_recalculate() -> None:
22
+ global _auto_recalculate_enabled
23
+ _auto_recalculate_enabled = False
24
+
25
+
26
+ def reset_inputs() -> None:
27
+ global _input_counter
28
+ _registered_inputs.clear()
29
+ _input_counter = 0
30
+
31
+
32
+ def _generate_input_id(title: str | None, defval: Any, type_: str) -> str:
33
+ global _input_counter
34
+ if title:
35
+ return title.replace(" ", "_")
36
+ id_ = f"{type_}_{_input_counter}_{defval}"
37
+ _input_counter += 1
38
+ return id_
39
+
40
+
41
+ def input_int(
42
+ defval: int, title: str | None = None,
43
+ min: int | None = None, max: int | None = None, step: int = 1,
44
+ ) -> int:
45
+ ctx = get_context()
46
+ if ctx is None:
47
+ return int(defval)
48
+
49
+ id_ = _generate_input_id(title, defval, "int")
50
+ config = InputConfig(
51
+ id=id_, type="int", defval=int(defval), title=title,
52
+ min=float(min) if min is not None else None,
53
+ max=float(max) if max is not None else None,
54
+ step=float(step),
55
+ )
56
+
57
+ if id_ not in _registered_inputs:
58
+ ctx.inputs.register_input(config)
59
+ _registered_inputs[id_] = True
60
+ if _auto_recalculate_enabled:
61
+ ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
62
+
63
+ value = ctx.inputs.get_value(id_)
64
+ return int(value) if isinstance(value, (int, float)) else int(defval)
65
+
66
+
67
+ def input_float(
68
+ defval: float, title: str | None = None,
69
+ min: float | None = None, max: float | None = None, step: float = 0.1,
70
+ ) -> float:
71
+ ctx = get_context()
72
+ if ctx is None:
73
+ return defval
74
+
75
+ id_ = _generate_input_id(title, defval, "float")
76
+ config = InputConfig(
77
+ id=id_, type="float", defval=defval, title=title,
78
+ min=min, max=max, step=step,
79
+ )
80
+
81
+ if id_ not in _registered_inputs:
82
+ ctx.inputs.register_input(config)
83
+ _registered_inputs[id_] = True
84
+ if _auto_recalculate_enabled:
85
+ ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
86
+
87
+ value = ctx.inputs.get_value(id_)
88
+ return float(value) if isinstance(value, (int, float)) else defval
89
+
90
+
91
+ def input_bool(defval: bool, title: str | None = None) -> bool:
92
+ ctx = get_context()
93
+ if ctx is None:
94
+ return defval
95
+
96
+ id_ = _generate_input_id(title, defval, "bool")
97
+ config = InputConfig(id=id_, type="bool", defval=defval, title=title)
98
+
99
+ if id_ not in _registered_inputs:
100
+ ctx.inputs.register_input(config)
101
+ _registered_inputs[id_] = True
102
+ if _auto_recalculate_enabled:
103
+ ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
104
+
105
+ value = ctx.inputs.get_value(id_)
106
+ return bool(value) if isinstance(value, bool) else defval
107
+
108
+
109
+ def input_string(
110
+ defval: str, title: str | None = None, options: list[str] | None = None,
111
+ ) -> str:
112
+ ctx = get_context()
113
+ if ctx is None:
114
+ return defval
115
+
116
+ id_ = _generate_input_id(title, defval, "string")
117
+ config = InputConfig(id=id_, type="string", defval=defval, title=title, options=options)
118
+
119
+ if id_ not in _registered_inputs:
120
+ ctx.inputs.register_input(config)
121
+ _registered_inputs[id_] = True
122
+ if _auto_recalculate_enabled:
123
+ ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
124
+
125
+ value = ctx.inputs.get_value(id_)
126
+ return str(value) if isinstance(value, str) else defval
127
+
128
+
129
+ def input_source(defval: str = "close", title: str | None = None) -> list[float]:
130
+ ctx = get_context()
131
+ if ctx is None:
132
+ return []
133
+
134
+ id_ = _generate_input_id(title, defval, "source")
135
+ config = InputConfig(
136
+ id=id_, type="source", defval=defval, title=title,
137
+ options=["open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4"],
138
+ )
139
+
140
+ if id_ not in _registered_inputs:
141
+ ctx.inputs.register_input(config)
142
+ _registered_inputs[id_] = True
143
+ if _auto_recalculate_enabled:
144
+ ctx.inputs.on_input_change(lambda cid, _v: recalculate() if cid == id_ else None)
145
+
146
+ source_name = ctx.inputs.get_value(id_)
147
+ if not isinstance(source_name, str):
148
+ source_name = defval
149
+
150
+ return _get_source_data(ctx.ohlcv, source_name)
151
+
152
+
153
+ def _get_source_data(ohlcv: Any, source_name: str) -> list[float]:
154
+ if source_name == "open":
155
+ return ohlcv.open
156
+ if source_name == "high":
157
+ return ohlcv.high
158
+ if source_name == "low":
159
+ return ohlcv.low
160
+ if source_name == "close":
161
+ return ohlcv.close
162
+ if source_name == "volume":
163
+ return ohlcv.volume
164
+ if source_name == "hl2":
165
+ return [(h + l) / 2 for h, l in zip(ohlcv.high, ohlcv.low)]
166
+ if source_name == "hlc3":
167
+ return [(h + l + c) / 3 for h, l, c in zip(ohlcv.high, ohlcv.low, ohlcv.close)]
168
+ if source_name == "ohlc4":
169
+ return [(o + h + l + c) / 4 for o, h, l, c in zip(ohlcv.open, ohlcv.high, ohlcv.low, ohlcv.close)]
170
+ return ohlcv.close
oakscriptpy/label.py ADDED
@@ -0,0 +1,110 @@
1
+ """Label namespace — mirrors PineScript label.* functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy as _copy
6
+
7
+ from ._types import Label
8
+
9
+
10
+ def new(
11
+ x: float, y: float, text: str | None = None,
12
+ xloc: str = "bar_index", yloc: str = "price",
13
+ color: str | int | None = None, style: str | None = None,
14
+ textcolor: str | int | None = None, size: str | int | None = None,
15
+ textalign: str | None = None, tooltip: str | None = None,
16
+ text_font_family: str | None = None,
17
+ ) -> Label:
18
+ return Label(
19
+ x=x, y=y, xloc=xloc, yloc=yloc, text=text, tooltip=tooltip,
20
+ color=color, style=style, textcolor=textcolor, size=size,
21
+ textalign=textalign, text_font_family=text_font_family,
22
+ )
23
+
24
+
25
+ def get_x(id: Label) -> float:
26
+ return id.x
27
+
28
+
29
+ def get_y(id: Label) -> float:
30
+ return id.y
31
+
32
+
33
+ def get_text(id: Label) -> str | None:
34
+ return id.text
35
+
36
+
37
+ def copy(id: Label) -> Label:
38
+ return _copy.copy(id)
39
+
40
+
41
+ def set_x(id: Label, x: float) -> Label:
42
+ id.x = x
43
+ return id
44
+
45
+
46
+ def set_y(id: Label, y: float) -> Label:
47
+ id.y = y
48
+ return id
49
+
50
+
51
+ def set_xy(id: Label, x: float, y: float) -> Label:
52
+ id.x = x
53
+ id.y = y
54
+ return id
55
+
56
+
57
+ def set_xloc(id: Label, x: float, xloc: str) -> Label:
58
+ id.x = x
59
+ id.xloc = xloc
60
+ return id
61
+
62
+
63
+ def set_yloc(id: Label, y: float, yloc: str) -> Label:
64
+ id.y = y
65
+ id.yloc = yloc
66
+ return id
67
+
68
+
69
+ def set_text(id: Label, text: str) -> Label:
70
+ id.text = text
71
+ return id
72
+
73
+
74
+ def set_tooltip(id: Label, tooltip: str) -> Label:
75
+ id.tooltip = tooltip
76
+ return id
77
+
78
+
79
+ def set_color(id: Label, color: str | int) -> Label:
80
+ id.color = color
81
+ return id
82
+
83
+
84
+ def set_textcolor(id: Label, color: str | int) -> Label:
85
+ id.textcolor = color
86
+ return id
87
+
88
+
89
+ def set_style(id: Label, style: str) -> Label:
90
+ id.style = style
91
+ return id
92
+
93
+
94
+ def set_size(id: Label, size: str | int) -> Label:
95
+ id.size = size
96
+ return id
97
+
98
+
99
+ def set_textalign(id: Label, align: str) -> Label:
100
+ id.textalign = align
101
+ return id
102
+
103
+
104
+ def set_text_font_family(id: Label, font: str) -> Label:
105
+ id.text_font_family = font
106
+ return id
107
+
108
+
109
+ def delete(id: Label) -> None:
110
+ pass
@@ -0,0 +1,5 @@
1
+ """Libraries."""
2
+
3
+ from .zigzag import ZigZag, calculate_zigzag
4
+
5
+ __all__ = ["ZigZag", "calculate_zigzag"]
@@ -0,0 +1,158 @@
1
+ """ZigZag library — identifies trend reversals via pivot highs and lows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from .._types import Bar
9
+
10
+
11
+ @dataclass
12
+ class ZigZagSettings:
13
+ dev_threshold: float = 5.0
14
+ depth: int = 10
15
+ line_color: str = "#2962FF"
16
+ extend_last: bool = True
17
+ display_reversal_price: bool = True
18
+ display_cumulative_volume: bool = True
19
+ display_reversal_price_change: bool = True
20
+ difference_price_mode: str = "Absolute"
21
+ allow_zigzag_on_one_bar: bool = True
22
+
23
+
24
+ @dataclass
25
+ class ZigZagPoint:
26
+ time: int
27
+ bar_index: int
28
+ price: float
29
+
30
+
31
+ @dataclass
32
+ class ZigZagPivot:
33
+ is_high: bool
34
+ volume: float
35
+ start: ZigZagPoint
36
+ end: ZigZagPoint
37
+
38
+
39
+ @dataclass
40
+ class ZigZagResult:
41
+ pivots: list[ZigZagPivot]
42
+ extension: ZigZagPivot | None
43
+
44
+
45
+ class ZigZag:
46
+ def __init__(self, settings: ZigZagSettings | None = None) -> None:
47
+ self.settings = settings or ZigZagSettings()
48
+ self.pivots: list[ZigZagPivot] = []
49
+ self.sum_vol: float = 0.0
50
+ self._high_buffer: list[float] = []
51
+ self._low_buffer: list[float] = []
52
+ self._time_buffer: list[int] = []
53
+ self._bar_count: int = 0
54
+
55
+ def update(self, bar: Bar, bar_index: int) -> bool:
56
+ self._high_buffer.append(bar.high)
57
+ self._low_buffer.append(bar.low)
58
+ self._time_buffer.append(bar.time)
59
+ self._bar_count += 1
60
+
61
+ depth = max(2, self.settings.depth // 2)
62
+ self.sum_vol += bar.volume
63
+
64
+ if self._bar_count < depth * 2 + 1:
65
+ return False
66
+
67
+ changed = self._try_find_pivot(True, depth, bar_index)
68
+ changed = self._try_find_pivot(
69
+ False, depth, bar_index,
70
+ self.settings.allow_zigzag_on_one_bar or not changed,
71
+ ) or changed
72
+ return changed
73
+
74
+ def last_pivot(self) -> ZigZagPivot | None:
75
+ return self.pivots[-1] if self.pivots else None
76
+
77
+ def get_extension(self, current_bar: Bar, bar_index: int) -> ZigZagPivot | None:
78
+ if not self.settings.extend_last:
79
+ return None
80
+ last = self.last_pivot()
81
+ if last is None:
82
+ return None
83
+ is_high = not last.is_high
84
+ price = current_bar.high if is_high else current_bar.low
85
+ return ZigZagPivot(
86
+ is_high=is_high, volume=self.sum_vol,
87
+ start=last.end, end=ZigZagPoint(time=current_bar.time, bar_index=bar_index, price=price),
88
+ )
89
+
90
+ def _try_find_pivot(self, is_high: bool, depth: int, current_bar_index: int, register: bool = True) -> bool:
91
+ point = self._find_pivot_point(is_high, depth, current_bar_index)
92
+ if point is None or not register:
93
+ return False
94
+ return self._new_pivot_found(is_high, point)
95
+
96
+ def _find_pivot_point(self, is_high: bool, depth: int, current_bar_index: int) -> ZigZagPoint | None:
97
+ buffer = self._high_buffer if is_high else self._low_buffer
98
+ pivot_idx = len(buffer) - 1 - depth
99
+ if pivot_idx < depth:
100
+ return None
101
+ pivot_price = buffer[pivot_idx]
102
+
103
+ for i in range(pivot_idx + 1, len(buffer)):
104
+ p = buffer[i]
105
+ if is_high and p > pivot_price:
106
+ return None
107
+ if not is_high and p < pivot_price:
108
+ return None
109
+
110
+ for i in range(pivot_idx - depth, pivot_idx):
111
+ p = buffer[i]
112
+ if is_high and p >= pivot_price:
113
+ return None
114
+ if not is_high and p <= pivot_price:
115
+ return None
116
+
117
+ return ZigZagPoint(
118
+ time=self._time_buffer[pivot_idx],
119
+ bar_index=current_bar_index - depth,
120
+ price=pivot_price,
121
+ )
122
+
123
+ def _calc_dev(self, base_price: float, price: float) -> float:
124
+ return 100 * (price - base_price) / abs(base_price) if base_price != 0 else 0.0
125
+
126
+ def _new_pivot_found(self, is_high: bool, point: ZigZagPoint) -> bool:
127
+ last = self.last_pivot()
128
+ if last is None:
129
+ self.pivots.append(ZigZagPivot(is_high=is_high, volume=self.sum_vol, start=point, end=point))
130
+ self.sum_vol = 0.0
131
+ return True
132
+
133
+ if last.is_high == is_high:
134
+ more_extreme = (point.price > last.end.price) if is_high else (point.price < last.end.price)
135
+ if more_extreme:
136
+ last.end = point
137
+ last.volume += self.sum_vol
138
+ self.sum_vol = 0.0
139
+ return True
140
+ else:
141
+ dev = self._calc_dev(last.end.price, point.price)
142
+ threshold = self.settings.dev_threshold
143
+ meets = (dev <= -threshold) if last.is_high else (dev >= threshold)
144
+ if meets:
145
+ self.pivots.append(ZigZagPivot(is_high=is_high, volume=self.sum_vol, start=last.end, end=point))
146
+ self.sum_vol = 0.0
147
+ return True
148
+ return False
149
+
150
+
151
+ def calculate_zigzag(bars: list[Bar], settings: ZigZagSettings | None = None) -> ZigZagResult:
152
+ if not bars:
153
+ return ZigZagResult(pivots=[], extension=None)
154
+ zz = ZigZag(settings)
155
+ for i, bar in enumerate(bars):
156
+ zz.update(bar, i)
157
+ extension = zz.get_extension(bars[-1], len(bars) - 1)
158
+ return ZigZagResult(pivots=zz.pivots, extension=extension)
oakscriptpy/line.py ADDED
@@ -0,0 +1,120 @@
1
+ """Line namespace — mirrors PineScript line.* functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy as _copy
6
+
7
+ from ._types import Line
8
+
9
+
10
+ def new(
11
+ x1: float, y1: float, x2: float, y2: float,
12
+ xloc: str = "bar_index", extend: str = "none",
13
+ color: str | int | None = None, style: str | None = None, width: int | None = None,
14
+ ) -> Line:
15
+ return Line(x1=x1, y1=y1, x2=x2, y2=y2, xloc=xloc, extend=extend,
16
+ color=color, style=style or "solid", width=width or 1)
17
+
18
+
19
+ def get_price(id: Line, x: float) -> float:
20
+ if id.xloc == "bar_time":
21
+ raise ValueError("line.get_price() only works with xloc.bar_index lines")
22
+ if id.x2 == id.x1:
23
+ return float("nan")
24
+ slope = (id.y2 - id.y1) / (id.x2 - id.x1)
25
+ price = id.y1 + slope * (x - id.x1)
26
+ lo = min(id.x1, id.x2)
27
+ hi = max(id.x1, id.x2)
28
+ if id.extend == "none":
29
+ if x < lo or x > hi:
30
+ return float("nan")
31
+ elif id.extend == "left":
32
+ if x > hi:
33
+ return float("nan")
34
+ elif id.extend == "right":
35
+ if x < lo:
36
+ return float("nan")
37
+ return price
38
+
39
+
40
+ def get_x1(id: Line) -> float:
41
+ return id.x1
42
+
43
+
44
+ def get_x2(id: Line) -> float:
45
+ return id.x2
46
+
47
+
48
+ def get_y1(id: Line) -> float:
49
+ return id.y1
50
+
51
+
52
+ def get_y2(id: Line) -> float:
53
+ return id.y2
54
+
55
+
56
+ def copy(id: Line) -> Line:
57
+ return _copy.copy(id)
58
+
59
+
60
+ def set_x1(id: Line, x: float) -> Line:
61
+ id.x1 = x
62
+ return id
63
+
64
+
65
+ def set_x2(id: Line, x: float) -> Line:
66
+ id.x2 = x
67
+ return id
68
+
69
+
70
+ def set_y1(id: Line, y: float) -> Line:
71
+ id.y1 = y
72
+ return id
73
+
74
+
75
+ def set_y2(id: Line, y: float) -> Line:
76
+ id.y2 = y
77
+ return id
78
+
79
+
80
+ def set_xy1(id: Line, x: float, y: float) -> Line:
81
+ id.x1 = x
82
+ id.y1 = y
83
+ return id
84
+
85
+
86
+ def set_xy2(id: Line, x: float, y: float) -> Line:
87
+ id.x2 = x
88
+ id.y2 = y
89
+ return id
90
+
91
+
92
+ def set_xloc(id: Line, x1: float, x2: float, xloc: str) -> Line:
93
+ id.x1 = x1
94
+ id.x2 = x2
95
+ id.xloc = xloc
96
+ return id
97
+
98
+
99
+ def set_extend(id: Line, extend: str) -> Line:
100
+ id.extend = extend
101
+ return id
102
+
103
+
104
+ def set_color(id: Line, color: str | int) -> Line:
105
+ id.color = color
106
+ return id
107
+
108
+
109
+ def set_style(id: Line, style: str) -> Line:
110
+ id.style = style
111
+ return id
112
+
113
+
114
+ def set_width(id: Line, width: int) -> Line:
115
+ id.width = width
116
+ return id
117
+
118
+
119
+ def delete(id: Line) -> None:
120
+ pass
@@ -0,0 +1,26 @@
1
+ """Linefill namespace — mirrors PineScript linefill.* functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._types import Linefill, Line
6
+
7
+
8
+ def new(line1: Line, line2: Line, color: str | int | None = None) -> Linefill:
9
+ return Linefill(line1=line1, line2=line2, color=color)
10
+
11
+
12
+ def get_line1(id: Linefill) -> Line:
13
+ return id.line1
14
+
15
+
16
+ def get_line2(id: Linefill) -> Line:
17
+ return id.line2
18
+
19
+
20
+ def set_color(id: Linefill, color: str | int) -> Linefill:
21
+ id.color = color
22
+ return id
23
+
24
+
25
+ def delete(id: Linefill) -> None:
26
+ pass