emlib 1.16.5__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.
emlib/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ **emlib** is a set of modules with miscellaneous functionality:
3
+
4
+ Repository: https://github.com/gesellkammer/emlib
5
+
6
+ Features
7
+ --------
8
+
9
+ - maths:
10
+ - base conversion, euclidian distance, min. common denominator, etc.
11
+ - combinatorics
12
+ - number theory
13
+ - number series
14
+ - markov chains
15
+ - utilities to traverse graphs
16
+ - matplotlib enhancements
17
+ - csv utilities
18
+ - iterator tools (similar to more_itertools)
19
+ - containers: RecordList (a list of named tuples)
20
+ - doctools: tools to generate documentation (for mkdocs particularly)
21
+ - etc
22
+ """
23
+
emlib/_dialogsqt.py ADDED
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+ import sys
3
+ import logging
4
+ import emlib.misc
5
+ from PyQt5 import QtWidgets, QtCore, QtGui
6
+ from PyQt5.QtCore import Qt
7
+ from typing import TYPE_CHECKING
8
+ if TYPE_CHECKING:
9
+ from typing import *
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ __all__ = (
15
+ 'FilteredList',
16
+ 'selectItem',
17
+ 'selectFile',
18
+ 'saveDialog',
19
+ 'showInfo'
20
+ )
21
+
22
+
23
+ def _makeApp():
24
+ app = QtWidgets.QApplication.instance()
25
+ if app is None:
26
+ logger.debug("Making new qt QApplication")
27
+ app = QtWidgets.QApplication([])
28
+ else:
29
+ logger.debug("Using existing QApplication instance")
30
+ return app
31
+
32
+
33
+ class _FilterEdit(QtWidgets.QLineEdit):
34
+ def __init__(self, parent: FilteredList, font:Tuple[str, int]=None):
35
+ self.parent = parent
36
+ super().__init__()
37
+ if font:
38
+ self.setFont(QtGui.QFont(*font))
39
+
40
+ def keyPressEvent(self, keyEvent):
41
+ k = keyEvent.key()
42
+ if k == Qt.Key_Return:
43
+ self.parent.accept()
44
+ elif k in (Qt.Key_Up, Qt.Key_Down):
45
+ self.parent.view.keyPressEvent(keyEvent)
46
+ elif k == Qt.Key_Escape:
47
+ self.parent.dismiss()
48
+ else:
49
+ # re = QRegularExpression(pattern, QRegularExpression.CaseInsensitiveOption | QRegularExpression.DotMatchesEverythingOption)
50
+ rx = QtCore.QRegularExpression(self.text(), QtCore.QRegularExpression.CaseInsensitiveOption | QtCore.QRegularExpression.DotMatchesEverythingOption)
51
+ self.parent.proxy_model.setFilterRegularExpression(rx)
52
+ self.parent.proxy_model.setFilterWildcard("*" + self.text() + "*")
53
+ super().keyPressEvent(keyEvent)
54
+
55
+
56
+ class _FilteredListView(QtWidgets.QListView):
57
+ def __init__(self, parent: FilteredList, font:Tuple[str, int]=None):
58
+ self.parent = parent
59
+ super().__init__()
60
+ if font:
61
+ self.setFont(QtGui.QFont(*font))
62
+
63
+ def keyPressEvent(self, keyEvent):
64
+ k = keyEvent.key()
65
+ if k == Qt.Key_Return:
66
+ self.parent.accept()
67
+ elif k == Qt.Key_Escape:
68
+ self.parent.dismiss()
69
+ else:
70
+ super().keyPressEvent(keyEvent)
71
+
72
+
73
+ def _calculateTextWidth(s: str, fontfamily: str, size: int) -> float:
74
+ f = QtGui.QFont(fontfamily, size)
75
+ fm = QtGui.QFontMetrics(f)
76
+ width = fm.horizontalAdvance(s)
77
+ return width
78
+
79
+
80
+ class FilteredList(QtWidgets.QMainWindow):
81
+ def __init__(self, items:Sequence[str], title:str,
82
+ listFont:Tuple[str, int]=None,
83
+ entryFont:Tuple[str, int]=None):
84
+ super().__init__()
85
+ self.out = None
86
+ self.view = _FilteredListView(self, font=listFont) # QListView()
87
+ self.model = QtCore.QStringListModel(items)
88
+ self.proxy_model = QtCore.QSortFilterProxyModel()
89
+ self.proxy_model.setSourceModel(self.model)
90
+ self.proxy_model.setSortCaseSensitivity(Qt.CaseInsensitive)
91
+ self.view.setModel(self.proxy_model)
92
+ self.setWindowTitle(title)
93
+ self.searchbar = _FilterEdit(self, font=entryFont) # QLineEdit()
94
+ # choose the type of search by connecting to a different slot here.
95
+ # see https://doc.qt.io/qt-5/qsortfilterproxymodel.html#public-slots
96
+ self.searchbar.textChanged.connect(self.proxy_model.setFilterFixedString)
97
+ minw = _calculateTextWidth(max(items, key=lambda s:len(s)), listFont[0], listFont[1])
98
+ self.setMinimumWidth(int(minw + 60))
99
+
100
+ layout = QtWidgets.QVBoxLayout()
101
+ layout.addWidget(self.searchbar)
102
+ layout.addWidget(self.view)
103
+
104
+ container = QtWidgets.QWidget()
105
+ container.setLayout(layout)
106
+ self.setCentralWidget(container)
107
+
108
+ def accept(self):
109
+ idx = self.view.currentIndex()
110
+ selection = idx.data()
111
+ if selection is None and self.proxy_model.rowCount():
112
+ selection = self.proxy_model.index(0, 0).data()
113
+ self.out = selection
114
+ self.close()
115
+
116
+ def dismiss(self):
117
+ self.out = None
118
+ self.close()
119
+
120
+
121
+ def selectItem(items: Sequence[str], title='Select',
122
+ listFont:Tuple[str, int]=None,
123
+ entryFont: Tuple[str, int]=None
124
+ ) -> Optional[str]:
125
+ app = _makeApp()
126
+ w = FilteredList(items, title=title, listFont=listFont, entryFont=entryFont)
127
+ w.show()
128
+ app.exec_()
129
+ return w.out
130
+
131
+
132
+ class _FilteredComboBox(QtWidgets.QComboBox):
133
+ def __init__(self, parent=None, title:str=''):
134
+ super().__init__(parent)
135
+ self.dismissed = False
136
+ if title:
137
+ self.setWindowTitle(title)
138
+
139
+ self.setFocusPolicy(Qt.StrongFocus)
140
+ self.setEditable(True)
141
+
142
+ # add a filter model to filter matching items
143
+ self.pFilterModel = QtCore.QSortFilterProxyModel(self)
144
+ self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
145
+ self.pFilterModel.setSourceModel(self.model())
146
+
147
+ # add a completer, which uses the filter model
148
+ self.completer = QtCore.QCompleter(self.pFilterModel, self)
149
+ # always show all (filtered) completions
150
+ self.completer.setCompletionMode(QtCore.QCompleter.UnfilteredPopupCompletion)
151
+ self.setCompleter(self.completer)
152
+
153
+ # connect signals
154
+ self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
155
+ self.completer.activated.connect(self.on_completer_activated)
156
+
157
+ # on selection of an item from the completer, select the corresponding item from combobox
158
+ def on_completer_activated(self, text):
159
+ if text:
160
+ index = self.findText(text)
161
+ self.setCurrentIndex(index)
162
+ self.activated[str].emit(self.itemText(index))
163
+
164
+ # on model change, update the models of the filter and completer as well
165
+ def setModel(self, model):
166
+ super().setModel(model)
167
+ self.pFilterModel.setSourceModel(model)
168
+ self.completer.setModel(self.pFilterModel)
169
+
170
+ # on model column change, update the model column of the filter and completer as well
171
+ def setModelColumn(self, column):
172
+ self.completer.setCompletionColumn(column)
173
+ self.pFilterModel.setFilterKeyColumn(column)
174
+ super().setModelColumn(column)
175
+
176
+ def keyPressEvent(self, e):
177
+ if e.key() == Qt.Key_Escape:
178
+ self.dismissed = True
179
+ self.close()
180
+ elif e.key() == Qt.Key_Enter or e.key() == Qt.Key_Return:
181
+ self.close()
182
+ else:
183
+ super().keyPressEvent(e)
184
+
185
+
186
+ def selectFromCombobox(items: List[str], title="Select", width=300, height=40
187
+ ) -> Optional[str]:
188
+ app = _makeApp()
189
+ combo = _FilteredComboBox(title=title)
190
+ # either fill the standard model of the combobox
191
+ # combo.addItems(options)
192
+
193
+ # or use another model
194
+ combo.setModel(QtCore.QStringListModel(items))
195
+
196
+ combo.resize(width, height)
197
+ combo.show()
198
+ app.exec_()
199
+ return None if combo.dismissed else combo.currentText()
200
+
201
+
202
+ def selectFile(directory:str=None, filter="All (*.*)", title="Open file") -> str:
203
+ app = _makeApp()
204
+ options = QtWidgets.QFileDialog.Options()
205
+ options |= QtWidgets.QFileDialog.DontUseNativeDialog
206
+ filter = filter.replace(",", " ")
207
+ name, mask = QtWidgets.QFileDialog.getOpenFileName(None, title, directory=directory,
208
+ filter=filter)
209
+ return name
210
+
211
+
212
+ def saveDialog(filter="All (*.*)", title="Save file", directory:str=None) -> str:
213
+ app = _makeApp()
214
+ filter = filter.replace(",", " ")
215
+ name, mask = QtWidgets.QFileDialog.getSaveFileName(None, title, filter=filter,
216
+ directory=directory)
217
+ return name
218
+
219
+
220
+ def showInfo(msg:str, title:str='Info', font:Tuple[str,int]=None, icon:str=None) -> None:
221
+ """
222
+ Open a message box with a text
223
+
224
+ Args:
225
+ msg: the text to display (one line)
226
+ title: the title of the dialog
227
+ font: if given, a tuple (fontfamily, size)
228
+ icon: either None or one of 'question', 'information', 'warning', 'critical'
229
+ """
230
+ app = _makeApp()
231
+ mbox = QtWidgets.QMessageBox()
232
+ mbox.setText(msg)
233
+ mbox.setBaseSize(QtCore.QSize(600, 120));
234
+ if title:
235
+ mbox.setWindowTitle(title)
236
+ if font:
237
+ mbox.setFont(QtCore.QFont(*font))
238
+ if icon:
239
+ if icon == 'question':
240
+ mbox.setIcon(QtWidgets.QMessageBox.Question)
241
+ elif icon == 'information':
242
+ mbox.setIcon(QtWidgets.QMessageBox.Information)
243
+ elif icon == 'warning':
244
+ mbox.setIcon(QtWidgets.QMessageBox.Warning)
245
+ elif icon == 'critical':
246
+ mbox.setIcon(QtWidgets.QMessageBox.Critical)
247
+ mbox.exec_()
248
+
249
+
250
+ # init
251
+ if sys.platform == 'darwin' and emlib.misc.inside_ipython() and not emlib.misc.inside_jupyter():
252
+ # macos needs loop integration inside ipython
253
+ ip = get_ipython()
254
+ if ip.active_eventloop is None:
255
+ logger.debug("Starting ipython/qt eventloop integration")
256
+ ip.run_line_magic('gui', 'qt')
257
+ elif ip.active_eventloop not in ('qt', 'qt5'):
258
+ logger.warning(
259
+ f"IPython has an active eventloop for {ip.active_eventloop}, but "
260
+ f"emlib.dialogs needs the qt eventloop to be able to open qt dialogs"
261
+ f" without blocking the shell. ")
262
+ ip.run_line_magic('gui', 'qt')
263
+
emlib/calculus.py ADDED
@@ -0,0 +1,189 @@
1
+ """
2
+ Calculus utilities, based on calculus.jl
3
+
4
+ """
5
+ from __future__ import annotations
6
+ import math
7
+ import struct as _struct
8
+ import random as _random
9
+ from typing import Callable
10
+
11
+ NAN = float('nan')
12
+ epsilon = math.ldexp(1.0, -53) # smallest double such that eps+0.5!=0.5
13
+ maxfloat = float(2**1024 - 2**971) # From the IEEE 754 standard
14
+ minfloat = math.ldexp(1.0, -1022) # min positive normalized double
15
+ smalleps = math.ldexp(1.0, -1074) # smallest increment for doubles < minfloat
16
+ infinity = math.ldexp(1.0, 1023) * 2
17
+
18
+
19
+ def nextafter(x:float, direction=1) -> float:
20
+ """
21
+ returns the next float after x in the direction indicated
22
+
23
+ if not possible, returns x
24
+
25
+ Args:
26
+ x: the value to evaluate
27
+ direction: if 1, the next float is searched upwards, otherwise downwards
28
+
29
+ Returns:
30
+ a next representable float from `x` in the direction indicated
31
+ """
32
+ if math.isnan(x) or math.isinf(x):
33
+ return x
34
+ # return small numbers for x very close to 0.0
35
+ if -minfloat < x < minfloat:
36
+ if direction > 0:
37
+ return x + smalleps
38
+ else:
39
+ return x - smalleps
40
+
41
+ # it looks like we have a normalized number
42
+ # break x down into a mantissa and exponent
43
+ m, e = math.frexp(x)
44
+
45
+ # all the special cases have been handled
46
+ if direction > 0:
47
+ m += epsilon
48
+ else:
49
+ m -= epsilon
50
+ return math.ldexp(m, e)
51
+
52
+
53
+ def eps(x:float) -> float:
54
+ """
55
+ Difference with the next representable float
56
+ """
57
+ if math.isinf(x):
58
+ return NAN
59
+ return abs(nextafter(x) - x)
60
+
61
+
62
+ def next_float_up(x:float) -> float:
63
+ """
64
+ Return the next representable float
65
+ """
66
+ # NaNs and positive infinity map to themselves.
67
+ if math.isnan(x) or (math.isinf(x) and x > 0):
68
+ return x
69
+
70
+ # 0.0 and -0.0 both map to the smallest +ve float.
71
+ if x == 0.0:
72
+ x = 0.0
73
+
74
+ n = _struct.unpack('<q', _struct.pack('<d', x))[0]
75
+ if n >= 0:
76
+ n += 1
77
+ else:
78
+ n -= 1
79
+ return _struct.unpack('<d', _struct.pack('<q', n))[0]
80
+
81
+
82
+ def next_float_down(x:float) -> float:
83
+ """
84
+ return the previous representable float
85
+ """
86
+ return -next_float_up(-x)
87
+
88
+
89
+ def next_toward(x:float, y:float) -> float:
90
+ """
91
+ return the next representable float between x and y
92
+ """
93
+ # If either argument is a NaN, return that argument.
94
+ # This matches the implementation in decimal.Decimal
95
+ if math.isnan(x):
96
+ return x
97
+ if math.isnan(y):
98
+ return y
99
+ if y == x:
100
+ return y
101
+ elif y > x:
102
+ return next_float_up(x)
103
+ else:
104
+ return next_float_down(x)
105
+
106
+
107
+ def cbrt(x:float) -> float:
108
+ """ cubic root """
109
+ return math.pow(x, 1.0 / 3)
110
+
111
+
112
+ def finite_difference_forward(func, x:float, h:float=None) -> float:
113
+ epsilon = math.sqrt(eps(max(1, abs(x)))) if h is None else h
114
+ # use machine-representable numbers
115
+ return (func(x + epsilon) - func(x)) / epsilon
116
+
117
+
118
+ def finite_difference_central(func, x:float, h:float=None) -> float:
119
+ epsilon = cbrt(eps(max(1, abs(x)))) if h is None else h
120
+ return (func(x + epsilon) - func(x - epsilon)) / (2 * epsilon)
121
+
122
+
123
+ def finite_difference(func, x:float, mode='central') -> float:
124
+ """
125
+ derivative of func at x
126
+ """
127
+ if mode == 'forward':
128
+ return finite_difference_forward(func, x)
129
+ elif mode == 'central':
130
+ return finite_difference_central(func, x)
131
+ else:
132
+ raise ValueError("mode must be 'forward' or 'central'")
133
+
134
+
135
+ def derivative(func) -> Callable[[float], float]:
136
+ """
137
+ return a new function representing the derivative of func
138
+ """
139
+ return lambda x: finite_difference_central(func, x)
140
+
141
+
142
+ def _integrate_adaptive_simpsons_inner(f:Callable, a:float, b:float, eps:float, S:float,
143
+ fa:float, fb:float, fc:float, bottom:float) -> float:
144
+ c = (a + b) / 2
145
+ h = b - a
146
+ d = (a + c) / 2
147
+ g = (c + b) / 2
148
+ fd = f(d)
149
+ fe = f(g)
150
+ Sleft = (h / 12) * (fa + 4 * fd + fc)
151
+ Sright = (h / 12) * (fc + 4 * fe + fb)
152
+ S2 = Sleft + Sright
153
+ if bottom <= 0 or abs(S2 - S) <= 15 * epsilon:
154
+ return S2 + (S2 - S) / 15
155
+ inner = _integrate_adaptive_simpsons_inner
156
+ return (
157
+ inner(f, a, c, eps/2, Sleft, fa, fc, fd, bottom - 1) +
158
+ inner(f, c, b, eps/2, Sright, fc, fb, fe, bottom - 1)
159
+ )
160
+
161
+
162
+ def integrate_adaptive_simpsons(f:Callable, a:float, b:float, accuracy=10e-10,
163
+ max_iterations=50) -> float:
164
+ c = (a + b) / 2
165
+ h = b - a
166
+ fa = f(a)
167
+ fb = f(b)
168
+ fc = f(c)
169
+ S = (h / 6) * (fa + 4 * fc + fb)
170
+ return _integrate_adaptive_simpsons_inner(f, a, b, accuracy,
171
+ S, fa, fb, fc, max_iterations)
172
+
173
+
174
+ def integrate_monte_carlo(f:Callable, a:float, b:float, iterations:int):
175
+ estimate = 0.0
176
+ width = (b - a)
177
+ for i in range(iterations):
178
+ x = width * _random.random() + a
179
+ estimate += f(x) * width
180
+ return estimate / iterations
181
+
182
+
183
+ def integrate(f:Callable, a:float, b:float, method='simpsons') -> float:
184
+ if method == 'simpsons':
185
+ return integrate_adaptive_simpsons(f, a, b)
186
+ elif method == 'montecarlo':
187
+ return integrate_monte_carlo(f, a, b, 10000)
188
+ else:
189
+ raise ValueError("Unknown method of integration")
emlib/classproperty.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ Very simple implementation of a class property
3
+
4
+ Example
5
+ ~~~~~~~
6
+
7
+ class Foo:
8
+ _active: Foo | None = None
9
+ _initdone = False
10
+
11
+ def __init__(self, bar=None):
12
+ self.bar = bar
13
+
14
+ @classmethod
15
+ def _initclass(self):
16
+ if Foo._initdone:
17
+ return
18
+ Foo._initdone = True
19
+ Foo._active = Foo()
20
+
21
+ @classproperty
22
+ def active(cls) -> Foo
23
+ assert Foo._initdone and Foo._active is not None
24
+ return Foo._active
25
+
26
+ Foo._initclass()
27
+ """
28
+
29
+
30
+ class classproperty(property):
31
+ def __get__(self, owner_self, owner_cls):
32
+ return self.fget(owner_cls)
emlib/colordata.py ADDED
@@ -0,0 +1,167 @@
1
+ """
2
+ These color definitions are taken from matplotlib._color_data.py
3
+
4
+ We include it here to be able to access color names without needing to import matplotlib
5
+ """
6
+
7
+ BASE_COLORS = {
8
+ 'b': (0, 0, 1), # blue
9
+ 'g': (0, 0.5, 0), # green
10
+ 'r': (1, 0, 0), # red
11
+ 'c': (0, 0.75, 0.75), # cyan
12
+ 'm': (0.75, 0, 0.75), # magenta
13
+ 'y': (0.75, 0.75, 0), # yellow
14
+ 'k': (0, 0, 0), # black
15
+ 'w': (1, 1, 1), # white
16
+ }
17
+
18
+ # https://drafts.csswg.org/css-color-4/#named-colors
19
+ CSS4_COLORS = {
20
+ 'aliceblue': '#F0F8FF',
21
+ 'antiquewhite': '#FAEBD7',
22
+ 'aqua': '#00FFFF',
23
+ 'aquamarine': '#7FFFD4',
24
+ 'azure': '#F0FFFF',
25
+ 'beige': '#F5F5DC',
26
+ 'bisque': '#FFE4C4',
27
+ 'black': '#000000',
28
+ 'blanchedalmond': '#FFEBCD',
29
+ 'blue': '#0000FF',
30
+ 'blueviolet': '#8A2BE2',
31
+ 'brown': '#A52A2A',
32
+ 'burlywood': '#DEB887',
33
+ 'cadetblue': '#5F9EA0',
34
+ 'chartreuse': '#7FFF00',
35
+ 'chocolate': '#D2691E',
36
+ 'coral': '#FF7F50',
37
+ 'cornflowerblue': '#6495ED',
38
+ 'cornsilk': '#FFF8DC',
39
+ 'crimson': '#DC143C',
40
+ 'cyan': '#00FFFF',
41
+ 'darkblue': '#00008B',
42
+ 'darkcyan': '#008B8B',
43
+ 'darkgoldenrod': '#B8860B',
44
+ 'darkgray': '#A9A9A9',
45
+ 'darkgreen': '#006400',
46
+ 'darkgrey': '#A9A9A9',
47
+ 'darkkhaki': '#BDB76B',
48
+ 'darkmagenta': '#8B008B',
49
+ 'darkolivegreen': '#556B2F',
50
+ 'darkorange': '#FF8C00',
51
+ 'darkorchid': '#9932CC',
52
+ 'darkred': '#8B0000',
53
+ 'darksalmon': '#E9967A',
54
+ 'darkseagreen': '#8FBC8F',
55
+ 'darkslateblue': '#483D8B',
56
+ 'darkslategray': '#2F4F4F',
57
+ 'darkslategrey': '#2F4F4F',
58
+ 'darkturquoise': '#00CED1',
59
+ 'darkviolet': '#9400D3',
60
+ 'deeppink': '#FF1493',
61
+ 'deepskyblue': '#00BFFF',
62
+ 'dimgray': '#696969',
63
+ 'dimgrey': '#696969',
64
+ 'dodgerblue': '#1E90FF',
65
+ 'firebrick': '#B22222',
66
+ 'floralwhite': '#FFFAF0',
67
+ 'forestgreen': '#228B22',
68
+ 'fuchsia': '#FF00FF',
69
+ 'gainsboro': '#DCDCDC',
70
+ 'ghostwhite': '#F8F8FF',
71
+ 'gold': '#FFD700',
72
+ 'goldenrod': '#DAA520',
73
+ 'gray': '#808080',
74
+ 'green': '#008000',
75
+ 'greenyellow': '#ADFF2F',
76
+ 'grey': '#808080',
77
+ 'honeydew': '#F0FFF0',
78
+ 'hotpink': '#FF69B4',
79
+ 'indianred': '#CD5C5C',
80
+ 'indigo': '#4B0082',
81
+ 'ivory': '#FFFFF0',
82
+ 'khaki': '#F0E68C',
83
+ 'lavender': '#E6E6FA',
84
+ 'lavenderblush': '#FFF0F5',
85
+ 'lawngreen': '#7CFC00',
86
+ 'lemonchiffon': '#FFFACD',
87
+ 'lightblue': '#ADD8E6',
88
+ 'lightcoral': '#F08080',
89
+ 'lightcyan': '#E0FFFF',
90
+ 'lightgoldenrodyellow': '#FAFAD2',
91
+ 'lightgray': '#D3D3D3',
92
+ 'lightgreen': '#90EE90',
93
+ 'lightgrey': '#D3D3D3',
94
+ 'lightpink': '#FFB6C1',
95
+ 'lightsalmon': '#FFA07A',
96
+ 'lightseagreen': '#20B2AA',
97
+ 'lightskyblue': '#87CEFA',
98
+ 'lightslategray': '#778899',
99
+ 'lightslategrey': '#778899',
100
+ 'lightsteelblue': '#B0C4DE',
101
+ 'lightyellow': '#FFFFE0',
102
+ 'lime': '#00FF00',
103
+ 'limegreen': '#32CD32',
104
+ 'linen': '#FAF0E6',
105
+ 'magenta': '#FF00FF',
106
+ 'maroon': '#800000',
107
+ 'mediumaquamarine': '#66CDAA',
108
+ 'mediumblue': '#0000CD',
109
+ 'mediumorchid': '#BA55D3',
110
+ 'mediumpurple': '#9370DB',
111
+ 'mediumseagreen': '#3CB371',
112
+ 'mediumslateblue': '#7B68EE',
113
+ 'mediumspringgreen': '#00FA9A',
114
+ 'mediumturquoise': '#48D1CC',
115
+ 'mediumvioletred': '#C71585',
116
+ 'midnightblue': '#191970',
117
+ 'mintcream': '#F5FFFA',
118
+ 'mistyrose': '#FFE4E1',
119
+ 'moccasin': '#FFE4B5',
120
+ 'navajowhite': '#FFDEAD',
121
+ 'navy': '#000080',
122
+ 'oldlace': '#FDF5E6',
123
+ 'olive': '#808000',
124
+ 'olivedrab': '#6B8E23',
125
+ 'orange': '#FFA500',
126
+ 'orangered': '#FF4500',
127
+ 'orchid': '#DA70D6',
128
+ 'palegoldenrod': '#EEE8AA',
129
+ 'palegreen': '#98FB98',
130
+ 'paleturquoise': '#AFEEEE',
131
+ 'palevioletred': '#DB7093',
132
+ 'papayawhip': '#FFEFD5',
133
+ 'peachpuff': '#FFDAB9',
134
+ 'peru': '#CD853F',
135
+ 'pink': '#FFC0CB',
136
+ 'plum': '#DDA0DD',
137
+ 'powderblue': '#B0E0E6',
138
+ 'purple': '#800080',
139
+ 'rebeccapurple': '#663399',
140
+ 'red': '#FF0000',
141
+ 'rosybrown': '#BC8F8F',
142
+ 'royalblue': '#4169E1',
143
+ 'saddlebrown': '#8B4513',
144
+ 'salmon': '#FA8072',
145
+ 'sandybrown': '#F4A460',
146
+ 'seagreen': '#2E8B57',
147
+ 'seashell': '#FFF5EE',
148
+ 'sienna': '#A0522D',
149
+ 'silver': '#C0C0C0',
150
+ 'skyblue': '#87CEEB',
151
+ 'slateblue': '#6A5ACD',
152
+ 'slategray': '#708090',
153
+ 'slategrey': '#708090',
154
+ 'snow': '#FFFAFA',
155
+ 'springgreen': '#00FF7F',
156
+ 'steelblue': '#4682B4',
157
+ 'tan': '#D2B48C',
158
+ 'teal': '#008080',
159
+ 'thistle': '#D8BFD8',
160
+ 'tomato': '#FF6347',
161
+ 'turquoise': '#40E0D0',
162
+ 'violet': '#EE82EE',
163
+ 'wheat': '#F5DEB3',
164
+ 'white': '#FFFFFF',
165
+ 'whitesmoke': '#F5F5F5',
166
+ 'yellow': '#FFFF00',
167
+ 'yellowgreen': '#9ACD32'}