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.
- _/__init__.py +46 -0
- _/application.py +278 -0
- _/auth.py +163 -0
- _/caches/__init__.py +50 -0
- _/caches/dbcache.py +140 -0
- _/caches/memory.py +77 -0
- _/caches/redis.py +104 -0
- _/components.py +107 -0
- _/databases/__init__.py +282 -0
- _/databases/postgres.py +38 -0
- _/databases/sqlite.py +78 -0
- _/handlers.py +45 -0
- _/logins/__init__.py +82 -0
- _/logins/dblogin.py +230 -0
- _/logins/facebook.py +40 -0
- _/logins/gitlab.py +58 -0
- _/logins/google.py +20 -0
- _/logins/htpasswd.py +47 -0
- _/logins/oauth2.py +52 -0
- _/logins/slap.py +56 -0
- _/records/Protobuf.proto +21 -0
- _/records/__init__.py +186 -0
- _/records/data.py +232 -0
- _/records/protobuf.py +496 -0
- _/settings.py +159 -0
- _/supports/__init__.py +28 -0
- _/supports/nginx/__init__.py +78 -0
- _/supports/nginx/nginx.conf +59 -0
- _/supports/systemd/__init__.py +60 -0
- _/supports/systemd/systemd.service +12 -0
- _/test.py +20 -0
- _/utils.py +85 -0
- _/version.py +5 -0
- _/websockets.py +57 -0
- underscorepy-0.9.0.dist-info/METADATA +85 -0
- underscorepy-0.9.0.dist-info/RECORD +39 -0
- underscorepy-0.9.0.dist-info/WHEEL +5 -0
- underscorepy-0.9.0.dist-info/licenses/LICENSE +22 -0
- underscorepy-0.9.0.dist-info/top_level.txt +1 -0
_/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
|