disermo 0.0.1__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.
disermo/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from . import checks # noqa
2
+ from . import notifiers # noqa
3
+ from . import storage # noqa
4
+ from .checks import Check # noqa
5
+ from .node import Node # noqa
6
+
7
+ __version__ = "0.0.1"
@@ -0,0 +1,5 @@
1
+ from .base import Check # noqa
2
+ from . import remote # noqa
3
+ from . import sensors # noqa
4
+ from . import storage # noqa
5
+ from . import system # noqa
disermo/checks/base.py ADDED
@@ -0,0 +1,83 @@
1
+ """
2
+ Checks
3
+ """
4
+ from __future__ import annotations
5
+ from typing import List, Dict, Any, TypeVar
6
+
7
+ from ..constants import Status
8
+ from ..notifiers import Notifier
9
+ from ..utils import camel_to_sentence
10
+
11
+
12
+ T = TypeVar('T', bound='Check')
13
+
14
+
15
+ class Check:
16
+ uid: str
17
+ label: str
18
+ subchecks: List[Check]
19
+ notifiers: List[Notifier]
20
+ status: Status
21
+ data: Dict[str, Any]
22
+
23
+ def __init__(self, label: str = None) -> None:
24
+ if label is None:
25
+ label = self.default_label
26
+ self.label = label
27
+ self.uid = self.get_uid()
28
+ self.subchecks = []
29
+ self.notifiers = []
30
+
31
+ self.status = Status.DISABLED
32
+ self.data = {}
33
+
34
+ def __repr__(self):
35
+ return f'<{type(self).__name__}: {self.uid}>'
36
+
37
+ @property
38
+ def class_id(self) -> str:
39
+ """
40
+ Default the class ID to the class name, but expect subclasses to
41
+ override with a string
42
+ """
43
+ return type(self).__name__.lower()
44
+
45
+ @property
46
+ def default_label(self) -> str:
47
+ """
48
+ Default the label to the class name, but expect subclasses to override
49
+ with a string
50
+ """
51
+ return camel_to_sentence(type(self).__name__)
52
+
53
+ def get_uid(self) -> str:
54
+ return f'{self.class_id}'
55
+
56
+ def add(self: T, *subchecks) -> T:
57
+ self.subchecks.extend(subchecks)
58
+ return self
59
+
60
+ def run(self) -> Status:
61
+ """
62
+ Run the check of this node, and of all its subchecks
63
+
64
+ The resulting status is the worst status for this node and all subnodes
65
+ """
66
+ self.update()
67
+ worst: Status = self.status
68
+ for check in self.subchecks:
69
+ result: Status = check.run()
70
+ if result > worst:
71
+ worst = result
72
+
73
+ # The status of this node is the worst found
74
+ self.status = worst
75
+ return worst
76
+
77
+ def update(self) -> None:
78
+ self.status = Status.DISABLED
79
+ self.data = {}
80
+
81
+ def notify(self: T, *notifiers: Notifier) -> T:
82
+ self.notifiers.extend(notifiers)
83
+ return self
@@ -0,0 +1,123 @@
1
+ """
2
+ Checks which perform remote requests
3
+ """
4
+ from dataclasses import dataclass, field
5
+ from http.client import HTTPResponse
6
+ import socket
7
+ from typing import cast, Union
8
+ from urllib.request import urlopen
9
+ from urllib.error import HTTPError, URLError
10
+
11
+ from ..constants import Status
12
+ from ..utils import Timer
13
+ from .base import Check
14
+
15
+
16
+ # Default timeout, in seconds
17
+ DEFAULT_TIMEOUT = 10
18
+
19
+ # Error code for failed connection
20
+ # This should be outside the standard HTTP error codes, so it is differentiated
21
+ STATUS_FAILED = 0
22
+
23
+
24
+ @dataclass
25
+ class Response:
26
+ """
27
+ Wrapper for an urllib Response
28
+ """
29
+ raw: Union[HTTPResponse, HTTPError]
30
+ _content: str = field(init=False, repr=False)
31
+
32
+ @property
33
+ def code(self):
34
+ return self.raw.code
35
+
36
+ @property
37
+ def content(self):
38
+ if self._content is None:
39
+ self._content = self.raw.read().decode('utf-8')
40
+ return self._content
41
+
42
+
43
+ class Web(Check):
44
+ url: str
45
+ timeout: float
46
+ status_code: int = 200
47
+ content_contains: Union[str, None]
48
+
49
+ def __init__(
50
+ self,
51
+ url: str,
52
+ label: str = None,
53
+ timeout: float = DEFAULT_TIMEOUT,
54
+ status_code: int = 200,
55
+ content_contains: str = None,
56
+ ):
57
+ super().__init__(label)
58
+ self.url = url
59
+ self.timeout = timeout
60
+ self.status_code = status_code
61
+ self.content_contains = content_contains
62
+
63
+ def update(self) -> None:
64
+ # Clean data, assuming we're going to fail
65
+ self.data = {
66
+ 'status': STATUS_FAILED,
67
+ }
68
+
69
+ # Make the request
70
+ timer = Timer()
71
+ try:
72
+ raw_response: HTTPResponse = cast(
73
+ HTTPResponse,
74
+ urlopen(
75
+ self.url,
76
+ timeout=self.timeout,
77
+ ),
78
+ )
79
+
80
+ except HTTPError as e_response:
81
+ # HTTP error - should be expected, analyse below
82
+ response = Response(raw=e_response)
83
+
84
+ except URLError as e:
85
+ # Unable to connect - not expected, raise immediately
86
+ self.status = Status.ERROR
87
+ self.data['error'] = f'Could not reach server: {e.reason}'
88
+ return
89
+
90
+ except Exception as e:
91
+ # Most likely here is socket.timeout for HTTPS
92
+ self.status = Status.ERROR
93
+ self.data['error'] = f'Could not reach server: {e}'
94
+ return
95
+
96
+ else:
97
+ # Successful request
98
+ response = Response(raw=raw_response)
99
+
100
+ finally:
101
+ # All checks log their elapsed time
102
+ self.data['elapsed'] = timer.elapsed()
103
+
104
+ # Update status code to that returned by server
105
+ self.data['status'] = response.code
106
+
107
+ if response.code != self.status_code:
108
+ self.status = Status.ERROR
109
+ self.data['error'] = (
110
+ f'Expected status {self.status_code}, '
111
+ f'instead found {response.code}.'
112
+ )
113
+ return
114
+
115
+ if (
116
+ self.content_contains and
117
+ self.content_contains not in response.content
118
+ ):
119
+ self.status = Status.ERROR
120
+ self.data['error'] = 'Expected content not found'
121
+ return
122
+
123
+ self.status = Status.OK
@@ -0,0 +1,5 @@
1
+ from .base import Check
2
+
3
+
4
+ class CPUTemperature(Check):
5
+ pass
@@ -0,0 +1,49 @@
1
+ """
2
+ Storage checks
3
+ """
4
+ from __future__ import annotations
5
+ import os
6
+
7
+ from ..constants import Status
8
+ from .base import Check
9
+
10
+
11
+ DEFAULT_WARNING = 0.1
12
+ DEFAULT_ERROR = 0.05
13
+
14
+
15
+ class FreeSpace(Check):
16
+ drive: str
17
+ warning: float
18
+ error: float
19
+
20
+ def __init__(
21
+ self,
22
+ drive: str,
23
+ label: str = None,
24
+ warning: float = DEFAULT_WARNING,
25
+ error: float = DEFAULT_ERROR,
26
+ ) -> None:
27
+ super().__init__(label)
28
+ self.drive = drive
29
+ self.warning = warning
30
+ self.error = error
31
+
32
+ def update(self) -> None:
33
+ data = os.statvfs(self.drive)
34
+ total_bytes = data.f_frsize * data.f_blocks
35
+ # free_bytes = data.f_frsize * data.f_bfree
36
+ allowed_bytes = data.f_frsize * data.f_bavail
37
+ percent_free = allowed_bytes / total_bytes
38
+
39
+ self.data = {
40
+ 'total': total_bytes,
41
+ 'free': allowed_bytes,
42
+ }
43
+
44
+ if percent_free < self.error:
45
+ self.status = Status.ERROR
46
+ elif percent_free < self.warning:
47
+ self.status = Status.WARN
48
+ else:
49
+ self.status = Status.OK
@@ -0,0 +1,5 @@
1
+ from .base import Check
2
+
3
+
4
+ class Load(Check):
5
+ pass
disermo/constants.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ Disermo constants
3
+ """
4
+ from .utils import OrderedEnum
5
+
6
+
7
+ class Status(OrderedEnum):
8
+ DISABLED = 0
9
+ OK = 1
10
+ WARN = 2
11
+ ERROR = 3
disermo/node.py ADDED
@@ -0,0 +1,83 @@
1
+ """
2
+ Node object
3
+ """
4
+ from collections import defaultdict
5
+ from typing import List, DefaultDict, Iterator, Tuple
6
+
7
+ from .checks.base import Check
8
+ from .constants import Status
9
+ from .notifiers import Notifier
10
+ from .storage import Storage
11
+
12
+
13
+ class Node(Check):
14
+ def check(self, storage: Storage) -> Status:
15
+ """
16
+ Run the checks and notify
17
+ """
18
+ status: Status = super().run()
19
+
20
+ # Collect flat list of all checks
21
+ checks: List[Check] = list(self.iter_flat_checks())
22
+
23
+ # Collect notifiers
24
+ notifiers: DefaultDict[Notifier, List[Check]] = defaultdict(list)
25
+ for check in checks:
26
+ for notifier in check.notifiers:
27
+ notifiers[notifier].append(check)
28
+
29
+ # Load storage so notifiers understand status context
30
+ storage.load()
31
+
32
+ # Call notifiers
33
+ for notifier, notifier_checks in notifiers.items():
34
+ notifier.process(self, storage, notifier_checks)
35
+
36
+ # Store statuses for trend spotting
37
+ for check in checks:
38
+ storage.set(check.uid, check.status)
39
+ storage.save()
40
+
41
+ return status
42
+
43
+ def iter_flat_checks(self) -> Iterator[Check]:
44
+ """
45
+ Generator to return a flat list of the node tree, starting with self
46
+ """
47
+ checks: List[Check] = [self]
48
+ while checks:
49
+ check = checks.pop(0)
50
+ checks = check.subchecks + checks
51
+ yield check
52
+
53
+ def iter_flat_depth_checks(self) -> Iterator[Tuple[Check, int]]:
54
+ """
55
+ Generator to return a flat list of (check, depth) pairs of the node
56
+ tree, starting with self, where depth is a 0-indexed depth (0 is self)
57
+ """
58
+ checks: List[Tuple[Check, int]] = [(self, 0)]
59
+ while checks:
60
+ check, depth = checks.pop(0)
61
+ checks = [
62
+ (subcheck, depth + 1)
63
+ for subcheck in check.subchecks
64
+ ] + checks
65
+ yield (check, depth)
66
+
67
+ def iter_flat_labelled_checks(self) -> Iterator[Tuple[Check, List[str]]]:
68
+ """
69
+ Generator to return a flat list of (check, labels) pairs of the node
70
+ tree, starting with self, where ``labels`` is a list of all parent
71
+ labels, in order oldest first
72
+ """
73
+ checks: List[Tuple[Check, List[str]]] = [(self, [self.label])]
74
+ while checks:
75
+ # Look at next and search its children
76
+ check, label = checks.pop(0)
77
+ checks = [
78
+ (subcheck, label + [subcheck.label])
79
+ for subcheck in check.subchecks
80
+ ] + checks
81
+
82
+ # See if we've found one
83
+ yield (check, label)
@@ -0,0 +1,3 @@
1
+ from .base import Notifier # noqa
2
+ from .email import Email # noqa
3
+ from .stream import Stream # noqa
@@ -0,0 +1,107 @@
1
+ """
2
+ Base class for all notifiers
3
+ """
4
+ from typing import TYPE_CHECKING, List
5
+
6
+ if TYPE_CHECKING: # pragma: no cover
7
+ from ..checks import Check
8
+ from ..constants import Status
9
+ from ..node import Node
10
+ from ..storage import Storage, GroupedStatus
11
+
12
+
13
+ DEFAULT_TREND_LIMIT = 5
14
+
15
+
16
+ class Notifier:
17
+ def process(
18
+ self,
19
+ node: 'Node',
20
+ storage: 'Storage',
21
+ checks: 'List[Check]',
22
+ ) -> None:
23
+ """
24
+ Process a set of checks
25
+
26
+ Filters the checks and passes notifiable ones on to send()
27
+ """
28
+ # Get list of checks with something worth notifying
29
+ notifiable = [
30
+ check for check in checks
31
+ if self.test(storage, check)
32
+ ]
33
+
34
+ # Send the notification(s) out
35
+ self.send(node, notifiable)
36
+
37
+ def test(self, storage: 'Storage', check: 'Check') -> bool:
38
+ """
39
+ Subclasses will perform tests to see if notification is required.
40
+
41
+ This base class will just pass all notifications through
42
+ """
43
+ return True
44
+
45
+ def send(self, node: 'Node', checks: 'List[Check]') -> None:
46
+ raise NotImplementedError() # pragma: no cover
47
+
48
+
49
+ class TrendNotifier(Notifier):
50
+ after: int
51
+
52
+ def __init__(self, after: int = DEFAULT_TREND_LIMIT):
53
+ self.after = after
54
+
55
+ def test(self, storage: 'Storage', check: 'Check') -> bool:
56
+ """
57
+ Performs any tests to see if the notification is required.
58
+
59
+ Looking for a change which occurred {self.after} ago
60
+ """
61
+ uid: str = check.uid
62
+ status: Status = check.status
63
+ generator: GroupedStatus = storage.get(key=uid, grouped=True)
64
+
65
+ # Get first (newest) status
66
+ first_status: Status
67
+ first_count: int
68
+ try:
69
+ first_status, first_count = next(generator)
70
+ except StopIteration:
71
+ # Nothing on the stack
72
+ if self.after <= 1:
73
+ return True
74
+ return False
75
+
76
+ # See if this is starting or continuing a trend
77
+ if first_status == status:
78
+ if first_count >= self.after:
79
+ # Continuing trend - no change
80
+ return False
81
+
82
+ elif first_count + 1 == self.after:
83
+ # This next one will start a new trend
84
+ # But what about the previous trend; is this actually a change?
85
+ past_status: Status
86
+ past_count: int
87
+ for past_status, past_count in generator:
88
+ if past_count >= self.after:
89
+ # Found the previous trend
90
+ if past_status == status:
91
+ # This is returning to an old trend - no change
92
+ return False
93
+ else:
94
+ # This is starting a new trend
95
+ return True
96
+
97
+ # Haven't found a previous trend - this is starting a new one
98
+ return True
99
+
100
+ else:
101
+ # Status doesn't match, it can't be a trend...
102
+ if self.after <= 1:
103
+ # ... unless we start a new trend immediately
104
+ return True
105
+
106
+ # Have not started a new trend yet - either a new status, or not enough
107
+ return False
@@ -0,0 +1,64 @@
1
+ """
2
+ Email notifier
3
+ """
4
+ from email.mime.text import MIMEText
5
+ import smtplib
6
+ from typing import TYPE_CHECKING, List, Tuple
7
+
8
+ from .base import TrendNotifier
9
+
10
+ if TYPE_CHECKING:
11
+ from ..checks import Check
12
+ from ..node import Node
13
+
14
+
15
+ class Email(TrendNotifier):
16
+ to_addr: str
17
+ from_addr: str
18
+
19
+ template_subject = '''[Disermo] {name} => {status}'''
20
+ template_body = '''{summary}'''
21
+ template_body_summary = '''{check}: {status}'''
22
+
23
+ def __init__(self, to_addr: str, from_addr: str, **kwargs) -> None:
24
+ super().__init__(**kwargs)
25
+ self.to_addr = to_addr
26
+ self.from_addr = from_addr
27
+
28
+ def send(self, node: 'Node', checks: 'List[Check]') -> None:
29
+ if not checks:
30
+ return
31
+
32
+ subject = self.template_subject.format(
33
+ name=node.label,
34
+ status=node.status,
35
+ )
36
+
37
+ # Generate full labels
38
+ found: List[Tuple[Check, List[str]]] = [
39
+ (check, label)
40
+ for check, label in node.iter_flat_labelled_checks()
41
+ if check in checks
42
+ ]
43
+
44
+ # Render summary
45
+ summary = '\n'.join([
46
+ self.template_body_summary.format(
47
+ check=' > '.join(label),
48
+ status=check.status.name.title(),
49
+ )
50
+ for check, label in found
51
+ ])
52
+
53
+ body = self.template_body.format(
54
+ summary=summary,
55
+ )
56
+
57
+ msg = MIMEText(body)
58
+ msg['Subject'] = subject
59
+ msg['To'] = self.to_addr
60
+ msg['From'] = self.from_addr
61
+
62
+ s = smtplib.SMTP('localhost')
63
+ s.send_message(msg)
64
+ s.quit()
@@ -0,0 +1,25 @@
1
+ """
2
+ Stream notifier
3
+ """
4
+ from typing import TYPE_CHECKING, List, TextIO
5
+
6
+ from .base import Notifier
7
+
8
+ if TYPE_CHECKING: # pragma: no cover
9
+ from ..checks import Check
10
+ from ..node import Node
11
+
12
+
13
+ class Stream(Notifier):
14
+ stream: TextIO
15
+
16
+ def __init__(self, stream: TextIO):
17
+ self.stream = stream
18
+
19
+ def send(self, node: 'Node', checks: 'List[Check]') -> None:
20
+ check: Check
21
+ depth: int
22
+ for check, depth in node.iter_flat_depth_checks():
23
+ self.stream.write(
24
+ f'{" " * depth}{check.label}: {check.status.name.title()}\n'
25
+ )
@@ -0,0 +1,2 @@
1
+ from .base import Storage, GroupedStatus, FlatStatus # noqa
2
+ from .csv import CSV # noqa
@@ -0,0 +1,72 @@
1
+ """
2
+ Storage base
3
+ """
4
+ from collections import defaultdict
5
+ from typing import DefaultDict, List, Tuple, Iterator, overload
6
+ from typing_extensions import Literal
7
+
8
+ from ..constants import Status
9
+
10
+
11
+ GroupedStatus = Iterator[Tuple[Status, int]]
12
+ FlatStatus = Iterator[Status]
13
+
14
+
15
+ class Storage:
16
+ """
17
+ Store recent statuses
18
+ """
19
+ path: str
20
+ data: DefaultDict[str, List[Tuple[Status, int]]]
21
+ max_memory: int
22
+
23
+ def __init__(self, max_memory: int = 10) -> None:
24
+ self.data = defaultdict(list)
25
+ self.max_memory = max_memory
26
+
27
+ def load(self) -> None:
28
+ raise NotImplementedError() # pragma: no cover
29
+
30
+ def save(self) -> None:
31
+ raise NotImplementedError() # pragma: no cover
32
+
33
+ def set(self, key: str, status: Status) -> None:
34
+ """
35
+ Add a status to the memory
36
+ """
37
+ count = 1
38
+ if self.data[key]:
39
+ last_status: Status
40
+ last_count: int
41
+ last_status, last_count = self.data[key][-1]
42
+ if last_status == status:
43
+ # Increment the last status rather than creating a new one
44
+ self.data[key].pop()
45
+ count = last_count + 1
46
+
47
+ self.data[key].append((status, count))
48
+
49
+ # Assert max memory
50
+ if len(self.data[key]) > self.max_memory:
51
+ self.data[key] = self.data[key][-self.max_memory:]
52
+
53
+ @overload
54
+ def get(self, key: str, grouped: Literal[False]) -> FlatStatus:
55
+ pass # pragma: no cover
56
+
57
+ @overload # noqa: F811 until flake8 supports PEP 484
58
+ def get(self, key: str, grouped: Literal[True]) -> GroupedStatus:
59
+ pass # pragma: no cover
60
+
61
+ def get(self, key, grouped=False): # noqa: E501,F811 until flake8 supports PEP 484
62
+ """
63
+ Generator to return stored statuses, with most recent first
64
+ """
65
+ status: Status
66
+ count: int
67
+ for status, count in reversed(self.data[key]):
68
+ if grouped:
69
+ yield status, count
70
+ else:
71
+ for i in range(count):
72
+ yield status
disermo/storage/csv.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ CSV storage
3
+ """
4
+ import csv
5
+ import os
6
+
7
+ from ..constants import Status
8
+ from .base import Storage
9
+
10
+
11
+ class CSV(Storage):
12
+ """
13
+ Store records in CSV format:
14
+
15
+ key,status|count,status|count,...
16
+
17
+ with oldest status first
18
+ """
19
+ SEPARATOR = '|'
20
+
21
+ def __init__(self, path: str, max_memory: int = 10) -> None:
22
+ super().__init__(max_memory=max_memory)
23
+ self.path = path
24
+
25
+ def load(self) -> None:
26
+ if not os.path.exists(self.path):
27
+ return
28
+
29
+ with open(self.path, 'r') as file:
30
+ reader = csv.reader(file)
31
+ for row in reader:
32
+ self.data[row[0]] = [
33
+ (Status(int(status)), int(count))
34
+ for (status, count) in [
35
+ cell.split(self.SEPARATOR, 1)
36
+ for cell in row[1:]
37
+ ]
38
+ ]
39
+
40
+ def save(self) -> None:
41
+ with open(self.path, 'w') as file:
42
+ writer = csv.writer(file)
43
+ for key, values in self.data.items():
44
+ writer.writerow([key] + [
45
+ f'{status.value}{self.SEPARATOR}{count}'
46
+ for status, count in values
47
+ ])
disermo/utils.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Generic utility functions
3
+ """
4
+ from enum import Enum
5
+ import re
6
+ import time
7
+
8
+
9
+ _re_camel_to_sentence = re.compile(r'([A-Z]+(?=[A-Z][a-z]|$)|[A-Z][a-z])')
10
+
11
+
12
+ class OrderedEnum(Enum): # pragma: no cover - comes from python docs
13
+ """
14
+ Ordered enum to allow value comparison, from python docs
15
+ """
16
+ def __ge__(self, other):
17
+ if self.__class__ is other.__class__:
18
+ return self.value >= other.value
19
+ return NotImplemented
20
+
21
+ def __gt__(self, other):
22
+ if self.__class__ is other.__class__:
23
+ return self.value > other.value
24
+ return NotImplemented
25
+
26
+ def __le__(self, other):
27
+ if self.__class__ is other.__class__:
28
+ return self.value <= other.value
29
+ return NotImplemented
30
+
31
+ def __lt__(self, other):
32
+ if self.__class__ is other.__class__:
33
+ return self.value < other.value
34
+ return NotImplemented
35
+
36
+
37
+ def camel_to_sentence(val: str) -> str:
38
+ """
39
+ Given a camelCase, convert it into Sentence case
40
+
41
+ It is acroynm-aware
42
+ """
43
+ if not val:
44
+ return ''
45
+
46
+ words = [
47
+ word if all([char.isupper() for char in word]) else word.lower()
48
+ for word in _re_camel_to_sentence.sub(r' \1', val).split(' ')
49
+ if word
50
+ ]
51
+
52
+ sentence = ' '.join(words)
53
+ return f'{sentence[0].upper()}{sentence[1:]}'
54
+
55
+
56
+ class Timer:
57
+ def __init__(self):
58
+ self.start = time.time()
59
+
60
+ def elapsed(self):
61
+ return time.time() - self.start
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: disermo
3
+ Version: 0.0.1
4
+ Summary: Self hosted server monitoring
5
+ Author-email: Richard Terry <code@radiac.net>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/radiac/disermo
8
+ Project-URL: Documentation, https://disermo.readthedocs.io/en/latest/
9
+ Project-URL: Repository, https://github.com/radiac/disermo
10
+ Project-URL: Issues, https://github.com/radiac/disermo/issues
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: System :: Monitoring
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/x-rst
19
+ License-File: LICENSE
20
+ Requires-Dist: typing_extensions
21
+ Dynamic: license-file
22
+
23
+ =======
24
+ Disermo
25
+ =======
26
+
27
+ Self hosted server monitoring.
28
+
29
+
30
+ Installation
31
+ ============
32
+
33
+ Install from PyPI::
34
+
35
+ pip install disermo
36
+
37
+ This requires Python 3.8 or later.
38
+
39
+
40
+ Development
41
+ ===========
42
+
43
+ During development, install in a virtual environment::
44
+
45
+ mkdir disermo
46
+ cd disermo
47
+ git clone <path-to-repo> repo
48
+ python3 -m venv venv
49
+ . venv/bin/activate
50
+ pip install pip-tools
51
+ cd repo
52
+ pip-sync
53
+
54
+
55
+ Quickstart
56
+ ==========
57
+
58
+ Create a Disermo script based on ``example.py``.
59
+
60
+ To run it::
61
+
62
+ /path/to/venv/bin/python /path/to/repo/example.py
63
+
64
+
65
+ Tests
66
+ =====
67
+
68
+ To run tests::
69
+
70
+ cd disermo/repo
71
+ . ../venv/bin/activate
72
+ pytest --flake8 --mypy --cov=disermo --cov-report=term --cov-report=html
@@ -0,0 +1,22 @@
1
+ disermo/__init__.py,sha256=7zpEy5zjgyVlyIfkApAJUUN6y5LkEhNjo7JiH5iACvM,179
2
+ disermo/constants.py,sha256=STm8odd1CrRY6XwVEddwyzkQAGYfW3hom-zFNRAXfd0,141
3
+ disermo/node.py,sha256=LaYhno9ucWCOSCyr34nI8eAyKntaXuyuiIxDxqIkqgw,2675
4
+ disermo/utils.py,sha256=iMpXG-0_Wy9NwKPkQNRafVg2INZlFcqQt5r0FVNff_0,1473
5
+ disermo/checks/__init__.py,sha256=4AAkhhWojk5u0bQKMLy4slcQAXiG7l1xY6dHj6M6Trc,150
6
+ disermo/checks/base.py,sha256=eyrS0OZFEu9aSu0t0bSO8sMMpKbumT28LV57YG4Wu0g,2066
7
+ disermo/checks/remote.py,sha256=0pQV9srEW8FPUPMrCovlCEYIeWBKw2h4wmQFyCzqFuY,3263
8
+ disermo/checks/sensors.py,sha256=6sUi81VfLRU86T-4k9eTZxNm7Afp2c6q69T5dW0TLZ4,64
9
+ disermo/checks/storage.py,sha256=9OYkJW1t_QLLq5zrOern9accXM9fHi5wOSZAKBtUBpo,1126
10
+ disermo/checks/system.py,sha256=MNswaVpnbvDqxB4JTj4lH5Uweeq1_4jOGlt5cU7pNj0,54
11
+ disermo/notifiers/__init__.py,sha256=4OzrWFTs1vdiw3NZN47t9clIvkq3UNhScSElYax-JO0,103
12
+ disermo/notifiers/base.py,sha256=xt9xzoGhy2IFo3eHy9NWYr28OdJom0AlDjo8Wno1SZo,3292
13
+ disermo/notifiers/email.py,sha256=YUq8ZnzG4D_Ft5JlMzyWyGoSF1naX_a0QuPL1W239Fc,1617
14
+ disermo/notifiers/stream.py,sha256=q6dGwQZYrFlAfvkdmptlL-6jFIRJom-Oj6WUH3I6q6Q,600
15
+ disermo/storage/__init__.py,sha256=OdTXE90YWG3YU6bMqlnPuLmwYG2EcevMqAbcyT62wRI,90
16
+ disermo/storage/base.py,sha256=u1TfrAzIl9cqRAlhlv5LIkfvO0bk4e89dURGFlg8a_c,2074
17
+ disermo/storage/csv.py,sha256=K9Al4ZQbtcPR283dYGfOisoPaj4JZ-JQd37i2UH9Dd4,1210
18
+ disermo-0.0.1.dist-info/licenses/LICENSE,sha256=irwtcC7DeK8IxEZ_wGUGmNAqgea3K958MGHGHsKCfHE,1505
19
+ disermo-0.0.1.dist-info/METADATA,sha256=zikAwklXMM7I3GQWQ44O4AHf2hgvpVQOe8B6TkQmIWg,1539
20
+ disermo-0.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ disermo-0.0.1.dist-info/top_level.txt,sha256=F4z0dBujonipf-15c6DDyCixVSHhgXIBnXOLnWmoRvE,8
22
+ disermo-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2019-2026, Richard Terry
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ disermo