DefStream 0.1.13__44-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.
Files changed (56) hide show
  1. DefStream/Broker/Configuration.py +18 -0
  2. DefStream/Broker/Connection.py +141 -0
  3. DefStream/Broker/Consumer.py +59 -0
  4. DefStream/Broker/Errors.py +14 -0
  5. DefStream/Broker/Event.py +35 -0
  6. DefStream/Broker/Implements/__init__.py +1 -0
  7. DefStream/Broker/Policies.py +25 -0
  8. DefStream/Broker/Priority.py +26 -0
  9. DefStream/Broker/Protocol.py +45 -0
  10. DefStream/Broker/__init__.py +59 -0
  11. DefStream/Descriptor/DataObject.py +409 -0
  12. DefStream/Descriptor/Types.py +248 -0
  13. DefStream/Descriptor/Value.py +188 -0
  14. DefStream/Descriptor/__init__.py +172 -0
  15. DefStream/Inspector/Extension.py +69 -0
  16. DefStream/Inspector/Extensions/Console.html +373 -0
  17. DefStream/Inspector/Extensions/Console.py +415 -0
  18. DefStream/Inspector/Extensions/Dashboard.html +206 -0
  19. DefStream/Inspector/Extensions/Dashboard.py +124 -0
  20. DefStream/Inspector/Extensions/HealthCheck.py +107 -0
  21. DefStream/Inspector/Extensions/Metrics.py +316 -0
  22. DefStream/Inspector/Extensions/Specifications.py +176 -0
  23. DefStream/Inspector/Extensions/__init__.py +13 -0
  24. DefStream/Inspector/Inspector.py +347 -0
  25. DefStream/Inspector/Response.py +14 -0
  26. DefStream/Inspector/__init__.py +9 -0
  27. DefStream/Validator/Pattern.py +36 -0
  28. DefStream/Validator/Patterns/Array.py +30 -0
  29. DefStream/Validator/Patterns/Common.py +93 -0
  30. DefStream/Validator/Patterns/Compare.py +180 -0
  31. DefStream/Validator/Patterns/Condition.py +60 -0
  32. DefStream/Validator/Patterns/DataObject.py +85 -0
  33. DefStream/Validator/Patterns/DateTime.py +111 -0
  34. DefStream/Validator/Patterns/Number.py +43 -0
  35. DefStream/Validator/Patterns/Object.py +150 -0
  36. DefStream/Validator/Patterns/Size.py +110 -0
  37. DefStream/Validator/Patterns/String.py +85 -0
  38. DefStream/Validator/Patterns/Type.py +350 -0
  39. DefStream/Validator/Patterns/__init__.py +137 -0
  40. DefStream/Validator/Validate.py +29 -0
  41. DefStream/Validator/Validator.py +30 -0
  42. DefStream/Validator/__init__.py +11 -0
  43. DefStream/Worker/Executor.py +371 -0
  44. DefStream/Worker/Message.py +180 -0
  45. DefStream/Worker/Pool.py +245 -0
  46. DefStream/Worker/Run.py +357 -0
  47. DefStream/Worker/Specification.py +87 -0
  48. DefStream/Worker/Startup.py +174 -0
  49. DefStream/Worker/Worker.py +421 -0
  50. DefStream/Worker/__init__.py +47 -0
  51. DefStream/__init__.py +0 -0
  52. defstream-0.1.13.dist-info/METADATA +679 -0
  53. defstream-0.1.13.dist-info/RECORD +56 -0
  54. defstream-0.1.13.dist-info/WHEEL +6 -0
  55. defstream-0.1.13.dist-info/licenses/LICENSE +674 -0
  56. defstream-0.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ from .HealthCheck import HealthCheck
2
+ from .Metrics import Metrics
3
+ from .Specifications import Specifications
4
+ from .Console import Console
5
+ from .Dashboard import Dashboard
6
+
7
+ __all__ = (
8
+ 'HealthCheck',
9
+ 'Metrics',
10
+ 'Specifications',
11
+ 'Console',
12
+ 'Dashboard',
13
+ )
@@ -0,0 +1,347 @@
1
+ from logging import getLogger
2
+ from socketserver import ThreadingMixIn
3
+ from sys import exc_info
4
+ from threading import Thread
5
+ from time import monotonic
6
+ from traceback import extract_tb
7
+ from typing import (
8
+ Callable,
9
+ Dict,
10
+ Iterable,
11
+ Optional,
12
+ Sequence,
13
+ TYPE_CHECKING,
14
+ Tuple,
15
+ )
16
+ from wsgiref.simple_server import (
17
+ WSGIRequestHandler,
18
+ ServerHandler,
19
+ WSGIServer,
20
+ make_server,
21
+ )
22
+
23
+ from .Extension import Extension
24
+ from .Response import Response
25
+
26
+ if TYPE_CHECKING:
27
+ from ..Worker.Worker import Worker
28
+
29
+ __all__ = (
30
+ 'Inspector',
31
+ )
32
+
33
+
34
+ class Server(
35
+ ThreadingMixIn,
36
+ WSGIServer,
37
+ ):
38
+ logger = getLogger('DefStream.Inspector')
39
+
40
+ daemon_threads = True
41
+ block_on_close = False
42
+
43
+ def get_request(self):
44
+ connection, address = super().get_request()
45
+ connection.settimeout(5)
46
+ return connection, address
47
+
48
+ def handle_error(
49
+ self,
50
+ request,
51
+ client_address,
52
+ ) -> None:
53
+ _, error, traceback = exc_info()
54
+ self.logger.error(
55
+ 'HTTP connection handling failed: error_type=%s\n%s',
56
+ type(error).__name__,
57
+ ''.join(
58
+ f' {frame.filename}:{frame.lineno} ({frame.name})\n'
59
+ for frame in extract_tb(traceback)
60
+ ),
61
+ )
62
+ return
63
+
64
+
65
+ class InspectorServerHandler(ServerHandler):
66
+ def log_exception(
67
+ self,
68
+ exc_info,
69
+ ) -> None:
70
+ _, error, traceback = exc_info
71
+ self.request_handler.logger.error(
72
+ 'WSGI request handling failed: error_type=%s\n%s',
73
+ type(error).__name__,
74
+ ''.join(
75
+ f' {frame.filename}:{frame.lineno} ({frame.name})\n'
76
+ for frame in extract_tb(traceback)
77
+ ),
78
+ )
79
+ return
80
+
81
+
82
+ class RequestHandler(WSGIRequestHandler):
83
+ logger = getLogger('DefStream.Inspector')
84
+
85
+ def handle(self) -> None:
86
+ self.started_at = monotonic()
87
+ self.path = ''
88
+ self.raw_requestline = self.rfile.readline(65537)
89
+ if len(self.raw_requestline) > 65536:
90
+ self.requestline = ''
91
+ self.request_version = ''
92
+ self.command = ''
93
+ self.send_error(414)
94
+ return
95
+ if not self.raw_requestline:
96
+ self.close_connection = True
97
+ return
98
+ if not self.parse_request():
99
+ return
100
+ self.logger.debug(
101
+ 'HTTP request received: method=%s path=%s',
102
+ self.command,
103
+ self.path.partition('?')[0],
104
+ )
105
+ handler = InspectorServerHandler(
106
+ self.rfile,
107
+ self.wfile,
108
+ self.get_stderr(),
109
+ self.get_environ(),
110
+ multithread=True,
111
+ )
112
+ handler.request_handler = self
113
+ handler.run(self.server.get_app())
114
+
115
+ def log_request(
116
+ self,
117
+ code='-',
118
+ size='-',
119
+ ) -> None:
120
+ self.logger.info(
121
+ 'HTTP response sent: method=%s path=%s status=%s bytes=%s duration=%s',
122
+ self.command,
123
+ self.path.partition('?')[0],
124
+ code,
125
+ size if size is not None else '-',
126
+ monotonic() - self.started_at,
127
+ )
128
+ return
129
+
130
+ def log_message(
131
+ self,
132
+ format: str,
133
+ *args,
134
+ ) -> None:
135
+ self.logger.warning('HTTP protocol error or request timeout')
136
+ return
137
+
138
+
139
+ class Inspector:
140
+ logger = getLogger('DefStream.Inspector')
141
+
142
+ def __init__(
143
+ self,
144
+ host: str = '127.0.0.1',
145
+ port: int = 9999,
146
+ exts: Sequence[Extension] = (),
147
+ ):
148
+ if isinstance(
149
+ port,
150
+ bool,
151
+ ) or not isinstance(
152
+ port,
153
+ int,
154
+ ) or not 0 <= port <= 65535:
155
+ raise ValueError('Inspector port must be between 0 and 65535')
156
+ self.host = host
157
+ self.port = port
158
+ self.exts: Dict[str, Extension] = {}
159
+ for ext in exts:
160
+ if not isinstance(
161
+ ext,
162
+ Extension,
163
+ ):
164
+ raise TypeError('Inspector extensions must inherit Extension')
165
+ if ext.path in self.exts:
166
+ raise ValueError('Duplicate Inspector path: ' + ext.path)
167
+ self.exts[ext.path] = ext
168
+ self.logger.debug(
169
+ 'Inspector extension registered: class=%s path=%s',
170
+ type(ext).__name__,
171
+ ext.path,
172
+ )
173
+ self.worker: Optional['Worker'] = None
174
+ self.server: Optional[WSGIServer] = None
175
+ self.thread: Optional[Thread] = None
176
+
177
+ def start(
178
+ self,
179
+ worker: 'Worker',
180
+ ) -> None:
181
+ if self.worker is not None:
182
+ raise RuntimeError('Inspector can only be attached and started once')
183
+ self.worker = worker
184
+ for ext in self.exts.values():
185
+ self.logger.debug(
186
+ 'Starting Inspector extension: class=%s path=%s',
187
+ type(ext).__name__,
188
+ ext.path,
189
+ )
190
+ try:
191
+ ext.start(self)
192
+ except Exception as error:
193
+ self.logger.error(
194
+ 'Inspector extension startup failed: class=%s path=%s error=%s',
195
+ type(ext).__name__,
196
+ ext.path,
197
+ type(error).__name__,
198
+ )
199
+ raise
200
+ try:
201
+ server = make_server(
202
+ self.host,
203
+ self.port,
204
+ self,
205
+ server_class=Server,
206
+ handler_class=RequestHandler,
207
+ )
208
+ except Exception as error:
209
+ self.logger.error(
210
+ 'Inspector HTTP server binding failed: error=%s',
211
+ type(error).__name__,
212
+ )
213
+ raise
214
+ self.server = server
215
+ thread = Thread(
216
+ target=self.serve,
217
+ daemon=True,
218
+ )
219
+ self.thread = thread
220
+ try:
221
+ thread.start()
222
+ except BaseException:
223
+ self.logger.error('Inspector HTTP server thread startup failed')
224
+ server.server_close()
225
+ raise
226
+ self.logger.info(
227
+ 'Inspector HTTP server started: host=%s port=%s',
228
+ server.server_address[0],
229
+ server.server_port,
230
+ )
231
+
232
+ def serve(self) -> None:
233
+ server = self.server
234
+ if server is None:
235
+ raise RuntimeError('Inspector server is not initialized')
236
+ try:
237
+ server.serve_forever(poll_interval=0.1)
238
+ except Exception as error:
239
+ self.logger.error(
240
+ 'Inspector HTTP server failed: error=%s',
241
+ type(error).__name__,
242
+ )
243
+ if self.worker is not None:
244
+ self.worker.stop()
245
+ raise
246
+
247
+ def stop(self) -> None:
248
+ server = self.server
249
+ thread = self.thread
250
+ if server is not None:
251
+ try:
252
+ if thread is not None and thread.is_alive():
253
+ self.logger.info('Stopping Inspector HTTP server')
254
+ server.shutdown()
255
+ thread.join()
256
+ finally:
257
+ server.server_close()
258
+ self.logger.info('Inspector HTTP server stopped')
259
+
260
+ def begin(
261
+ self,
262
+ task: str,
263
+ ) -> Tuple[Tuple[Extension, object], ...]:
264
+ return tuple(
265
+ (
266
+ ext,
267
+ ext.begin(task),
268
+ )
269
+ for ext in self.exts.values()
270
+ )
271
+
272
+ def end(
273
+ self,
274
+ task: str,
275
+ contexts: Tuple[Tuple[Extension, object], ...],
276
+ success: Optional[bool],
277
+ ) -> None:
278
+ for ext, context in contexts:
279
+ ext.end(
280
+ task,
281
+ context,
282
+ success,
283
+ )
284
+
285
+ def __call__(
286
+ self,
287
+ environ: dict,
288
+ start_response: Callable,
289
+ ) -> Iterable[bytes]:
290
+ method = environ['REQUEST_METHOD']
291
+ environ['wsgi.multithread'] = True
292
+ headers = []
293
+ ext = self.exts.get(environ['PATH_INFO'])
294
+ self.logger.debug(
295
+ 'Routing Inspector request: method=%s path=%r extension=%s',
296
+ method,
297
+ environ['PATH_INFO'],
298
+ type(ext).__name__ if ext is not None else 'none',
299
+ )
300
+ methods = ext.methods if ext is not None else (
301
+ 'GET',
302
+ 'HEAD',
303
+ )
304
+ if method not in methods:
305
+ response = Response(
306
+ '405 Method Not Allowed',
307
+ b'Method not allowed\n',
308
+ 'text/plain; charset=utf-8',
309
+ )
310
+ headers.append((
311
+ 'Allow',
312
+ ', '.join(methods),
313
+ ))
314
+ else:
315
+ response = ext.handle(
316
+ self,
317
+ environ,
318
+ ) if ext is not None else Response(
319
+ '404 Not Found',
320
+ b'Not found\n',
321
+ 'text/plain; charset=utf-8',
322
+ )
323
+ headers.extend(response.headers)
324
+ headers.extend((
325
+ (
326
+ 'Content-Type',
327
+ response.content_type,
328
+ ),
329
+ (
330
+ 'Content-Length',
331
+ str(len(response.body)),
332
+ ),
333
+ (
334
+ 'Cache-Control',
335
+ 'no-store',
336
+ ),
337
+ ))
338
+ self.logger.debug(
339
+ 'Inspector response built: status=%s bytes=%d',
340
+ response.status,
341
+ len(response.body),
342
+ )
343
+ start_response(
344
+ response.status,
345
+ headers,
346
+ )
347
+ return [b'' if method == 'HEAD' else response.body]
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass
2
+ from typing import Tuple
3
+
4
+ __all__ = (
5
+ 'Response',
6
+ )
7
+
8
+
9
+ @dataclass
10
+ class Response:
11
+ status: str
12
+ body: bytes
13
+ content_type: str
14
+ headers: Tuple[Tuple[str, str], ...] = ()
@@ -0,0 +1,9 @@
1
+ from .Inspector import Inspector
2
+ from .Extension import Extension
3
+ from .Response import Response
4
+
5
+ __all__ = (
6
+ 'Inspector',
7
+ 'Extension',
8
+ 'Response',
9
+ )
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import (
4
+ ABC,
5
+ abstractmethod,
6
+ )
7
+
8
+ __all__ = (
9
+ 'Pattern',
10
+ )
11
+
12
+
13
+ class Pattern(ABC):
14
+ """
15
+ Pattern Interface Class for Validator
16
+
17
+ Sample:
18
+ from operator import eq
19
+
20
+ class SamplePattern(Pattern):
21
+ def __init__(self, compare):
22
+ super(SamplePattern, self).__init__(error)
23
+ self.compare = compare
24
+ return
25
+
26
+ def __call__(self, parameter):
27
+ if not eq(parameter, self.compare):
28
+ raise ValueError('{} is not equal to {}'.format(parameter, self.condition))
29
+ return parameter # if true, return parameter
30
+ """
31
+ @abstractmethod
32
+ def __call__(self, parameter):
33
+ pass
34
+
35
+ def __repr__(self):
36
+ return '{}()'.format(self.__class__.__name__)
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from ..Pattern import Pattern
4
+
5
+ from typing import Any
6
+
7
+ __all__ = (
8
+ 'IsElementOf',
9
+ )
10
+
11
+
12
+ class IsElementOf(Pattern):
13
+ def __init__(self, *args):
14
+ self.patterns = args
15
+ return
16
+
17
+ def __call__(self, parameter: Any) -> Any:
18
+ _ = list(parameter)
19
+ for pattern in self.patterns:
20
+ for i, e in enumerate(_):
21
+ _[i] = pattern(e)
22
+ return type(parameter)(_)
23
+
24
+ def __repr__(self):
25
+ return '{}({})'.format(
26
+ self.__class__.__name__,
27
+ ', '.join(
28
+ [repr(p) for p in self.patterns]
29
+ )
30
+ )
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from ..Pattern import Pattern
4
+
5
+ from operator import eq
6
+
7
+ __all__ = (
8
+ 'SetDefault',
9
+ 'IsToNone',
10
+ 'IsNotToNone',
11
+ 'IsNotEmpty',
12
+ )
13
+
14
+
15
+ class SetDefault(Pattern):
16
+ def __init__(self, default):
17
+ self.default = default
18
+ return
19
+
20
+ def __call__(self, parameter):
21
+ if parameter == None:
22
+ return self.default
23
+ return parameter
24
+
25
+ def __repr__(self):
26
+ return '{}({})'.format(
27
+ self.__class__.__name__,
28
+ self.default
29
+ )
30
+
31
+
32
+ class IsToNone(Pattern):
33
+ def __init__(self, *args):
34
+ self.patterns = args
35
+ return
36
+
37
+ def __call__(self, parameter):
38
+ if parameter is None:
39
+ return parameter
40
+ for pattern in self.patterns:
41
+ parameter = pattern(parameter)
42
+ return parameter
43
+
44
+ def __repr__(self):
45
+ return '{}({})'.format(
46
+ self.__class__.__name__,
47
+ ', '.join([p.__repr__() for p in self.patterns])
48
+ )
49
+
50
+
51
+ class IsNotToNone(Pattern):
52
+ def __init__(self, *args, error=None):
53
+ self.patterns = args
54
+ self.error = error
55
+ return
56
+
57
+ def __call__(self, parameter):
58
+ if parameter is None:
59
+ if self.error:
60
+ raise self.error
61
+ raise ValueError('{} is None'.format(parameter))
62
+ for pattern in self.patterns:
63
+ parameter = pattern(parameter)
64
+ return parameter
65
+
66
+ def __repr__(self):
67
+ return '{}({})'.format(
68
+ self.__class__.__name__,
69
+ ', '.join([p.__repr__() for p in self.patterns])
70
+ )
71
+
72
+
73
+ class IsNotEmpty(Pattern):
74
+ def __init__(self, *args, error=None):
75
+ self.patterns = args
76
+ self.error = error
77
+ return
78
+
79
+ def __call__(self, parameter):
80
+ if eq(len(parameter), 0):
81
+ if self.error:
82
+ raise self.error
83
+ raise ValueError('{} must be not empty'.format(parameter))
84
+ for pattern in self.patterns:
85
+ parameter = pattern(parameter)
86
+ return parameter
87
+
88
+ def __repr__(self):
89
+ return '{}({})'.format(
90
+ self.__class__.__name__,
91
+ ', '.join([p.__repr__() for p in self.patterns])
92
+ )
93
+
@@ -0,0 +1,180 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from ..Pattern import Pattern
4
+
5
+ from operator import eq, ne, gt, ge, lt, le
6
+
7
+ __all__ = (
8
+ 'IsIn',
9
+ 'IsNotIn',
10
+ 'IsEqualTo',
11
+ 'IsNotEqualTo',
12
+ 'IsGreaterThan',
13
+ 'IsGreaterEqualTo',
14
+ 'IsLessThan',
15
+ 'IsLessEqualTo',
16
+ )
17
+
18
+
19
+ class IsIn(Pattern):
20
+ def __init__(self, *args, error=None):
21
+ self.compares = args
22
+ self.error = error
23
+ return
24
+
25
+ def __call__(self, parameter):
26
+ if parameter not in self.compares:
27
+ if self.error:
28
+ raise self.error
29
+ raise ValueError('{} must be in {}'.format(parameter, self.compares))
30
+ return parameter
31
+
32
+ def __repr__(self):
33
+ return '{}({})'.format(
34
+ self.__class__.__name__,
35
+ ', '.join(
36
+ [repr(c) for c in self.compares]
37
+ )
38
+ )
39
+
40
+
41
+ class IsNotIn(Pattern):
42
+ def __init__(self, *args, error=None):
43
+ self.compares = args
44
+ self.error = error
45
+ return
46
+
47
+ def __call__(self, parameter):
48
+ if parameter in self.compares:
49
+ if self.error:
50
+ raise self.error
51
+ raise ValueError('{} must be not in {}'.format(parameter, self.compares))
52
+ return parameter
53
+
54
+ def __repr__(self):
55
+ return '{}({})'.format(
56
+ self.__class__.__name__,
57
+ ', '.join(
58
+ [repr(c) for c in self.compares]
59
+ )
60
+ )
61
+
62
+
63
+ class IsEqualTo(Pattern):
64
+ def __init__(self, equal, error=None):
65
+ self.equal = equal
66
+ self.error = error
67
+ return
68
+
69
+ def __call__(self, parameter):
70
+ if not eq(parameter, self.equal):
71
+ if self.error:
72
+ raise self.error
73
+ raise ValueError('{} must equal to {}'.format(parameter, self.equal))
74
+ return parameter
75
+
76
+ def __repr__(self):
77
+ return '{}({})'.format(
78
+ self.__class__.__name__,
79
+ self.equal
80
+ )
81
+
82
+
83
+ class IsNotEqualTo(Pattern):
84
+ def __init__(self, equal, error=None):
85
+ self.equal = equal
86
+ self.error = error
87
+ return
88
+
89
+ def __call__(self, parameter):
90
+ if not ne(parameter, self.equal):
91
+ if self.error:
92
+ raise self.error
93
+ raise ValueError('{} must not equal to {}'.format(parameter, self.equal))
94
+ return parameter
95
+
96
+ def __repr__(self):
97
+ return '{}({})'.format(
98
+ self.__class__.__name__,
99
+ self.equal
100
+ )
101
+
102
+
103
+ class IsGreaterEqualTo(Pattern):
104
+ def __init__(self, compare, error=None):
105
+ self.compare = compare
106
+ self.error = error
107
+ return
108
+
109
+ def __call__(self, parameter):
110
+ if not ge(parameter, self.compare):
111
+ if self.error:
112
+ raise self.error
113
+ raise ValueError('{} must be greater equal to {}'.format(parameter, self.compare))
114
+ return parameter
115
+
116
+ def __repr__(self):
117
+ return '{}({})'.format(
118
+ self.__class__.__name__,
119
+ self.compare
120
+ )
121
+
122
+
123
+ class IsGreaterThan(Pattern):
124
+ def __init__(self, compare, error=None):
125
+ self.compare = compare
126
+ self.error = error
127
+ return
128
+
129
+ def __call__(self, parameter):
130
+ if not gt(parameter, self.compare):
131
+ if self.error:
132
+ raise self.error
133
+ raise ValueError('{} must be greater than {}'.format(parameter, self.compare))
134
+ return parameter
135
+
136
+ def __repr__(self):
137
+ return '{}({})'.format(
138
+ self.__class__.__name__,
139
+ self.compare
140
+ )
141
+
142
+
143
+ class IsLessEqualTo(Pattern):
144
+ def __init__(self, compare, error=None):
145
+ self.compare = compare
146
+ self.error = error
147
+ return
148
+
149
+ def __call__(self, parameter):
150
+ if not le(parameter, self.compare):
151
+ if self.error:
152
+ raise self.error
153
+ raise ValueError('{} must be less equal to {}'.format(parameter, self.compare))
154
+ return parameter
155
+
156
+ def __repr__(self):
157
+ return '{}({})'.format(
158
+ self.__class__.__name__,
159
+ self.compare
160
+ )
161
+
162
+
163
+ class IsLessThan(Pattern):
164
+ def __init__(self, compare, error=None):
165
+ self.compare = compare
166
+ self.error = error
167
+ return
168
+
169
+ def __call__(self, parameter):
170
+ if not lt(parameter, self.compare):
171
+ if self.error:
172
+ raise self.error
173
+ raise ValueError('{} must be less than {}'.format(parameter, self.compare))
174
+ return parameter
175
+
176
+ def __repr__(self):
177
+ return '{}({})'.format(
178
+ self.__class__.__name__,
179
+ self.compare
180
+ )