aprsd 1.0.0__py3-none-any.whl → 3.4.2__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 (140) hide show
  1. aprsd/__init__.py +6 -4
  2. aprsd/cli_helper.py +151 -0
  3. aprsd/client/__init__.py +13 -0
  4. aprsd/client/aprsis.py +132 -0
  5. aprsd/client/base.py +105 -0
  6. aprsd/client/drivers/__init__.py +0 -0
  7. aprsd/client/drivers/aprsis.py +228 -0
  8. aprsd/client/drivers/fake.py +73 -0
  9. aprsd/client/drivers/kiss.py +119 -0
  10. aprsd/client/factory.py +88 -0
  11. aprsd/client/fake.py +48 -0
  12. aprsd/client/kiss.py +103 -0
  13. aprsd/client/stats.py +38 -0
  14. aprsd/cmds/__init__.py +0 -0
  15. aprsd/cmds/completion.py +22 -0
  16. aprsd/cmds/dev.py +162 -0
  17. aprsd/cmds/fetch_stats.py +156 -0
  18. aprsd/cmds/healthcheck.py +86 -0
  19. aprsd/cmds/list_plugins.py +319 -0
  20. aprsd/cmds/listen.py +231 -0
  21. aprsd/cmds/send_message.py +171 -0
  22. aprsd/cmds/server.py +137 -0
  23. aprsd/cmds/webchat.py +674 -0
  24. aprsd/conf/__init__.py +56 -0
  25. aprsd/conf/client.py +131 -0
  26. aprsd/conf/common.py +301 -0
  27. aprsd/conf/log.py +65 -0
  28. aprsd/conf/opts.py +80 -0
  29. aprsd/conf/plugin_common.py +182 -0
  30. aprsd/conf/plugin_email.py +105 -0
  31. aprsd/exception.py +13 -0
  32. aprsd/log/__init__.py +0 -0
  33. aprsd/log/log.py +138 -0
  34. aprsd/main.py +104 -867
  35. aprsd/packets/__init__.py +20 -0
  36. aprsd/packets/collector.py +79 -0
  37. aprsd/packets/core.py +823 -0
  38. aprsd/packets/log.py +161 -0
  39. aprsd/packets/packet_list.py +110 -0
  40. aprsd/packets/seen_list.py +49 -0
  41. aprsd/packets/tracker.py +103 -0
  42. aprsd/packets/watch_list.py +119 -0
  43. aprsd/plugin.py +474 -284
  44. aprsd/plugin_utils.py +86 -0
  45. aprsd/plugins/__init__.py +0 -0
  46. aprsd/plugins/email.py +709 -0
  47. aprsd/plugins/fortune.py +61 -0
  48. aprsd/plugins/location.py +179 -0
  49. aprsd/plugins/notify.py +61 -0
  50. aprsd/plugins/ping.py +31 -0
  51. aprsd/plugins/time.py +115 -0
  52. aprsd/plugins/version.py +31 -0
  53. aprsd/plugins/weather.py +405 -0
  54. aprsd/stats/__init__.py +20 -0
  55. aprsd/stats/app.py +49 -0
  56. aprsd/stats/collector.py +37 -0
  57. aprsd/threads/__init__.py +11 -0
  58. aprsd/threads/aprsd.py +119 -0
  59. aprsd/threads/keep_alive.py +131 -0
  60. aprsd/threads/log_monitor.py +121 -0
  61. aprsd/threads/registry.py +56 -0
  62. aprsd/threads/rx.py +354 -0
  63. aprsd/threads/stats.py +44 -0
  64. aprsd/threads/tx.py +255 -0
  65. aprsd/utils/__init__.py +218 -0
  66. aprsd/utils/counter.py +51 -0
  67. aprsd/utils/json.py +80 -0
  68. aprsd/utils/objectstore.py +123 -0
  69. aprsd/utils/ring_buffer.py +40 -0
  70. aprsd/utils/trace.py +180 -0
  71. aprsd/web/__init__.py +0 -0
  72. aprsd/web/admin/__init__.py +0 -0
  73. aprsd/web/admin/static/css/index.css +84 -0
  74. aprsd/web/admin/static/css/prism.css +4 -0
  75. aprsd/web/admin/static/css/tabs.css +35 -0
  76. aprsd/web/admin/static/images/Untitled.png +0 -0
  77. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  78. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  79. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  80. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  81. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  82. aprsd/web/admin/static/js/charts.js +235 -0
  83. aprsd/web/admin/static/js/echarts.js +465 -0
  84. aprsd/web/admin/static/js/logs.js +26 -0
  85. aprsd/web/admin/static/js/main.js +231 -0
  86. aprsd/web/admin/static/js/prism.js +12 -0
  87. aprsd/web/admin/static/js/send-message.js +114 -0
  88. aprsd/web/admin/static/js/tabs.js +28 -0
  89. aprsd/web/admin/templates/index.html +196 -0
  90. aprsd/web/chat/static/css/chat.css +115 -0
  91. aprsd/web/chat/static/css/index.css +66 -0
  92. aprsd/web/chat/static/css/style.css.map +1 -0
  93. aprsd/web/chat/static/css/tabs.css +41 -0
  94. aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
  95. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  96. aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
  97. aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
  98. aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
  99. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  100. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  101. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  102. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  103. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  104. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  105. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  106. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  107. aprsd/web/chat/static/images/Untitled.png +0 -0
  108. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  109. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  110. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  111. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  112. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  113. aprsd/web/chat/static/images/globe.svg +3 -0
  114. aprsd/web/chat/static/js/gps.js +84 -0
  115. aprsd/web/chat/static/js/main.js +45 -0
  116. aprsd/web/chat/static/js/send-message.js +585 -0
  117. aprsd/web/chat/static/js/tabs.js +28 -0
  118. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
  119. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
  120. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
  121. aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
  122. aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
  123. aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
  124. aprsd/web/chat/templates/index.html +139 -0
  125. aprsd/wsgi.py +315 -0
  126. aprsd-3.4.2.dist-info/AUTHORS +13 -0
  127. aprsd-3.4.2.dist-info/LICENSE +175 -0
  128. aprsd-3.4.2.dist-info/METADATA +793 -0
  129. aprsd-3.4.2.dist-info/RECORD +133 -0
  130. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/WHEEL +1 -1
  131. aprsd-3.4.2.dist-info/entry_points.txt +8 -0
  132. aprsd/fake_aprs.py +0 -83
  133. aprsd/utils.py +0 -166
  134. aprsd-1.0.0.dist-info/AUTHORS +0 -6
  135. aprsd-1.0.0.dist-info/METADATA +0 -181
  136. aprsd-1.0.0.dist-info/RECORD +0 -13
  137. aprsd-1.0.0.dist-info/entry_points.txt +0 -4
  138. aprsd-1.0.0.dist-info/pbr.json +0 -1
  139. /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
  140. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,123 @@
1
+ import logging
2
+ import os
3
+ import pathlib
4
+ import pickle
5
+ import threading
6
+
7
+ from oslo_config import cfg
8
+
9
+
10
+ CONF = cfg.CONF
11
+ LOG = logging.getLogger("APRSD")
12
+
13
+
14
+ class ObjectStoreMixin:
15
+ """Class 'MIXIN' intended to save/load object data.
16
+
17
+ The asumption of how this mixin is used:
18
+ The using class has to have a:
19
+ * data in self.data as a dictionary
20
+ * a self.lock thread lock
21
+ * Class must specify self.save_file as the location.
22
+
23
+
24
+ When APRSD quits, it calls save()
25
+ When APRSD Starts, it calls load()
26
+ aprsd server -f (flush) will wipe all saved objects.
27
+ """
28
+
29
+ def __init__(self):
30
+ self.lock = threading.RLock()
31
+
32
+ def __len__(self):
33
+ with self.lock:
34
+ return len(self.data)
35
+
36
+ def __iter__(self):
37
+ with self.lock:
38
+ return iter(self.data)
39
+
40
+ def get_all(self):
41
+ with self.lock:
42
+ return self.data
43
+
44
+ def get(self, key):
45
+ with self.lock:
46
+ return self.data.get(key)
47
+
48
+ def copy(self):
49
+ with self.lock:
50
+ return self.data.copy()
51
+
52
+ def _init_store(self):
53
+ if not CONF.enable_save:
54
+ return
55
+ sl = CONF.save_location
56
+ if not os.path.exists(sl):
57
+ LOG.warning(f"Save location {sl} doesn't exist")
58
+ try:
59
+ os.makedirs(sl)
60
+ except Exception as ex:
61
+ LOG.exception(ex)
62
+
63
+ def _save_filename(self):
64
+ save_location = CONF.save_location
65
+
66
+ return "{}/{}.p".format(
67
+ save_location,
68
+ self.__class__.__name__.lower(),
69
+ )
70
+
71
+ def save(self):
72
+ """Save any queued to disk?"""
73
+ if not CONF.enable_save:
74
+ return
75
+ self._init_store()
76
+ save_filename = self._save_filename()
77
+ if len(self) > 0:
78
+ LOG.info(
79
+ f"{self.__class__.__name__}::Saving"
80
+ f" {len(self)} entries to disk at "
81
+ f"{save_filename}",
82
+ )
83
+ with self.lock:
84
+ with open(save_filename, "wb+") as fp:
85
+ pickle.dump(self.data, fp)
86
+ else:
87
+ LOG.debug(
88
+ "{} Nothing to save, flushing old save file '{}'".format(
89
+ self.__class__.__name__,
90
+ save_filename,
91
+ ),
92
+ )
93
+ self.flush()
94
+
95
+ def load(self):
96
+ if not CONF.enable_save:
97
+ return
98
+ if os.path.exists(self._save_filename()):
99
+ try:
100
+ with open(self._save_filename(), "rb") as fp:
101
+ raw = pickle.load(fp)
102
+ if raw:
103
+ self.data = raw
104
+ LOG.debug(
105
+ f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
106
+ )
107
+ else:
108
+ LOG.debug(f"{self.__class__.__name__}::No data to load.")
109
+ except (pickle.UnpicklingError, Exception) as ex:
110
+ LOG.error(f"Failed to UnPickle {self._save_filename()}")
111
+ LOG.error(ex)
112
+ self.data = {}
113
+ else:
114
+ LOG.debug(f"{self.__class__.__name__}::No save file found.")
115
+
116
+ def flush(self):
117
+ """Nuke the old pickle file that stored the old results from last aprsd run."""
118
+ if not CONF.enable_save:
119
+ return
120
+ if os.path.exists(self._save_filename()):
121
+ pathlib.Path(self._save_filename()).unlink()
122
+ with self.lock:
123
+ self.data = {}
@@ -0,0 +1,40 @@
1
+ class RingBuffer:
2
+ """class that implements a not-yet-full buffer"""
3
+
4
+ max: int = 100
5
+ data: list = []
6
+
7
+ def __init__(self, size_max):
8
+ self.max = size_max
9
+ self.data = []
10
+
11
+ class __Full:
12
+ """class that implements a full buffer"""
13
+
14
+ def append(self, x):
15
+ """Append an element overwriting the oldest one."""
16
+ self.data[self.cur] = x
17
+ self.cur = (self.cur + 1) % self.max
18
+
19
+ def get(self):
20
+ """return list of elements in correct order"""
21
+ return self.data[self.cur :] + self.data[: self.cur]
22
+
23
+ def __len__(self):
24
+ return len(self.data)
25
+
26
+ def append(self, x):
27
+ """append an element at the end of the buffer"""
28
+
29
+ self.data.append(x)
30
+ if len(self.data) == self.max:
31
+ self.cur = 0
32
+ # Permanently change self's class from non-full to full
33
+ self.__class__ = self.__Full
34
+
35
+ def get(self):
36
+ """Return a list of elements from the oldest to the newest."""
37
+ return self.data
38
+
39
+ def __len__(self):
40
+ return len(self.data)
aprsd/utils/trace.py ADDED
@@ -0,0 +1,180 @@
1
+ import abc
2
+ import functools
3
+ import inspect
4
+ import logging
5
+ import time
6
+ import types
7
+
8
+
9
+ VALID_TRACE_FLAGS = {"method", "api"}
10
+ TRACE_API = False
11
+ TRACE_METHOD = False
12
+ TRACE_ENABLED = False
13
+ LOG = logging.getLogger("APRSD")
14
+
15
+
16
+ def trace(*dec_args, **dec_kwargs):
17
+ """Trace calls to the decorated function.
18
+
19
+ This decorator should always be defined as the outermost decorator so it
20
+ is defined last. This is important so it does not interfere
21
+ with other decorators.
22
+
23
+ Using this decorator on a function will cause its execution to be logged at
24
+ `DEBUG` level with arguments, return values, and exceptions.
25
+
26
+ :returns: a function decorator
27
+ """
28
+
29
+ def _decorator(f):
30
+
31
+ func_name = f.__name__
32
+
33
+ @functools.wraps(f)
34
+ def trace_logging_wrapper(*args, **kwargs):
35
+ filter_function = dec_kwargs.get("filter_function")
36
+ logger = LOG
37
+
38
+ # NOTE(ameade): Don't bother going any further if DEBUG log level
39
+ # is not enabled for the logger.
40
+ if not logger.isEnabledFor(logging.DEBUG) or not TRACE_ENABLED:
41
+ return f(*args, **kwargs)
42
+
43
+ all_args = inspect.getcallargs(f, *args, **kwargs)
44
+
45
+ pass_filter = filter_function is None or filter_function(all_args)
46
+
47
+ if pass_filter:
48
+ logger.debug(
49
+ "==> %(func)s: call %(all_args)r",
50
+ {
51
+ "func": func_name,
52
+ "all_args": str(all_args),
53
+ },
54
+ )
55
+
56
+ start_time = time.time() * 1000
57
+ try:
58
+ result = f(*args, **kwargs)
59
+ except Exception as exc:
60
+ total_time = int(round(time.time() * 1000)) - start_time
61
+ logger.debug(
62
+ "<== %(func)s: exception (%(time)dms) %(exc)r",
63
+ {
64
+ "func": func_name,
65
+ "time": total_time,
66
+ "exc": exc,
67
+ },
68
+ )
69
+ raise
70
+ total_time = int(round(time.time() * 1000)) - start_time
71
+
72
+ if isinstance(result, dict):
73
+ mask_result = result
74
+ elif isinstance(result, str):
75
+ mask_result = result
76
+ else:
77
+ mask_result = result
78
+
79
+ if pass_filter:
80
+ logger.debug(
81
+ "<== %(func)s: return (%(time)dms) %(result)r",
82
+ {
83
+ "func": func_name,
84
+ "time": total_time,
85
+ "result": mask_result,
86
+ },
87
+ )
88
+ return result
89
+
90
+ return trace_logging_wrapper
91
+
92
+ if len(dec_args) == 0:
93
+ # filter_function is passed and args does not contain f
94
+ return _decorator
95
+ else:
96
+ # filter_function is not passed
97
+ return _decorator(dec_args[0])
98
+
99
+
100
+ def trace_api(*dec_args, **dec_kwargs):
101
+ """Decorates a function if TRACE_API is true."""
102
+
103
+ def _decorator(f):
104
+ @functools.wraps(f)
105
+ def trace_api_logging_wrapper(*args, **kwargs):
106
+ if TRACE_API:
107
+ return trace(f, *dec_args, **dec_kwargs)(*args, **kwargs)
108
+ return f(*args, **kwargs)
109
+
110
+ return trace_api_logging_wrapper
111
+
112
+ if len(dec_args) == 0:
113
+ # filter_function is passed and args does not contain f
114
+ return _decorator
115
+ else:
116
+ # filter_function is not passed
117
+ return _decorator(dec_args[0])
118
+
119
+
120
+ def trace_method(f):
121
+ """Decorates a function if TRACE_METHOD is true."""
122
+
123
+ @functools.wraps(f)
124
+ def trace_method_logging_wrapper(*args, **kwargs):
125
+ if TRACE_METHOD:
126
+ return trace(f)(*args, **kwargs)
127
+ return f(*args, **kwargs)
128
+
129
+ return trace_method_logging_wrapper
130
+
131
+
132
+ class TraceWrapperMetaclass(type):
133
+ """Metaclass that wraps all methods of a class with trace_method.
134
+
135
+ This metaclass will cause every function inside of the class to be
136
+ decorated with the trace_method decorator.
137
+
138
+ To use the metaclass you define a class like so:
139
+ class MyClass(object, metaclass=utils.TraceWrapperMetaclass):
140
+ """
141
+
142
+ def __new__(cls, classname, bases, class_dict):
143
+ new_class_dict = {}
144
+ for attribute_name, attribute in class_dict.items():
145
+ if isinstance(attribute, types.FunctionType):
146
+ # replace it with a wrapped version
147
+ attribute = functools.update_wrapper(
148
+ trace_method(attribute),
149
+ attribute,
150
+ )
151
+ new_class_dict[attribute_name] = attribute
152
+
153
+ return type.__new__(cls, classname, bases, new_class_dict)
154
+
155
+
156
+ class TraceWrapperWithABCMetaclass(abc.ABCMeta, TraceWrapperMetaclass):
157
+ """Metaclass that wraps all methods of a class with trace."""
158
+
159
+
160
+ def setup_tracing(trace_flags):
161
+ """Set global variables for each trace flag.
162
+
163
+ Sets variables TRACE_METHOD and TRACE_API, which represent
164
+ whether to log methods or api traces.
165
+
166
+ :param trace_flags: a list of strings
167
+ """
168
+ global TRACE_METHOD
169
+ global TRACE_API
170
+ global TRACE_ENABLED
171
+
172
+ try:
173
+ trace_flags = [flag.strip() for flag in trace_flags]
174
+ except TypeError: # Handle when trace_flags is None or a test mock
175
+ trace_flags = []
176
+ for invalid_flag in set(trace_flags) - VALID_TRACE_FLAGS:
177
+ LOG.warning("Invalid trace flag: %s", invalid_flag)
178
+ TRACE_METHOD = "method" in trace_flags
179
+ TRACE_API = "api" in trace_flags
180
+ TRACE_ENABLED = True
aprsd/web/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,84 @@
1
+ body {
2
+ background: #eeeeee;
3
+ margin: 2em;
4
+ text-align: center;
5
+ font-family: system-ui, sans-serif;
6
+ }
7
+
8
+ footer {
9
+ padding: 2em;
10
+ text-align: center;
11
+ height: 10vh;
12
+ }
13
+
14
+ .ui.segment {
15
+ background: #eeeeee;
16
+ }
17
+
18
+ #graphs {
19
+ display: grid;
20
+ width: 100%;
21
+ height: 300px;
22
+ grid-template-columns: 1fr 1fr;
23
+ }
24
+ #graphs_center {
25
+ display: block;
26
+ margin-top: 10px;
27
+ margin-bottom: 10px;
28
+ width: 100%;
29
+ height: 300px;
30
+ }
31
+ #left {
32
+ margin-right: 2px;
33
+ height: 300px;
34
+ }
35
+ #right {
36
+ height: 300px;
37
+ }
38
+ #center {
39
+ height: 300px;
40
+ }
41
+ #packetsChart, #messageChart, #emailChart, #memChart {
42
+ border: 1px solid #ccc;
43
+ background: #ddd;
44
+ }
45
+ #stats {
46
+ margin: auto;
47
+ width: 80%;
48
+ }
49
+ #jsonstats {
50
+ display: none;
51
+ }
52
+ #title {
53
+ font-size: 4em;
54
+ }
55
+ #version{
56
+ font-size: .5em;
57
+ }
58
+ #uptime, #aprsis {
59
+ font-size: 1em;
60
+ }
61
+ #callsign {
62
+ font-size: 1.4em;
63
+ color: #00F;
64
+ padding-top: 8px;
65
+ margin:10px;
66
+ }
67
+
68
+ #title_rx {
69
+ background-color: darkseagreen;
70
+ text-align: left;
71
+ }
72
+
73
+ #title_tx {
74
+ background-color: lightcoral;
75
+ text-align: left;
76
+ }
77
+
78
+ .aprsd_1 {
79
+ background-image: url(/static/images/aprs-symbols-16-0.png);
80
+ background-repeat: no-repeat;
81
+ background-position: -160px -48px;
82
+ width: 16px;
83
+ height: 16px;
84
+ }
@@ -0,0 +1,4 @@
1
+ /* PrismJS 1.29.0
2
+ https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */
3
+ code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
4
+ div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
@@ -0,0 +1,35 @@
1
+ /* Style the tab */
2
+ .tab {
3
+ overflow: hidden;
4
+ border: 1px solid #ccc;
5
+ background-color: #f1f1f1;
6
+ }
7
+
8
+ /* Style the buttons that are used to open the tab content */
9
+ .tab button {
10
+ background-color: inherit;
11
+ float: left;
12
+ border: none;
13
+ outline: none;
14
+ cursor: pointer;
15
+ padding: 14px 16px;
16
+ transition: 0.3s;
17
+ }
18
+
19
+ /* Change background color of buttons on hover */
20
+ .tab button:hover {
21
+ background-color: #ddd;
22
+ }
23
+
24
+ /* Create an active/current tablink class */
25
+ .tab button.active {
26
+ background-color: #ccc;
27
+ }
28
+
29
+ /* Style the tab content */
30
+ .tabcontent {
31
+ display: none;
32
+ padding: 6px 12px;
33
+ border: 1px solid #ccc;
34
+ border-top: none;
35
+ }