underscorepy 0.9.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.
_/records/protobuf.py ADDED
@@ -0,0 +1,496 @@
1
+ #
2
+ # (c) 2015-2024 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import configparser
10
+ import dataclasses
11
+ import json
12
+ import keyword
13
+ import logging
14
+ import os
15
+ import types
16
+ import typing
17
+
18
+ import sqlalchemy
19
+ import sqlalchemy.ext.orderinglist
20
+ import tornado.web
21
+ import google.protobuf.descriptor
22
+ import google.protobuf.message
23
+ import google.protobuf.json_format
24
+
25
+ import _
26
+
27
+
28
+ def _repeated(field):
29
+ # FieldDescriptor.label was removed in protobuf 7
30
+ try:
31
+ return field.is_repeated
32
+ except AttributeError:
33
+ return field.label == field.LABEL_REPEATED
34
+
35
+
36
+ def _check_names(descriptor):
37
+ # record fields are Python attributes so keywords can't be used
38
+ for field in descriptor.fields:
39
+ if keyword.iskeyword(field.name):
40
+ raise _.error('Field "%s" in %s is a Python keyword', field.name, descriptor.full_name)
41
+
42
+
43
+ def _is_map(field):
44
+ return field.message_type is not None and field.message_type.GetOptions().map_entry
45
+
46
+
47
+ def _is_message(field):
48
+ return field.type == field.TYPE_MESSAGE
49
+
50
+
51
+ def _json_form(pb):
52
+ 'the proto3 JSON form of a message, keeping the field names from the .proto'
53
+ return google.protobuf.json_format.MessageToDict(pb,
54
+ always_print_fields_with_no_presence = True,
55
+ preserving_proto_field_name = True,
56
+ )
57
+
58
+
59
+ class Protobuf(_.records.Record):
60
+ async def init(self, module, **kwds):
61
+ # setup the container beforehand so the data module can use data decorators
62
+ if hasattr(_, self.component_name):
63
+ raise _.error('Record name "%s" for "%s" conflicts in _ root', self.component_name, module.__name__)
64
+
65
+ _.argparser.add_argument(f'--{self.component_name}',
66
+ action='store_true',
67
+ help='output Protobuf options template'
68
+ )
69
+
70
+ # options come from Protobuf.proto but need to be compiled in-line to avoid headaches
71
+ self._options_name = f"{kwds.get('options', 'Protobuf')}_pb2"
72
+
73
+ # container class for derived Python types
74
+ self._container = ProtobufContainer()
75
+
76
+ # access the types via the name or alias of the component
77
+ setattr(_, self.component_name, self._container)
78
+
79
+ # default to JSON style tables instead of relational
80
+ try:
81
+ self.relational = _.config.getboolean(self.component_name, 'relational')
82
+ except configparser.NoOptionError:
83
+ self.relational = False
84
+
85
+ # default to creating handlers unless specified otherwise in the ini
86
+ try:
87
+ self.handlers = _.config.getboolean(self.component_name, 'handlers')
88
+ except configparser.NoOptionError:
89
+ self.handlers = True
90
+
91
+ await super().init(module, **kwds)
92
+
93
+ @classmethod
94
+ async def args(cls, component_name):
95
+ if getattr(_.args, component_name, False):
96
+ path = os.path.join(os.path.dirname(__file__), 'Protobuf.proto')
97
+ protobuf = open(path, 'r').read()
98
+ print(protobuf)
99
+ _.application.stop()
100
+
101
+ def load(self, module):
102
+ try:
103
+ self._options = getattr(module, self._options_name)
104
+ except AttributeError:
105
+ raise _.error('Cannot find protobuf module "%s"', self._options_name)
106
+ self._package = module.__name__
107
+ # container path of the subpackage being loaded, e.g. ['sub']
108
+ self._path = []
109
+ self.load_module(module)
110
+ del self._module_name
111
+ del self._path
112
+
113
+ def load_module(self, module):
114
+ self._module_name = module.__name__
115
+ for member_name in dir(module):
116
+ # ignore the options file
117
+ if member_name == self._options_name:
118
+ continue
119
+
120
+ # get a handle to the pb2 module descriptor
121
+ member = getattr(module, member_name)
122
+
123
+ # ignore anything that isn't a module from this package
124
+ if not isinstance(member, types.ModuleType):
125
+ continue
126
+ if not member.__name__.startswith(self._package + '.'):
127
+ continue
128
+
129
+ # all proto messages end with _pb2
130
+ if not member_name.endswith('_pb2'):
131
+ # ignore any module starting with _
132
+ if not member_name.startswith('_'):
133
+ # load submodules for nested protobufs
134
+ self._container[member_name] = ProtobufContainer(self._container.handlers)
135
+ stash = (self._container,self._module_name)
136
+ self._container = self._container[member_name]
137
+ self._path.append(member_name)
138
+ self.load_module(member)
139
+ self._path.pop()
140
+ (self._container,self._module_name) = stash
141
+ continue
142
+
143
+ # iterate over all the message definitions
144
+ for name in member.DESCRIPTOR.message_types_by_name:
145
+ message = getattr(member, name)
146
+ options = message.DESCRIPTOR.GetOptions()
147
+
148
+ # subpackage messages get their own tables and routes,
149
+ # pbt.sub.Tags is table sub_Tags served at /<component>/sub/Tags
150
+ table_name = '_'.join(self._path + [name])
151
+ route = '/'.join(self._path + [name])
152
+
153
+ _check_names(message.DESCRIPTOR)
154
+ if self.db and not options.Extensions[self._options.no_table]:
155
+ record_type = self._proto_table(table_name, message=message)
156
+ else:
157
+ # declare the message fields so _as_dict has something to return
158
+ fields = message.DESCRIPTOR.fields
159
+ members = {f.name:None for f in fields}
160
+ members['__annotations__'] = {f.name:typing.Any for f in fields}
161
+ members['_ProtoInterface__pb'] = message
162
+ members['_proto_children'] = {}
163
+ record_type = type(name, (ProtoInterface,), members)
164
+ record_type = dataclasses.dataclass(init=False, kw_only=True)(record_type)
165
+
166
+ self._container[name] = record_type
167
+
168
+ if self.handlers and not options.Extensions[self._options.no_handler]:
169
+ self._proto_handler(route, record_type, module.__name__, message.DESCRIPTOR.full_name)
170
+
171
+ def _proto_table(self, name, message=None, descriptor=None, parent=None, parent_key=None, parent_col=None, ordered=False):
172
+ child_tables = {}
173
+ annotations = {}
174
+
175
+ members = {
176
+ '__tablename__' : name,
177
+ '__annotations__' : annotations,
178
+ '__module__' : self._module_name,
179
+ }
180
+
181
+ if message:
182
+ members['_ProtoInterface__pb'] = message
183
+
184
+ if descriptor is None:
185
+ descriptor = message.DESCRIPTOR
186
+ _check_names(descriptor)
187
+
188
+ primary_key = None
189
+ # iterate over message to determine columns
190
+ for field in descriptor.fields:
191
+ col_options = field.GetOptions()
192
+
193
+ # check if column should be primary key
194
+ is_primary_key = col_options.Extensions[self._options.pkey]
195
+ if is_primary_key:
196
+ if primary_key:
197
+ raise _.error('Only one primary key can be specified')
198
+ primary_key = field.name
199
+
200
+ # check if column should be unique
201
+ unique = col_options.Extensions[self._options.uniq]
202
+
203
+ if _is_message(field) and is_primary_key:
204
+ raise _.error('Message type cannot be primary key')
205
+ if _repeated(field) and is_primary_key:
206
+ raise _.error('Repeated field cannot be primary key')
207
+
208
+ # check for foreign key
209
+ if col_options.HasExtension(self._options.ref):
210
+ print('TODO: FKEY:', col_options.Extensions[self._options.ref])
211
+
212
+ if _is_message(field) and self.relational and not _is_map(field):
213
+ # messages become child tables, maps stay JSON
214
+ ref_table_name = f'{name}_{field.name}'
215
+ ordering = {}
216
+ if _repeated(field):
217
+ annotations[field.name] = sqlalchemy.orm.Mapped[list[ref_table_name]]
218
+ # keep repeated children in the order they were given
219
+ ordering = dict(
220
+ order_by = f'{ref_table_name}._position',
221
+ collection_class = sqlalchemy.ext.orderinglist.ordering_list('_position'),
222
+ )
223
+ else:
224
+ annotations[field.name] = sqlalchemy.orm.Mapped[typing.Optional[ref_table_name]]
225
+ members[field.name] = sqlalchemy.orm.relationship(
226
+ back_populates=name,
227
+ lazy='joined',
228
+ cascade="all, delete-orphan",
229
+ passive_deletes=True,
230
+ init=False,
231
+ **ordering,
232
+ )
233
+ child_tables[ref_table_name] = field
234
+ continue
235
+
236
+ if _is_message(field):
237
+ # messages and maps are stored in their proto3 JSON form
238
+ column_type = (self.db.JSON or sqlalchemy.JSON)(none_as_null=True)
239
+ else:
240
+ column_type = self._column_type(field)
241
+ if _repeated(field):
242
+ # TEXT[] rather than VARCHAR[] to match existing tables
243
+ if field.type == field.TYPE_STRING:
244
+ column_type = sqlalchemy.TEXT()
245
+ column_type = self.db.array(column_type)
246
+
247
+ annotations[field.name] = sqlalchemy.orm.Mapped[typing.Optional[typing.Any]]
248
+ members[field.name] = sqlalchemy.orm.mapped_column(
249
+ column_type,
250
+ primary_key=is_primary_key,
251
+ unique=unique,
252
+ init=False,
253
+ )
254
+
255
+ table_options = descriptor.GetOptions()
256
+
257
+ if table_options.HasExtension(self._options.id):
258
+ primary_key = table_options.Extensions[self._options.id]
259
+ members[primary_key] = sqlalchemy.orm.mapped_column(
260
+ sqlalchemy.INTEGER,
261
+ primary_key=True,
262
+ autoincrement=True,
263
+ init=False,
264
+ )
265
+ elif primary_key is None:
266
+ primary_key = f'{name}_id'
267
+ members[primary_key] = sqlalchemy.orm.mapped_column(
268
+ sqlalchemy.INTEGER,
269
+ primary_key=True,
270
+ autoincrement=True,
271
+ init=False,
272
+ )
273
+
274
+ if ordered:
275
+ # unannotated so it stays out of _as_dict, like the foreign key
276
+ members['_position'] = sqlalchemy.orm.mapped_column(
277
+ sqlalchemy.INTEGER,
278
+ init=False,
279
+ default=None,
280
+ )
281
+
282
+ if parent and parent_key and parent_col:
283
+ members[f'{parent}_{parent_key}'] = sqlalchemy.orm.mapped_column(
284
+ sqlalchemy.ForeignKey(f'{parent}.{parent_key}', ondelete='CASCADE'),
285
+ init=False,
286
+ )
287
+ members[f'{parent}'] = sqlalchemy.orm.relationship(
288
+ parent,
289
+ back_populates=parent_col,
290
+ lazy='joined',
291
+ init=False,
292
+ )
293
+
294
+ members['__primary_key__'] = primary_key
295
+ members['_proto_children'] = {}
296
+
297
+ table_type = type(name, (ProtoInterface,self.db.Base,), members)
298
+
299
+ if message:
300
+ # refuse to store a record that its message can't represent
301
+ sqlalchemy.event.listen(table_type, 'before_insert', _validate)
302
+ sqlalchemy.event.listen(table_type, 'before_update', _validate)
303
+
304
+ for child_table,field in child_tables.items():
305
+ child_type = self._proto_table(child_table, descriptor=field.message_type, parent=name, parent_key=primary_key, parent_col=field.name, ordered=_repeated(field))
306
+ table_type._proto_children[field.name] = child_type
307
+ setattr(table_type, f'_{field.name}', child_type)
308
+
309
+ return table_type
310
+
311
+ def _proto_handler(self, route, record_type, module_name, full_name):
312
+ ## check if a custom handler was defined
313
+ proto_handler = self._container.handlers.get(full_name)
314
+ # the default handler needs a table
315
+ if not proto_handler and not hasattr(record_type, '__table__'):
316
+ return
317
+
318
+ handler_name = route.replace('/', '_')
319
+ if proto_handler:
320
+ handler_name = proto_handler.__name__
321
+ module_name = proto_handler.__module__
322
+ types = [proto_handler] if proto_handler else [_.records.HandlerInterface]
323
+ types.append(tornado.web.RequestHandler)
324
+
325
+ # served at the message's route, whichever handler class is used
326
+ record_handler = type(handler_name, tuple(types), {
327
+ '_component' : route,
328
+ '_db' : self.db,
329
+ '_record' : record_type,
330
+ '__module__' : module_name,
331
+ })
332
+
333
+ _.application._record_handler(self.component_name, record_handler)
334
+
335
+ def _column_type(self, field):
336
+ F = google.protobuf.descriptor.FieldDescriptor
337
+ if field.type in (F.TYPE_DOUBLE, F.TYPE_FLOAT):
338
+ return sqlalchemy.Float()
339
+ if field.type == F.TYPE_BOOL:
340
+ return sqlalchemy.Boolean()
341
+ if field.type == F.TYPE_STRING:
342
+ return sqlalchemy.String()
343
+ if field.type == F.TYPE_BYTES:
344
+ return (self.db.BYTES or sqlalchemy.LargeBinary)()
345
+ if field.type in (F.TYPE_UINT64, F.TYPE_FIXED64):
346
+ return _.databases.UInt64()
347
+ # the remaining integers and enums all fit in a BIGINT
348
+ return sqlalchemy.BIGINT()
349
+
350
+
351
+ class ProtoInterface(_.records.RecordsInterface):
352
+ 'converts between records and protobuf messages'
353
+
354
+ # child table classes by field name, only used in relational mode
355
+ _proto_children = {}
356
+
357
+ def _assign_pb(self, pb):
358
+ 'set the record fields from a protobuf message'
359
+ json_form = None
360
+ for field in pb.DESCRIPTOR.fields:
361
+ child_type = self._proto_children.get(field.name)
362
+ value = getattr(pb, field.name)
363
+ if _is_message(field) and not child_type:
364
+ if json_form is None:
365
+ json_form = _json_form(pb)
366
+ # unset singular messages are left out of the JSON form
367
+ value = json_form.get(field.name)
368
+ elif child_type:
369
+ if _repeated(field):
370
+ value = [child_type()._assign_pb(item) for item in value]
371
+ elif pb.HasField(field.name):
372
+ value = child_type()._assign_pb(value)
373
+ else:
374
+ value = None
375
+ elif _repeated(field):
376
+ value = list(value)
377
+ elif field.has_presence and not pb.HasField(field.name):
378
+ # optional and oneof fields that were not set
379
+ value = None
380
+ setattr(self, field.name, value)
381
+ return self
382
+
383
+ def _fill_pb(self, pb):
384
+ 'set the fields of a protobuf message from the record'
385
+ json_form = {}
386
+ for field in pb.DESCRIPTOR.fields:
387
+ value = getattr(self, field.name, None)
388
+ if value is None:
389
+ continue
390
+ if _is_message(field) and not self._proto_children.get(field.name):
391
+ json_form[field.name] = value
392
+ elif _is_message(field):
393
+ if _repeated(field):
394
+ for item in value:
395
+ item._fill_pb(getattr(pb, field.name).add())
396
+ else:
397
+ getattr(pb, field.name).SetInParent()
398
+ value._fill_pb(getattr(pb, field.name))
399
+ elif _repeated(field):
400
+ getattr(pb, field.name).extend(_scalar(field, v) for v in value)
401
+ else:
402
+ setattr(pb, field.name, _scalar(field, value))
403
+ if json_form:
404
+ google.protobuf.json_format.ParseDict(json_form, pb)
405
+ return pb
406
+
407
+ @classmethod
408
+ def _parse(cls, parse, *args):
409
+ pb = cls.__pb()
410
+ try:
411
+ parse(*args, pb)
412
+ except (google.protobuf.json_format.ParseError, google.protobuf.message.DecodeError) as e:
413
+ raise _.error('%s', e) from None
414
+ return pb
415
+
416
+ def __call__(self, *args, **kwds):
417
+ msg = args[0] if args else kwds
418
+ return self._assign_pb(self._parse(google.protobuf.json_format.ParseDict, msg))
419
+
420
+ @classmethod
421
+ def _from_dict(cls, *args, **kwds):
422
+ msg = args[0] if args else kwds
423
+ return cls()._assign_pb(cls._parse(google.protobuf.json_format.ParseDict, msg))
424
+
425
+ @classmethod
426
+ def _from_json(cls, msg):
427
+ return cls()._assign_pb(cls._parse(google.protobuf.json_format.Parse, msg))
428
+
429
+ @classmethod
430
+ def _from_binary(cls, packed):
431
+ pb = cls.__pb()
432
+ try:
433
+ pb.ParseFromString(packed)
434
+ except google.protobuf.message.DecodeError as e:
435
+ raise _.error('%s', e) from None
436
+ return cls()._assign_pb(pb)
437
+
438
+ @classmethod
439
+ def _from_pb(cls, pb):
440
+ return cls()._assign_pb(pb)
441
+
442
+ def _as_binary(self):
443
+ return self._as_pb().SerializeToString()
444
+
445
+ def _as_pb(self):
446
+ try:
447
+ return self._fill_pb(self.__pb())
448
+ except (google.protobuf.json_format.ParseError, TypeError, ValueError, KeyError) as e:
449
+ raise _.error('%s', e) from None
450
+
451
+
452
+ def _validate(mapper, connection, record):
453
+ # raises _.error when a field doesn't fit the message, e.g. an unknown nested key
454
+ record._as_pb()
455
+
456
+
457
+ def _scalar(field, value):
458
+ # databases can return Decimal for integers and float columns
459
+ F = google.protobuf.descriptor.FieldDescriptor
460
+ if field.type in (F.TYPE_DOUBLE, F.TYPE_FLOAT):
461
+ return float(value)
462
+ if field.type == F.TYPE_ENUM and isinstance(value, str):
463
+ return field.enum_type.values_by_name[value].number
464
+ if field.type not in (F.TYPE_STRING, F.TYPE_BYTES, F.TYPE_BOOL):
465
+ return int(value)
466
+ return value
467
+
468
+
469
+ class ProtobufContainer(_.Container):
470
+ def __init__(self, handlers=None):
471
+ super().__init__()
472
+ # custom handlers by message full name, shared with subpackage containers
473
+ self.handlers = {} if handlers is None else handlers
474
+
475
+ # decorator for adding custom handlers for message types
476
+ def handles(self, _message):
477
+ def wrap(_handler):
478
+ self.handlers[_message.DESCRIPTOR.full_name] = _handler
479
+ return _handler
480
+ return wrap
481
+
482
+ MessageToDict = _json_form
483
+
484
+ class ProtoJSONEncoder(_.JSONEncoder):
485
+ def default(self, obj):
486
+ if hasattr(obj, 'DESCRIPTOR'):
487
+ return MessageToDict(obj)
488
+ return _.JSONEncoder.default(self, obj)
489
+
490
+ json._default_encoder = ProtoJSONEncoder()
491
+
492
+
493
+ if '__main__' == __name__:
494
+ path = os.path.join(os.path.dirname(__file__), 'Protobuf.proto')
495
+ protobuf = open(path, 'r').read()
496
+ print(protobuf)
_/settings.py ADDED
@@ -0,0 +1,159 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import argparse
10
+ import collections
11
+ import configparser
12
+ import inspect
13
+ import logging
14
+ import os
15
+ import sys
16
+ import time
17
+ import traceback
18
+
19
+ import _
20
+
21
+
22
+ # override exit to set the stop event
23
+ class ArgParser(argparse.ArgumentParser):
24
+ def exit(self, status=0, message=None):
25
+ if message:
26
+ self._print_message(message, sys.stderr)
27
+ _.application.stop()
28
+
29
+
30
+ _.argparser = ArgParser(add_help=False)
31
+
32
+ _.argparser.add_argument('--ini', '-I',
33
+ metavar='<path>',
34
+ help='Specify additional ini file')
35
+
36
+ _.config = configparser.ConfigParser(
37
+ allow_no_value = True,
38
+ interpolation = None,
39
+ )
40
+ _.config.optionxform = str
41
+
42
+
43
+ async def load(**kwds):
44
+ # get the path of the caller
45
+ try:
46
+ caller = inspect.getfile(_.application.__class__)
47
+ except (TypeError,OSError):
48
+ caller = '.'
49
+
50
+ # allow apps to define a name or derive from caller
51
+ _.name = kwds.get('name', None)
52
+ if _.name is None:
53
+ # get the name of the script
54
+ _.name = os.path.basename(caller)
55
+ if _.name.endswith('.py'):
56
+ _.name = _.name[:-3]
57
+
58
+ # allow apps to define a namespace
59
+ _.ns = kwds.get('ns', _.name)
60
+ if _.ns is None:
61
+ _.ns = ''
62
+
63
+ # get the directory of the script
64
+ root = kwds.get('root', None)
65
+ if root is None:
66
+ root = os.path.dirname(caller)
67
+ root = os.path.abspath(root)
68
+
69
+ _.paths = _.Paths(root=root, ns=_.ns)
70
+
71
+ # if ns is not passed in use the supplied or derived ns
72
+ ini_name = _.name or _.ns
73
+ ini_dir = _.ns or _.name
74
+
75
+ # allow several locations for ini files
76
+ ini_files = [
77
+ _.paths(f'{ini_name}.ini'),
78
+ _.paths(f'{ini_name}.ini.local'),
79
+ os.path.join(os.path.sep, 'etc', f'{ini_dir}', f'{ini_name}.ini'),
80
+ os.path.join(os.path.expanduser('~'), f'.{ini_name}.ini'),
81
+ ]
82
+
83
+ # first pass at parsing args to get additional ini files
84
+ _.args,remainder = _.argparser.parse_known_args()
85
+
86
+ # append a custom ini file if specified
87
+ if _.args.ini:
88
+ ini_files.append(_.args.ini)
89
+
90
+ # little hack because I like the generic options at the bottom of the help message
91
+ _.args.debug = '--debug' in remainder or '-D' in remainder
92
+ logging.basicConfig(
93
+ format = '%(asctime)s %(levelname)-8s %(message)s',
94
+ datefmt = '%Y-%m-%d %H:%M:%S',
95
+ level = logging.DEBUG if _.args.debug else logging.INFO,
96
+ force = True
97
+ )
98
+
99
+ # load the ini files, at least one file must exist
100
+ try:
101
+ if _.args.debug:
102
+ for ini_file in ini_files:
103
+ logging.debug('Loading ini file: %s', ini_file)
104
+ ok = _.config.read(ini_files)
105
+ except configparser.ParsingError as e:
106
+ raise _.error('Unable to parse file: %s', e)
107
+
108
+ if not ok:
109
+ logging.warning('Unable to read config file(s):\n %s', '\n '.join(ini_files))
110
+
111
+ # load the components specified in the ini file
112
+ try:
113
+ await _.components.load('databases')
114
+ await _.components.load('records')
115
+ await _.components.load('caches')
116
+ await _.components.load('logins')
117
+ await _.components.load('supports')
118
+ except _.error as e:
119
+ logging.error('%s', e)
120
+ _.application.stop()
121
+ return
122
+
123
+ # add the generic arguments after any components
124
+ _.argparser.add_argument('--debug', '-D',
125
+ action='store_true',
126
+ help='Log verbose debugging information')
127
+
128
+ _.argparser.add_argument('--version', '-V',
129
+ action='store_true',
130
+ help='Show version and exit'
131
+ )
132
+
133
+ _.argparser.add_argument('--help', '-h',
134
+ action='help', default=argparse.SUPPRESS,
135
+ help='Show help message')
136
+
137
+ _.args = _.argparser.parse_args()
138
+
139
+ try:
140
+ # tables are created once every component has defined its models
141
+ for name,component in _.databases.items():
142
+ if not _.application._stop_event.is_set():
143
+ await component.create_tables()
144
+
145
+ for name,component in _.records.items():
146
+ if not _.application._stop_event.is_set():
147
+ await _.wait(component.args(name))
148
+
149
+ for name,component in _.supports.items():
150
+ if not _.application._stop_event.is_set():
151
+ await _.wait(component.args(name))
152
+
153
+ for name,component in _.logins.items():
154
+ if not _.application._stop_event.is_set():
155
+ await _.wait(component.args(name))
156
+ except _.error as e:
157
+ logging.error('%s', e)
158
+ _.application.stop()
159
+ return
_/supports/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import os
10
+
11
+ import _
12
+
13
+
14
+ class Support:
15
+ @classmethod
16
+ async def _(cls, component_name, **kwds):
17
+ self = cls()
18
+ _.supports[component_name] = self
19
+ self.root = os.path.dirname(__file__)
20
+ self.root = os.path.abspath(self.root)
21
+ await self.init(component_name, **kwds)
22
+
23
+ async def init(self, component_name, **kwds):
24
+ pass
25
+
26
+ @classmethod
27
+ async def args(cls, component_name):
28
+ pass