keepup-http 0.5.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.
Files changed (43) hide show
  1. keepup_http/__init__.py +38 -0
  2. keepup_http/api_docs.py +76 -0
  3. keepup_http/body_limit.py +156 -0
  4. keepup_http/events_api.py +237 -0
  5. keepup_http/factory.py +562 -0
  6. keepup_http/lock_routes.py +155 -0
  7. keepup_http/plugin.py +53 -0
  8. keepup_http/routes.py +549 -0
  9. keepup_http/static/css/aos.css +1 -0
  10. keepup_http/static/css/main_nebula.css +232 -0
  11. keepup_http/static/css/main_new.css +869 -0
  12. keepup_http/static/css/tailwind.css +1 -0
  13. keepup_http/static/index_nebula.html +282 -0
  14. keepup_http/static/index_new.html +275 -0
  15. keepup_http/static/js/aos.js +1 -0
  16. keepup_http/static/js/feather-icons.js +13 -0
  17. keepup_http/static/js/layout_bootstrap.js +29 -0
  18. keepup_http/static/js/main_new.js +2076 -0
  19. keepup_http/static/js/tailwind.js +83 -0
  20. keepup_http/static/modules/css/background_tasks.css +233 -0
  21. keepup_http/static/modules/css/cluster.css +16 -0
  22. keepup_http/static/modules/css/event_manager.css +386 -0
  23. keepup_http/static/modules/css/integration_logs.css +33 -0
  24. keepup_http/static/modules/css/metrics.css +115 -0
  25. keepup_http/static/modules/css/modules.css +189 -0
  26. keepup_http/static/modules/css/themes.css +563 -0
  27. keepup_http/static/modules/css/users.css +278 -0
  28. keepup_http/static/modules/js/background_tasks.js +695 -0
  29. keepup_http/static/modules/js/chart.js +14 -0
  30. keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
  31. keepup_http/static/modules/js/cluster.js +374 -0
  32. keepup_http/static/modules/js/event_manager.js +1007 -0
  33. keepup_http/static/modules/js/integration_logs.js +795 -0
  34. keepup_http/static/modules/js/metrics.js +929 -0
  35. keepup_http/static/modules/js/modules.js +1128 -0
  36. keepup_http/static/modules/js/themes.js +684 -0
  37. keepup_http/static/modules/js/users.js +898 -0
  38. keepup_http/web.py +214 -0
  39. keepup_http-0.5.0.dist-info/METADATA +29 -0
  40. keepup_http-0.5.0.dist-info/RECORD +43 -0
  41. keepup_http-0.5.0.dist-info/WHEEL +5 -0
  42. keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
  43. keepup_http-0.5.0.dist-info/top_level.txt +1 -0
keepup_http/routes.py ADDED
@@ -0,0 +1,549 @@
1
+ """How a declared route becomes an endpoint of the application.
2
+
3
+ A plugin's routes are data -- a path, a set of methods, a handler and a few
4
+ flags -- and this module is the single place where that data turns into
5
+ something FastAPI will call. The wrapper it builds signs the caller in, unpacks
6
+ the path and the query into the handler's arguments, parses a body when the
7
+ method carries one, and writes the call to the incoming request audit.
8
+
9
+ Single is the point. There used to be eight nearly identical wrappers here,
10
+ one per shape of route, and every change had to be made in all of them: the
11
+ request mask added a hundred and fifty lines by being added eight times, and
12
+ two of the eight had drifted apart from the rest without anybody noticing
13
+ (keepup-21). Every route of every plugin of every application is born here, so
14
+ what this module does once, it does to all of them.
15
+ """
16
+
17
+ import json
18
+ import logging
19
+
20
+ from fastapi import Depends, HTTPException, Request, Response
21
+ from starlette.requests import ClientDisconnect
22
+
23
+ from keepup_auth.websocket import authenticate_websocket
24
+ from keepup_http.body_limit import declare_limit
25
+ from keepup.kernel import observability, security
26
+ from keepup.kernel.call import (
27
+ KIND_WRITE,
28
+ Call,
29
+ CallError,
30
+ RouteSpec,
31
+ admit,
32
+ admitted,
33
+ invoke as invoke_route,
34
+ route_kind,
35
+ )
36
+ from keepup.plugins import route_mask
37
+
38
+ #: What an application may import from this module. Everything else is
39
+ #: internal and may change without notice -- see doc/keepup.md.
40
+ __all__ = [
41
+ "accepted_params",
42
+ "raw_request_wrapper",
43
+ "register_plugin_routes",
44
+ ]
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ #: The media type whose body the runtime parses. One type, compared as a media
49
+ #: type and not as a whole header -- see media_type().
50
+ JSON_MEDIA_TYPE = "application/json"
51
+
52
+ #: The structured syntax suffix of types that are JSON by construction
53
+ #: (RFC 6839): application/merge-patch+json and friends. They are recognised in
54
+ #: order to be named in the log, not in order to be read -- a route declares
55
+ #: what it accepts.
56
+ JSON_SUFFIX = "+json"
57
+
58
+
59
+ async def current_subject(request: Request):
60
+ """Who is calling, asked of whatever this deployment put behind ``auth``.
61
+
62
+ The kernel owns the question and this owns the HTTP half of it: a deployment
63
+ with no sign-in configured answers 401 rather than failing to import
64
+ something (keepup-119).
65
+
66
+ Args:
67
+ request: the request being served.
68
+
69
+ Returns:
70
+ The caller, in whatever shape the deployment's identity system uses.
71
+
72
+ Raises:
73
+ HTTPException: 401, when this deployment has no identity system at all.
74
+ """
75
+ dependency = security.subject_dependency()
76
+ if dependency is None:
77
+ raise HTTPException(status_code=401, detail="sign-in is not configured")
78
+ return await dependency(request)
79
+
80
+
81
+ def raw_request_wrapper(handler):
82
+ """A route whose handler takes the request as it is and answers as it likes.
83
+
84
+ For surfaces with a wire format of their own -- the OpenAI-compatible gateway
85
+ answers errors in OpenAI's shape and streams server-sent events. The handler
86
+ authenticates the caller itself, and the body is not written to the incoming
87
+ request log: it carries users' prompts.
88
+ """
89
+ async def wrapper_raw(request: Request):
90
+ return await handler(request=request)
91
+
92
+ return wrapper_raw
93
+
94
+
95
+ def accepted_params(handler, params):
96
+ """The parameters this handler declares, refusing the ones it does not.
97
+
98
+ The decision is the kernel's (:func:`keepup.kernel.call.admitted`); what an
99
+ HTTP caller reads is the refusal, which is a 400 here.
100
+
101
+ Args:
102
+ handler: the plugin's callable.
103
+ params: what arrived in the path and the query.
104
+
105
+ Returns:
106
+ The parameters to pass.
107
+
108
+ Raises:
109
+ HTTPException: 400, naming the parameters the route does not take.
110
+ """
111
+ try:
112
+ return admitted(handler, params)
113
+ except CallError as refused:
114
+ raise HTTPException(status_code=refused.http_status, detail=refused.detail) from refused
115
+
116
+
117
+ def admit_params(handler, mask, params):
118
+ """What reaches the handler: the mask decides, or the signature still does.
119
+
120
+ A route that declared a mask is checked against it -- names, types,
121
+ required, allowed values -- and the values arrive converted. A route without
122
+ one keeps the previous behaviour exactly, which is the condition of the
123
+ change: the applications convert their routes by their own tasks, and a
124
+ framework that changed the default would have turned every route of every
125
+ application into a refusal at once. See doc/keepup.md.
126
+
127
+ Args:
128
+ handler: the plugin's callable.
129
+ mask: the route's parsed mask, or None.
130
+ params: what arrived in the path and the query.
131
+
132
+ Returns:
133
+ The parameters to pass.
134
+
135
+ Raises:
136
+ HTTPException: 400, naming everything the request got wrong.
137
+ """
138
+ spec = RouteSpec(path="", methods=(), handler=handler, mask=mask)
139
+ try:
140
+ return admit(spec, params)
141
+ except CallError as refused:
142
+ raise HTTPException(status_code=refused.http_status, detail=refused.detail) from refused
143
+
144
+
145
+ def media_type(header):
146
+ """The media type of a Content-Type header, without its parameters.
147
+
148
+ Lowercased and stripped of everything after the semicolon, because that is
149
+ what the standard says the type is: `application/json; charset=utf-8` and
150
+ `Application/JSON` name the same type as `application/json`. Comparing the
151
+ header as a whole string -- which is what this replaced -- lost the body of
152
+ every client that writes a charset, and most write one by default.
153
+
154
+ Args:
155
+ header: the Content-Type header, or None when there is none.
156
+
157
+ Returns:
158
+ The media type in lower case, or an empty string when the header is
159
+ absent.
160
+ """
161
+ return (header or "").split(";", 1)[0].strip().lower()
162
+
163
+
164
+ async def json_body(request: Request, path: str = None):
165
+ """The body of a write, parsed, or an empty one.
166
+
167
+ The content type decides, not the bytes: a handler written for a dictionary
168
+ would otherwise be handed a string the first time somebody posted a form.
169
+ A body that claims to be JSON and is not becomes empty rather than a
170
+ refusal -- the plugin is handed {} and decides for itself, because a
171
+ handler that needs a field says so by missing it.
172
+
173
+ That tolerance stays, but it is no longer silent. An empty body used to
174
+ mean three different things at once -- there was none, the type was not
175
+ JSON, or it arrived and could not be read -- and the handler, unable to
176
+ tell them apart, answered "field required" about a field the client had
177
+ sent. An agent writing its body in the machine's code page cost three rounds
178
+ of searching that way, with the word "encoding" in neither log. A body that
179
+ claims to be JSON and does not parse is now named in the log.
180
+
181
+ Args:
182
+ request: the request being served.
183
+ path: the route's path as declared, for the log line. Defaults to the
184
+ address of this request, which names an instance rather than the
185
+ route.
186
+
187
+ Returns:
188
+ The parsed body, or {} when there is none, the type is not JSON, or it
189
+ could not be read.
190
+ """
191
+ declared = media_type(request.headers.get("content-type"))
192
+ if declared != JSON_MEDIA_TYPE:
193
+ if declared.endswith(JSON_SUFFIX):
194
+ # JSON by construction (RFC 6839) and refused anyway: a route
195
+ # declares what it accepts, and a media type nobody declared must
196
+ # not start arriving because the runtime grew lenient. Named in the
197
+ # log, because the point of this whole function is that a body
198
+ # nobody reads does not vanish quietly.
199
+ logger.warning(
200
+ "body of a media type the runtime does not read: "
201
+ "route=%s method=%s content-type=%s",
202
+ path or request.url.path, request.method, declared)
203
+ return {}
204
+ body = None
205
+ try:
206
+ body = await request.body()
207
+ if not body:
208
+ # A write with no body at all is ordinary -- a great many routes
209
+ # take everything in the path. Warning about it would bury the
210
+ # lines this exists to produce.
211
+ return {}
212
+ return json.loads(body)
213
+ except ClientDisconnect:
214
+ # The body limit tells the application the client went away once a body
215
+ # without a declared length passes the route's limit. An empty body
216
+ # here would run the handler -- and its writes -- under a 413 the client
217
+ # was already answered with (keepup-61).
218
+ raise
219
+ except Exception as exc:
220
+ # Not a bare except, which used to be here: CancelledError does not
221
+ # inherit from Exception, and swallowing it turned a cancelled request
222
+ # into an empty body and a handler that ran on anyway.
223
+ #
224
+ # The body itself is never written: passwords and keys go through these
225
+ # routes. The length says one arrived without saying what was in it,
226
+ # and the exception text names a position and at most the single byte
227
+ # that could not be decoded -- which is what makes an encoding fault
228
+ # diagnosable at all. "unread" rather than 0 when reading itself
229
+ # failed: a client that disconnected mid-request sent a length nobody
230
+ # knows, and 0 would read as "sent nothing".
231
+ logger.warning(
232
+ "body declared as JSON could not be read: "
233
+ "route=%s method=%s content-type=%s length=%s reason=%s: %s",
234
+ path or request.url.path, request.method,
235
+ request.headers.get("content-type"),
236
+ len(body) if body is not None else "unread",
237
+ type(exc).__name__, exc)
238
+ return {}
239
+
240
+
241
+ def create_wrapper(handler, path, methods, require_auth: bool = True,
242
+ is_upload: bool = False, mask=None, permission: str = None,
243
+ audit: bool = True, response_media_type: str = None):
244
+ """Build the endpoint FastAPI will call for one declared route.
245
+
246
+ One implementation rather than one per shape. It used to be eight nearly
247
+ identical copies -- three shapes x a path parameter x authentication -- and
248
+ two of them had quietly drifted apart from the rest (keepup-21).
249
+
250
+ Two signatures remain, and only because FastAPI builds an endpoint out of
251
+ one: a route that is signed in has to declare the dependency, and a public
252
+ route must not.
253
+
254
+ Args:
255
+ handler: the plugin's callable.
256
+ path: the route's path as declared, parameters and all.
257
+ methods: the methods the route declares.
258
+ require_auth: whether the framework signs the caller in.
259
+ is_upload: whether the handler is handed the request instead of a body.
260
+ mask: the route's parsed mask, or None.
261
+ permission: the right the caller must hold, or None
262
+ (keepup/auth/identity/access.py).
263
+ audit: whether the call is written to the incoming-request audit. A
264
+ route that answers a monitor is asked every few seconds by every
265
+ replica, and a row per scrape is not an audit (keepup-102).
266
+ response_media_type: what the answer is, when it is not JSON -- text
267
+ for a metrics scrape, for one. A handler that returns a Response
268
+ itself is passed through either way.
269
+
270
+ Returns:
271
+ An async endpoint to hand to app.add_api_route().
272
+ """
273
+ kind, method_name = route_kind(methods)
274
+ if kind is None:
275
+ logger.warning(f"No matching wrapper found for {path} with methods {methods}")
276
+
277
+ # It takes nothing on purpose. Written as (*args, **kwargs) it never
278
+ # answered 501 at all: FastAPI reads a signature to build the endpoint,
279
+ # so those two became required query parameters and the route answered
280
+ # 422 "args, kwargs: field required" (keepup-20).
281
+ async def fallback_wrapper():
282
+ raise HTTPException(status_code=501,
283
+ detail=f"Not implemented for methods {methods}")
284
+
285
+ return fallback_wrapper
286
+
287
+ has_path_params = any('{' in part and '}' in part for part in path.split('/'))
288
+
289
+ def as_answer(value):
290
+ """A handler's value as the answer: a Response as it is, text when asked."""
291
+ if response_media_type is None or isinstance(value, Response):
292
+ return value
293
+ return Response(content="" if value is None else str(value),
294
+ media_type=response_media_type)
295
+
296
+ spec = RouteSpec(
297
+ path=path,
298
+ methods=tuple(str(item).upper() for item in methods or ()),
299
+ handler=handler,
300
+ mask=mask,
301
+ permission=permission,
302
+ require_auth=require_auth,
303
+ is_upload=is_upload,
304
+ audit=audit,
305
+ response_media_type=response_media_type,
306
+ )
307
+
308
+ async def invoke(request: Request, current_user):
309
+ """What the handler does, with or without the audit around it.
310
+
311
+ The reading of the request is the HTTP adapter's; everything after it --
312
+ the mask, the caller, the body, the right, the answer -- is the
313
+ transport-neutral invocation of ``keepup.kernel.call``.
314
+ """
315
+ params = {}
316
+ # The query first and the path over it: a value taken from the address
317
+ # is what the route matched on, and a client that sends ?id=99 to
318
+ # /api/x/5 used to reach the handler with 99 while every log and every
319
+ # rule built on the path still said 5.
320
+ params.update(request.query_params)
321
+ params.update(request.path_params)
322
+
323
+ body = None
324
+ if kind == KIND_WRITE:
325
+ # An upload is handed the request itself: reading a file into
326
+ # memory to pass it as a value is what such a route exists to avoid.
327
+ body = request if is_upload else await json_body(request, path)
328
+
329
+ call = Call(route=spec, params=params, actor=current_user, body=body, source="http")
330
+ try:
331
+ answer = await invoke_route(call, checker=security.checker())
332
+ except CallError as refused:
333
+ raise HTTPException(status_code=refused.http_status, detail=refused.detail) from refused
334
+ return as_answer(answer)
335
+
336
+ async def call(request: Request, current_user):
337
+ """One call of a plugin route, from the audit's first row to its last."""
338
+ request_data = {
339
+ 'user_id': current_user.get('id') if current_user else None,
340
+ 'username': current_user.get('username') if current_user else 'anonymous',
341
+ }
342
+ if has_path_params:
343
+ request_data['path_params'] = dict(request.path_params)
344
+ request_data['query_params'] = dict(request.query_params)
345
+ if kind == KIND_WRITE:
346
+ request_data['is_upload'] = is_upload
347
+
348
+ if not audit:
349
+ # A route that said so is not written down: no context, no row.
350
+ return await invoke(request, current_user)
351
+
352
+ async with observability.start_call(
353
+ method=method_name,
354
+ endpoint=path,
355
+ host='plugin_api',
356
+ request_data=request_data
357
+ ) as request_id:
358
+ try:
359
+ response = await invoke(request, current_user)
360
+
361
+ await observability.end_call(
362
+ request_id=request_id,
363
+ http_status=200,
364
+ response_data=response if isinstance(response, dict) else {"data": response}
365
+ )
366
+
367
+ return response
368
+ except HTTPException as e:
369
+ await observability.end_call(
370
+ request_id=request_id,
371
+ http_status=e.status_code,
372
+ error_message=e.detail
373
+ )
374
+ raise
375
+ except Exception as e:
376
+ # The kind of failure, not its text: the text of an unexpected
377
+ # exception carries whatever the failing code held -- values,
378
+ # queries, secrets -- and the audit's redaction never sees it
379
+ # (keepup-76). The text goes to the application log.
380
+ await observability.end_call(
381
+ request_id=request_id,
382
+ http_status=500,
383
+ error_message=type(e).__name__
384
+ )
385
+ raise
386
+
387
+ if require_auth:
388
+ # The dependency FastAPI resolves, chosen when the route is registered:
389
+ # whatever the deployment put behind `auth` -- the framework's own
390
+ # sign-in until keepup-auth is a plugin -- or this module's refusal, so
391
+ # that a process with no identity system answers 401 instead of failing
392
+ # to import one (keepup-119). A dependency, not a call: the registered
393
+ # one has sub-dependencies of its own, and FastAPI is what resolves
394
+ # them.
395
+ dependency = security.subject_dependency() or current_subject
396
+
397
+ async def wrapper_signed_in(request: Request,
398
+ current_user: dict = Depends(dependency)):
399
+ return await call(request, current_user)
400
+
401
+ return wrapper_signed_in
402
+
403
+ async def wrapper_public(request: Request):
404
+ return await call(request, None)
405
+
406
+ return wrapper_public
407
+
408
+
409
+ async def register_plugin_routes(app, manager):
410
+ """Register every plugin's API routes on the FastAPI application."""
411
+
412
+ routes = manager.get_all_api_routes()
413
+ logger.info(f"Found {len(routes)} plugin routes to register")
414
+
415
+ for route in routes:
416
+ handler = route['handler']
417
+ path = route['path']
418
+ methods = route['methods']
419
+ require_auth = route.get('require_auth', True)
420
+ is_upload = route.get('is_upload', False)
421
+ permission = route.get('permission')
422
+ audit = route.get('audit', True)
423
+ response_media_type = route.get('response_media_type')
424
+ if response_media_type is not None and (
425
+ not isinstance(response_media_type, str) or not response_media_type.strip()):
426
+ raise ValueError(
427
+ f"{path}: response_media_type is a media type, like 'text/plain'")
428
+ if response_media_type is not None and route.get('raw_request'):
429
+ raise ValueError(
430
+ f"{path}: a raw_request route answers with a Response of its own; "
431
+ "response_media_type would be ignored. Drop one of the two.")
432
+ if permission is not None:
433
+ # A right needs somebody to hold it: on a route nobody signs in to,
434
+ # or one that signs itself in, it would be a check that never runs.
435
+ if not isinstance(permission, str) or not permission.strip():
436
+ raise ValueError(f"{path}: permission must be a non-empty string")
437
+ if route.get('raw_request') or not require_auth:
438
+ raise ValueError(
439
+ f"{path}: a route that declares a permission must be signed in by "
440
+ "the framework -- drop require_auth: false or raw_request, or the "
441
+ "permission")
442
+
443
+ logger.info(
444
+ f"Registering route {path} - methods: {methods}, auth: {require_auth}, upload: {is_upload}")
445
+
446
+ # The kernel parsed and checked the mask when it collected the route,
447
+ # for every transport there will ever be (keepup-102); a route that
448
+ # reaches here without one was declared by something else, and is
449
+ # checked the same way rather than trusted.
450
+ mask = route.get('_mask')
451
+ if mask is None:
452
+ try:
453
+ mask = route_mask.parse(route.get(route_mask.MASK_FIELD), path)
454
+ route_mask.check_signature(handler, mask)
455
+ except route_mask.MaskError as error:
456
+ # The same loudness as the raw_request check below: a mask naming
457
+ # a parameter the handler does not take is a typo, and finding it
458
+ # on a request would mean hearing about it from a user.
459
+ raise ValueError(f"{path}: {error}")
460
+
461
+ if route.get('raw_request'):
462
+ # A raw route authenticates inside the handler -- it is handed the
463
+ # Request and nothing else. Declaring require_auth on it therefore
464
+ # promises a check the framework does not make, and the start-up
465
+ # line used to print auth=True over an open route (keepup-12).
466
+ if route.get('require_auth') is True:
467
+ raise ValueError(
468
+ f"{path}: a raw_request route cannot also declare "
469
+ "require_auth -- the handler receives the Request and "
470
+ "authenticates itself. Drop one of the two.")
471
+ require_auth = False
472
+ wrapper = raw_request_wrapper(handler)
473
+ else:
474
+ wrapper = create_wrapper(handler, path, methods, require_auth, is_upload, mask,
475
+ permission, audit=audit,
476
+ response_media_type=response_media_type)
477
+
478
+ # What the route says about its body: its own limit, or that it reads
479
+ # the body itself (keepup/body_limit.py).
480
+ declare_limit(wrapper, route)
481
+
482
+ app.add_api_route(
483
+ path,
484
+ wrapper,
485
+ methods=methods,
486
+ include_in_schema=route.get('include_in_schema', True)
487
+ )
488
+
489
+ logger.info(
490
+ f"Registered plugin route: {path} - {methods} (auth={require_auth}, upload={is_upload})")
491
+
492
+ websocket_routes = manager.get_all_websocket_routes()
493
+ logger.info(f"Found {len(websocket_routes)} WebSocket plugin routes to register")
494
+
495
+ for ws_route in websocket_routes:
496
+ path = ws_route['path']
497
+ handler = ws_route['handler']
498
+ # Off by default, unlike HTTP routes: sockets of agents and guests carry
499
+ # tokens of their own and would be refused by the panel's sign-in rule.
500
+ if ws_route.get('require_auth', False):
501
+ handler = signed_in_websocket(handler)
502
+ _register_websocket(app, path, handler)
503
+ logger.info(f"Registered WebSocket plugin route: {path} "
504
+ f"(auth={bool(ws_route.get('require_auth', False))})")
505
+
506
+
507
+ def _register_websocket(app, path, handler):
508
+ """Register a socket route on the application, whichever Starlette it has.
509
+
510
+ The route is appended as a plain ``WebSocketRoute`` rather than through the
511
+ application's helpers, because neither helper works on both lines:
512
+ Starlette 1.0 dropped ``add_websocket_route``, and FastAPI's
513
+ ``add_api_websocket_route`` reads the handler's signature and refuses one
514
+ whose socket parameter carries no annotation -- which a plugin's handler
515
+ need not carry.
516
+
517
+ It is done this way rather than by pinning Starlette below 1.0, because
518
+ that pin is what held installations on a version with four published
519
+ advisories (keepup-30).
520
+
521
+ Args:
522
+ app: The FastAPI application.
523
+ path: The address the socket answers on.
524
+ handler: The coroutine serving the connection.
525
+ """
526
+ from starlette.routing import WebSocketRoute
527
+
528
+ app.router.routes.append(WebSocketRoute(path, handler))
529
+
530
+
531
+ def signed_in_websocket(handler):
532
+ """Wrap a socket handler so it runs only for a signed-in user, who is passed in.
533
+
534
+ Refusal closes the socket with 1008 before the handler sees it.
535
+ """
536
+ async def wrapper(websocket):
537
+ user = await authenticate_websocket(websocket)
538
+ if user is None:
539
+ return
540
+ await handler(websocket, current_user=user)
541
+
542
+ return wrapper
543
+
544
+
545
+ # --- Post-construct and the administrative surface ----------------------------
546
+ #
547
+ # post_construct runs after the server is up, for the self-checks that need a
548
+ # live HTTP server; the endpoints below are what an administrator sees of the
549
+ # runtime -- which plugins are declared, which are running, and why one is not.
@@ -0,0 +1 @@
1
+ [data-aos][data-aos][data-aos-duration="50"],body[data-aos-duration="50"] [data-aos]{transition-duration:50ms}[data-aos][data-aos][data-aos-delay="50"],body[data-aos-delay="50"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="50"].aos-animate,body[data-aos-delay="50"] [data-aos].aos-animate{transition-delay:50ms}[data-aos][data-aos][data-aos-duration="100"],body[data-aos-duration="100"] [data-aos]{transition-duration:.1s}[data-aos][data-aos][data-aos-delay="100"],body[data-aos-delay="100"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="100"].aos-animate,body[data-aos-delay="100"] [data-aos].aos-animate{transition-delay:.1s}[data-aos][data-aos][data-aos-duration="150"],body[data-aos-duration="150"] [data-aos]{transition-duration:.15s}[data-aos][data-aos][data-aos-delay="150"],body[data-aos-delay="150"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="150"].aos-animate,body[data-aos-delay="150"] [data-aos].aos-animate{transition-delay:.15s}[data-aos][data-aos][data-aos-duration="200"],body[data-aos-duration="200"] [data-aos]{transition-duration:.2s}[data-aos][data-aos][data-aos-delay="200"],body[data-aos-delay="200"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="200"].aos-animate,body[data-aos-delay="200"] [data-aos].aos-animate{transition-delay:.2s}[data-aos][data-aos][data-aos-duration="250"],body[data-aos-duration="250"] [data-aos]{transition-duration:.25s}[data-aos][data-aos][data-aos-delay="250"],body[data-aos-delay="250"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="250"].aos-animate,body[data-aos-delay="250"] [data-aos].aos-animate{transition-delay:.25s}[data-aos][data-aos][data-aos-duration="300"],body[data-aos-duration="300"] [data-aos]{transition-duration:.3s}[data-aos][data-aos][data-aos-delay="300"],body[data-aos-delay="300"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="300"].aos-animate,body[data-aos-delay="300"] [data-aos].aos-animate{transition-delay:.3s}[data-aos][data-aos][data-aos-duration="350"],body[data-aos-duration="350"] [data-aos]{transition-duration:.35s}[data-aos][data-aos][data-aos-delay="350"],body[data-aos-delay="350"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="350"].aos-animate,body[data-aos-delay="350"] [data-aos].aos-animate{transition-delay:.35s}[data-aos][data-aos][data-aos-duration="400"],body[data-aos-duration="400"] [data-aos]{transition-duration:.4s}[data-aos][data-aos][data-aos-delay="400"],body[data-aos-delay="400"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="400"].aos-animate,body[data-aos-delay="400"] [data-aos].aos-animate{transition-delay:.4s}[data-aos][data-aos][data-aos-duration="450"],body[data-aos-duration="450"] [data-aos]{transition-duration:.45s}[data-aos][data-aos][data-aos-delay="450"],body[data-aos-delay="450"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="450"].aos-animate,body[data-aos-delay="450"] [data-aos].aos-animate{transition-delay:.45s}[data-aos][data-aos][data-aos-duration="500"],body[data-aos-duration="500"] [data-aos]{transition-duration:.5s}[data-aos][data-aos][data-aos-delay="500"],body[data-aos-delay="500"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="500"].aos-animate,body[data-aos-delay="500"] [data-aos].aos-animate{transition-delay:.5s}[data-aos][data-aos][data-aos-duration="550"],body[data-aos-duration="550"] [data-aos]{transition-duration:.55s}[data-aos][data-aos][data-aos-delay="550"],body[data-aos-delay="550"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="550"].aos-animate,body[data-aos-delay="550"] [data-aos].aos-animate{transition-delay:.55s}[data-aos][data-aos][data-aos-duration="600"],body[data-aos-duration="600"] [data-aos]{transition-duration:.6s}[data-aos][data-aos][data-aos-delay="600"],body[data-aos-delay="600"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="600"].aos-animate,body[data-aos-delay="600"] [data-aos].aos-animate{transition-delay:.6s}[data-aos][data-aos][data-aos-duration="650"],body[data-aos-duration="650"] [data-aos]{transition-duration:.65s}[data-aos][data-aos][data-aos-delay="650"],body[data-aos-delay="650"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="650"].aos-animate,body[data-aos-delay="650"] [data-aos].aos-animate{transition-delay:.65s}[data-aos][data-aos][data-aos-duration="700"],body[data-aos-duration="700"] [data-aos]{transition-duration:.7s}[data-aos][data-aos][data-aos-delay="700"],body[data-aos-delay="700"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="700"].aos-animate,body[data-aos-delay="700"] [data-aos].aos-animate{transition-delay:.7s}[data-aos][data-aos][data-aos-duration="750"],body[data-aos-duration="750"] [data-aos]{transition-duration:.75s}[data-aos][data-aos][data-aos-delay="750"],body[data-aos-delay="750"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="750"].aos-animate,body[data-aos-delay="750"] [data-aos].aos-animate{transition-delay:.75s}[data-aos][data-aos][data-aos-duration="800"],body[data-aos-duration="800"] [data-aos]{transition-duration:.8s}[data-aos][data-aos][data-aos-delay="800"],body[data-aos-delay="800"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="800"].aos-animate,body[data-aos-delay="800"] [data-aos].aos-animate{transition-delay:.8s}[data-aos][data-aos][data-aos-duration="850"],body[data-aos-duration="850"] [data-aos]{transition-duration:.85s}[data-aos][data-aos][data-aos-delay="850"],body[data-aos-delay="850"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="850"].aos-animate,body[data-aos-delay="850"] [data-aos].aos-animate{transition-delay:.85s}[data-aos][data-aos][data-aos-duration="900"],body[data-aos-duration="900"] [data-aos]{transition-duration:.9s}[data-aos][data-aos][data-aos-delay="900"],body[data-aos-delay="900"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="900"].aos-animate,body[data-aos-delay="900"] [data-aos].aos-animate{transition-delay:.9s}[data-aos][data-aos][data-aos-duration="950"],body[data-aos-duration="950"] [data-aos]{transition-duration:.95s}[data-aos][data-aos][data-aos-delay="950"],body[data-aos-delay="950"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="950"].aos-animate,body[data-aos-delay="950"] [data-aos].aos-animate{transition-delay:.95s}[data-aos][data-aos][data-aos-duration="1000"],body[data-aos-duration="1000"] [data-aos]{transition-duration:1s}[data-aos][data-aos][data-aos-delay="1000"],body[data-aos-delay="1000"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1000"].aos-animate,body[data-aos-delay="1000"] [data-aos].aos-animate{transition-delay:1s}[data-aos][data-aos][data-aos-duration="1050"],body[data-aos-duration="1050"] [data-aos]{transition-duration:1.05s}[data-aos][data-aos][data-aos-delay="1050"],body[data-aos-delay="1050"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1050"].aos-animate,body[data-aos-delay="1050"] [data-aos].aos-animate{transition-delay:1.05s}[data-aos][data-aos][data-aos-duration="1100"],body[data-aos-duration="1100"] [data-aos]{transition-duration:1.1s}[data-aos][data-aos][data-aos-delay="1100"],body[data-aos-delay="1100"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1100"].aos-animate,body[data-aos-delay="1100"] [data-aos].aos-animate{transition-delay:1.1s}[data-aos][data-aos][data-aos-duration="1150"],body[data-aos-duration="1150"] [data-aos]{transition-duration:1.15s}[data-aos][data-aos][data-aos-delay="1150"],body[data-aos-delay="1150"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1150"].aos-animate,body[data-aos-delay="1150"] [data-aos].aos-animate{transition-delay:1.15s}[data-aos][data-aos][data-aos-duration="1200"],body[data-aos-duration="1200"] [data-aos]{transition-duration:1.2s}[data-aos][data-aos][data-aos-delay="1200"],body[data-aos-delay="1200"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1200"].aos-animate,body[data-aos-delay="1200"] [data-aos].aos-animate{transition-delay:1.2s}[data-aos][data-aos][data-aos-duration="1250"],body[data-aos-duration="1250"] [data-aos]{transition-duration:1.25s}[data-aos][data-aos][data-aos-delay="1250"],body[data-aos-delay="1250"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1250"].aos-animate,body[data-aos-delay="1250"] [data-aos].aos-animate{transition-delay:1.25s}[data-aos][data-aos][data-aos-duration="1300"],body[data-aos-duration="1300"] [data-aos]{transition-duration:1.3s}[data-aos][data-aos][data-aos-delay="1300"],body[data-aos-delay="1300"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1300"].aos-animate,body[data-aos-delay="1300"] [data-aos].aos-animate{transition-delay:1.3s}[data-aos][data-aos][data-aos-duration="1350"],body[data-aos-duration="1350"] [data-aos]{transition-duration:1.35s}[data-aos][data-aos][data-aos-delay="1350"],body[data-aos-delay="1350"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1350"].aos-animate,body[data-aos-delay="1350"] [data-aos].aos-animate{transition-delay:1.35s}[data-aos][data-aos][data-aos-duration="1400"],body[data-aos-duration="1400"] [data-aos]{transition-duration:1.4s}[data-aos][data-aos][data-aos-delay="1400"],body[data-aos-delay="1400"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1400"].aos-animate,body[data-aos-delay="1400"] [data-aos].aos-animate{transition-delay:1.4s}[data-aos][data-aos][data-aos-duration="1450"],body[data-aos-duration="1450"] [data-aos]{transition-duration:1.45s}[data-aos][data-aos][data-aos-delay="1450"],body[data-aos-delay="1450"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1450"].aos-animate,body[data-aos-delay="1450"] [data-aos].aos-animate{transition-delay:1.45s}[data-aos][data-aos][data-aos-duration="1500"],body[data-aos-duration="1500"] [data-aos]{transition-duration:1.5s}[data-aos][data-aos][data-aos-delay="1500"],body[data-aos-delay="1500"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1500"].aos-animate,body[data-aos-delay="1500"] [data-aos].aos-animate{transition-delay:1.5s}[data-aos][data-aos][data-aos-duration="1550"],body[data-aos-duration="1550"] [data-aos]{transition-duration:1.55s}[data-aos][data-aos][data-aos-delay="1550"],body[data-aos-delay="1550"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1550"].aos-animate,body[data-aos-delay="1550"] [data-aos].aos-animate{transition-delay:1.55s}[data-aos][data-aos][data-aos-duration="1600"],body[data-aos-duration="1600"] [data-aos]{transition-duration:1.6s}[data-aos][data-aos][data-aos-delay="1600"],body[data-aos-delay="1600"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1600"].aos-animate,body[data-aos-delay="1600"] [data-aos].aos-animate{transition-delay:1.6s}[data-aos][data-aos][data-aos-duration="1650"],body[data-aos-duration="1650"] [data-aos]{transition-duration:1.65s}[data-aos][data-aos][data-aos-delay="1650"],body[data-aos-delay="1650"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1650"].aos-animate,body[data-aos-delay="1650"] [data-aos].aos-animate{transition-delay:1.65s}[data-aos][data-aos][data-aos-duration="1700"],body[data-aos-duration="1700"] [data-aos]{transition-duration:1.7s}[data-aos][data-aos][data-aos-delay="1700"],body[data-aos-delay="1700"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1700"].aos-animate,body[data-aos-delay="1700"] [data-aos].aos-animate{transition-delay:1.7s}[data-aos][data-aos][data-aos-duration="1750"],body[data-aos-duration="1750"] [data-aos]{transition-duration:1.75s}[data-aos][data-aos][data-aos-delay="1750"],body[data-aos-delay="1750"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1750"].aos-animate,body[data-aos-delay="1750"] [data-aos].aos-animate{transition-delay:1.75s}[data-aos][data-aos][data-aos-duration="1800"],body[data-aos-duration="1800"] [data-aos]{transition-duration:1.8s}[data-aos][data-aos][data-aos-delay="1800"],body[data-aos-delay="1800"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1800"].aos-animate,body[data-aos-delay="1800"] [data-aos].aos-animate{transition-delay:1.8s}[data-aos][data-aos][data-aos-duration="1850"],body[data-aos-duration="1850"] [data-aos]{transition-duration:1.85s}[data-aos][data-aos][data-aos-delay="1850"],body[data-aos-delay="1850"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1850"].aos-animate,body[data-aos-delay="1850"] [data-aos].aos-animate{transition-delay:1.85s}[data-aos][data-aos][data-aos-duration="1900"],body[data-aos-duration="1900"] [data-aos]{transition-duration:1.9s}[data-aos][data-aos][data-aos-delay="1900"],body[data-aos-delay="1900"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1900"].aos-animate,body[data-aos-delay="1900"] [data-aos].aos-animate{transition-delay:1.9s}[data-aos][data-aos][data-aos-duration="1950"],body[data-aos-duration="1950"] [data-aos]{transition-duration:1.95s}[data-aos][data-aos][data-aos-delay="1950"],body[data-aos-delay="1950"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1950"].aos-animate,body[data-aos-delay="1950"] [data-aos].aos-animate{transition-delay:1.95s}[data-aos][data-aos][data-aos-duration="2000"],body[data-aos-duration="2000"] [data-aos]{transition-duration:2s}[data-aos][data-aos][data-aos-delay="2000"],body[data-aos-delay="2000"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2000"].aos-animate,body[data-aos-delay="2000"] [data-aos].aos-animate{transition-delay:2s}[data-aos][data-aos][data-aos-duration="2050"],body[data-aos-duration="2050"] [data-aos]{transition-duration:2.05s}[data-aos][data-aos][data-aos-delay="2050"],body[data-aos-delay="2050"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2050"].aos-animate,body[data-aos-delay="2050"] [data-aos].aos-animate{transition-delay:2.05s}[data-aos][data-aos][data-aos-duration="2100"],body[data-aos-duration="2100"] [data-aos]{transition-duration:2.1s}[data-aos][data-aos][data-aos-delay="2100"],body[data-aos-delay="2100"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2100"].aos-animate,body[data-aos-delay="2100"] [data-aos].aos-animate{transition-delay:2.1s}[data-aos][data-aos][data-aos-duration="2150"],body[data-aos-duration="2150"] [data-aos]{transition-duration:2.15s}[data-aos][data-aos][data-aos-delay="2150"],body[data-aos-delay="2150"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2150"].aos-animate,body[data-aos-delay="2150"] [data-aos].aos-animate{transition-delay:2.15s}[data-aos][data-aos][data-aos-duration="2200"],body[data-aos-duration="2200"] [data-aos]{transition-duration:2.2s}[data-aos][data-aos][data-aos-delay="2200"],body[data-aos-delay="2200"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2200"].aos-animate,body[data-aos-delay="2200"] [data-aos].aos-animate{transition-delay:2.2s}[data-aos][data-aos][data-aos-duration="2250"],body[data-aos-duration="2250"] [data-aos]{transition-duration:2.25s}[data-aos][data-aos][data-aos-delay="2250"],body[data-aos-delay="2250"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2250"].aos-animate,body[data-aos-delay="2250"] [data-aos].aos-animate{transition-delay:2.25s}[data-aos][data-aos][data-aos-duration="2300"],body[data-aos-duration="2300"] [data-aos]{transition-duration:2.3s}[data-aos][data-aos][data-aos-delay="2300"],body[data-aos-delay="2300"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2300"].aos-animate,body[data-aos-delay="2300"] [data-aos].aos-animate{transition-delay:2.3s}[data-aos][data-aos][data-aos-duration="2350"],body[data-aos-duration="2350"] [data-aos]{transition-duration:2.35s}[data-aos][data-aos][data-aos-delay="2350"],body[data-aos-delay="2350"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2350"].aos-animate,body[data-aos-delay="2350"] [data-aos].aos-animate{transition-delay:2.35s}[data-aos][data-aos][data-aos-duration="2400"],body[data-aos-duration="2400"] [data-aos]{transition-duration:2.4s}[data-aos][data-aos][data-aos-delay="2400"],body[data-aos-delay="2400"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2400"].aos-animate,body[data-aos-delay="2400"] [data-aos].aos-animate{transition-delay:2.4s}[data-aos][data-aos][data-aos-duration="2450"],body[data-aos-duration="2450"] [data-aos]{transition-duration:2.45s}[data-aos][data-aos][data-aos-delay="2450"],body[data-aos-delay="2450"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2450"].aos-animate,body[data-aos-delay="2450"] [data-aos].aos-animate{transition-delay:2.45s}[data-aos][data-aos][data-aos-duration="2500"],body[data-aos-duration="2500"] [data-aos]{transition-duration:2.5s}[data-aos][data-aos][data-aos-delay="2500"],body[data-aos-delay="2500"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2500"].aos-animate,body[data-aos-delay="2500"] [data-aos].aos-animate{transition-delay:2.5s}[data-aos][data-aos][data-aos-duration="2550"],body[data-aos-duration="2550"] [data-aos]{transition-duration:2.55s}[data-aos][data-aos][data-aos-delay="2550"],body[data-aos-delay="2550"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2550"].aos-animate,body[data-aos-delay="2550"] [data-aos].aos-animate{transition-delay:2.55s}[data-aos][data-aos][data-aos-duration="2600"],body[data-aos-duration="2600"] [data-aos]{transition-duration:2.6s}[data-aos][data-aos][data-aos-delay="2600"],body[data-aos-delay="2600"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2600"].aos-animate,body[data-aos-delay="2600"] [data-aos].aos-animate{transition-delay:2.6s}[data-aos][data-aos][data-aos-duration="2650"],body[data-aos-duration="2650"] [data-aos]{transition-duration:2.65s}[data-aos][data-aos][data-aos-delay="2650"],body[data-aos-delay="2650"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2650"].aos-animate,body[data-aos-delay="2650"] [data-aos].aos-animate{transition-delay:2.65s}[data-aos][data-aos][data-aos-duration="2700"],body[data-aos-duration="2700"] [data-aos]{transition-duration:2.7s}[data-aos][data-aos][data-aos-delay="2700"],body[data-aos-delay="2700"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2700"].aos-animate,body[data-aos-delay="2700"] [data-aos].aos-animate{transition-delay:2.7s}[data-aos][data-aos][data-aos-duration="2750"],body[data-aos-duration="2750"] [data-aos]{transition-duration:2.75s}[data-aos][data-aos][data-aos-delay="2750"],body[data-aos-delay="2750"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2750"].aos-animate,body[data-aos-delay="2750"] [data-aos].aos-animate{transition-delay:2.75s}[data-aos][data-aos][data-aos-duration="2800"],body[data-aos-duration="2800"] [data-aos]{transition-duration:2.8s}[data-aos][data-aos][data-aos-delay="2800"],body[data-aos-delay="2800"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2800"].aos-animate,body[data-aos-delay="2800"] [data-aos].aos-animate{transition-delay:2.8s}[data-aos][data-aos][data-aos-duration="2850"],body[data-aos-duration="2850"] [data-aos]{transition-duration:2.85s}[data-aos][data-aos][data-aos-delay="2850"],body[data-aos-delay="2850"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2850"].aos-animate,body[data-aos-delay="2850"] [data-aos].aos-animate{transition-delay:2.85s}[data-aos][data-aos][data-aos-duration="2900"],body[data-aos-duration="2900"] [data-aos]{transition-duration:2.9s}[data-aos][data-aos][data-aos-delay="2900"],body[data-aos-delay="2900"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2900"].aos-animate,body[data-aos-delay="2900"] [data-aos].aos-animate{transition-delay:2.9s}[data-aos][data-aos][data-aos-duration="2950"],body[data-aos-duration="2950"] [data-aos]{transition-duration:2.95s}[data-aos][data-aos][data-aos-delay="2950"],body[data-aos-delay="2950"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2950"].aos-animate,body[data-aos-delay="2950"] [data-aos].aos-animate{transition-delay:2.95s}[data-aos][data-aos][data-aos-duration="3000"],body[data-aos-duration="3000"] [data-aos]{transition-duration:3s}[data-aos][data-aos][data-aos-delay="3000"],body[data-aos-delay="3000"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="3000"].aos-animate,body[data-aos-delay="3000"] [data-aos].aos-animate{transition-delay:3s}[data-aos][data-aos][data-aos-easing=linear],body[data-aos-easing=linear] [data-aos]{transition-timing-function:cubic-bezier(.25,.25,.75,.75)}[data-aos][data-aos][data-aos-easing=ease],body[data-aos-easing=ease] [data-aos]{transition-timing-function:ease}[data-aos][data-aos][data-aos-easing=ease-in],body[data-aos-easing=ease-in] [data-aos]{transition-timing-function:ease-in}[data-aos][data-aos][data-aos-easing=ease-out],body[data-aos-easing=ease-out] [data-aos]{transition-timing-function:ease-out}[data-aos][data-aos][data-aos-easing=ease-in-out],body[data-aos-easing=ease-in-out] [data-aos]{transition-timing-function:ease-in-out}[data-aos][data-aos][data-aos-easing=ease-in-back],body[data-aos-easing=ease-in-back] [data-aos]{transition-timing-function:cubic-bezier(.6,-.28,.735,.045)}[data-aos][data-aos][data-aos-easing=ease-out-back],body[data-aos-easing=ease-out-back] [data-aos]{transition-timing-function:cubic-bezier(.175,.885,.32,1.275)}[data-aos][data-aos][data-aos-easing=ease-in-out-back],body[data-aos-easing=ease-in-out-back] [data-aos]{transition-timing-function:cubic-bezier(.68,-.55,.265,1.55)}[data-aos][data-aos][data-aos-easing=ease-in-sine],body[data-aos-easing=ease-in-sine] [data-aos]{transition-timing-function:cubic-bezier(.47,0,.745,.715)}[data-aos][data-aos][data-aos-easing=ease-out-sine],body[data-aos-easing=ease-out-sine] [data-aos]{transition-timing-function:cubic-bezier(.39,.575,.565,1)}[data-aos][data-aos][data-aos-easing=ease-in-out-sine],body[data-aos-easing=ease-in-out-sine] [data-aos]{transition-timing-function:cubic-bezier(.445,.05,.55,.95)}[data-aos][data-aos][data-aos-easing=ease-in-quad],body[data-aos-easing=ease-in-quad] [data-aos]{transition-timing-function:cubic-bezier(.55,.085,.68,.53)}[data-aos][data-aos][data-aos-easing=ease-out-quad],body[data-aos-easing=ease-out-quad] [data-aos]{transition-timing-function:cubic-bezier(.25,.46,.45,.94)}[data-aos][data-aos][data-aos-easing=ease-in-out-quad],body[data-aos-easing=ease-in-out-quad] [data-aos]{transition-timing-function:cubic-bezier(.455,.03,.515,.955)}[data-aos][data-aos][data-aos-easing=ease-in-cubic],body[data-aos-easing=ease-in-cubic] [data-aos]{transition-timing-function:cubic-bezier(.55,.085,.68,.53)}[data-aos][data-aos][data-aos-easing=ease-out-cubic],body[data-aos-easing=ease-out-cubic] [data-aos]{transition-timing-function:cubic-bezier(.25,.46,.45,.94)}[data-aos][data-aos][data-aos-easing=ease-in-out-cubic],body[data-aos-easing=ease-in-out-cubic] [data-aos]{transition-timing-function:cubic-bezier(.455,.03,.515,.955)}[data-aos][data-aos][data-aos-easing=ease-in-quart],body[data-aos-easing=ease-in-quart] [data-aos]{transition-timing-function:cubic-bezier(.55,.085,.68,.53)}[data-aos][data-aos][data-aos-easing=ease-out-quart],body[data-aos-easing=ease-out-quart] [data-aos]{transition-timing-function:cubic-bezier(.25,.46,.45,.94)}[data-aos][data-aos][data-aos-easing=ease-in-out-quart],body[data-aos-easing=ease-in-out-quart] [data-aos]{transition-timing-function:cubic-bezier(.455,.03,.515,.955)}[data-aos^=fade][data-aos^=fade]{opacity:0;transition-property:opacity,transform}[data-aos^=fade][data-aos^=fade].aos-animate{opacity:1;transform:translateZ(0)}[data-aos=fade-up]{transform:translate3d(0,100px,0)}[data-aos=fade-down]{transform:translate3d(0,-100px,0)}[data-aos=fade-right]{transform:translate3d(-100px,0,0)}[data-aos=fade-left]{transform:translate3d(100px,0,0)}[data-aos=fade-up-right]{transform:translate3d(-100px,100px,0)}[data-aos=fade-up-left]{transform:translate3d(100px,100px,0)}[data-aos=fade-down-right]{transform:translate3d(-100px,-100px,0)}[data-aos=fade-down-left]{transform:translate3d(100px,-100px,0)}[data-aos^=zoom][data-aos^=zoom]{opacity:0;transition-property:opacity,transform}[data-aos^=zoom][data-aos^=zoom].aos-animate{opacity:1;transform:translateZ(0) scale(1)}[data-aos=zoom-in]{transform:scale(.6)}[data-aos=zoom-in-up]{transform:translate3d(0,100px,0) scale(.6)}[data-aos=zoom-in-down]{transform:translate3d(0,-100px,0) scale(.6)}[data-aos=zoom-in-right]{transform:translate3d(-100px,0,0) scale(.6)}[data-aos=zoom-in-left]{transform:translate3d(100px,0,0) scale(.6)}[data-aos=zoom-out]{transform:scale(1.2)}[data-aos=zoom-out-up]{transform:translate3d(0,100px,0) scale(1.2)}[data-aos=zoom-out-down]{transform:translate3d(0,-100px,0) scale(1.2)}[data-aos=zoom-out-right]{transform:translate3d(-100px,0,0) scale(1.2)}[data-aos=zoom-out-left]{transform:translate3d(100px,0,0) scale(1.2)}[data-aos^=slide][data-aos^=slide]{transition-property:transform}[data-aos^=slide][data-aos^=slide].aos-animate{transform:translateZ(0)}[data-aos=slide-up]{transform:translate3d(0,100%,0)}[data-aos=slide-down]{transform:translate3d(0,-100%,0)}[data-aos=slide-right]{transform:translate3d(-100%,0,0)}[data-aos=slide-left]{transform:translate3d(100%,0,0)}[data-aos^=flip][data-aos^=flip]{backface-visibility:hidden;transition-property:transform}[data-aos=flip-left]{transform:perspective(2500px) rotateY(-100deg)}[data-aos=flip-left].aos-animate{transform:perspective(2500px) rotateY(0)}[data-aos=flip-right]{transform:perspective(2500px) rotateY(100deg)}[data-aos=flip-right].aos-animate{transform:perspective(2500px) rotateY(0)}[data-aos=flip-up]{transform:perspective(2500px) rotateX(-100deg)}[data-aos=flip-up].aos-animate{transform:perspective(2500px) rotateX(0)}[data-aos=flip-down]{transform:perspective(2500px) rotateX(100deg)}[data-aos=flip-down].aos-animate{transform:perspective(2500px) rotateX(0)}