uyeia 0.1.0__tar.gz

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.
@@ -0,0 +1,25 @@
1
+ name: Lint
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ lint:
7
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
8
+ runs-on: ubuntu-22.04
9
+ steps:
10
+ - name: Checkout repository
11
+ uses: actions/checkout@v4
12
+ - name: Set up Python
13
+ uses: actions/setup-python@v5
14
+ with:
15
+ python-version: '3.11'
16
+ - name: Install Rye
17
+ uses: eifinger/setup-rye@v4
18
+ with:
19
+ version: '0.43.0'
20
+ - name: Install dependencies
21
+ run: |
22
+ rye sync
23
+ - name: Run Linter
24
+ run: |
25
+ rye run lint
@@ -0,0 +1,50 @@
1
+ name: Packaging
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
8
+ runs-on: ubuntu-22.04
9
+ steps:
10
+ - name: Checkout repository
11
+ uses: actions/checkout@v4
12
+ - name: Set up Python
13
+ uses: actions/setup-python@v5
14
+ with:
15
+ python-version: '3.11'
16
+ - name: Install Rye
17
+ uses: eifinger/setup-rye@v4
18
+ with:
19
+ version: '0.43.0'
20
+ - name: Install dependencies
21
+ run: |
22
+ rye sync
23
+ - name: Clean build directory
24
+ run: |
25
+ rye build --clean
26
+ - name: Build package
27
+ run: |
28
+ rye build
29
+ - name: Upload package
30
+ uses: actions/upload-artifact@v4
31
+ with:
32
+ name: python-package-distributions
33
+ path: dist/
34
+ publish:
35
+ if: startsWith(github.ref, 'refs/tags/')
36
+ runs-on: ubuntu-22.04
37
+ needs: build
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/project/loguru/
41
+ permissions:
42
+ id-token: write
43
+ steps:
44
+ - name: Download package
45
+ uses: actions/download-artifact@v4
46
+ with:
47
+ name: python-package-distributions
48
+ path: dist/
49
+ - name: Publish package
50
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,62 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ tests:
9
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os:
14
+ - ubuntu-22.04
15
+ python-version:
16
+ - '3.8'
17
+ - '3.9'
18
+ - '3.10'
19
+ - '3.11'
20
+ - '3.12'
21
+ - '3.13'
22
+ - pypy-3.10
23
+ allow-failure:
24
+ - false
25
+ include:
26
+ - os: ubuntu-20.04
27
+ python-version: '3.5'
28
+ allow-failure: false
29
+ - os: ubuntu-20.04
30
+ python-version: '3.6'
31
+ allow-failure: false
32
+ - os: windows-2022
33
+ python-version: '3.12'
34
+ allow-failure: false
35
+ - os: macos-13
36
+ python-version: '3.12'
37
+ allow-failure: false
38
+ - os: ubuntu-22.04
39
+ python-version: 3.14-dev
40
+ allow-failure: true
41
+ runs-on: ${{ matrix.os }}
42
+ steps:
43
+ - name: Checkout repository
44
+ uses: actions/checkout@v4
45
+ - name: Set up Python
46
+ uses: actions/setup-python@v5
47
+ with:
48
+ python-version: ${{ matrix.python-version }}
49
+ env:
50
+ # Workaround for https://github.com/actions/setup-python/issues/866
51
+ PIP_TRUSTED_HOST: pypi.python.org pypi.org files.pythonhosted.org
52
+ - name: Install Rye
53
+ uses: eifinger/setup-rye@v4
54
+ with:
55
+ version: '0.43.0'
56
+ - name: Install dependencies
57
+ run: |
58
+ rye sync
59
+ - name: Run tests
60
+ run: |
61
+ rye run test
62
+ continue-on-error: ${{ matrix.allow-failure }}
uyeia-0.1.0/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # venv
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.12.6
uyeia-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: uyeia
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: dair <d.aires@easi.net>
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: jsonschema>=4.23.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # uyeia
11
+
12
+ Describe your project here.
uyeia-0.1.0/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # uyeia
2
+
3
+ Describe your project here.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "uyeia"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ authors = [
6
+ { name = "dair", email = "d.aires@easi.net" }
7
+ ]
8
+ dependencies = [
9
+ "jsonschema>=4.23.0",
10
+ ]
11
+ readme = "README.md"
12
+ requires-python = ">= 3.8"
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.pytest.ini_options]
19
+ testpaths = ["tests"]
20
+
21
+ [tool.rye]
22
+ managed = true
23
+ dev-dependencies = [
24
+ "ruff>=0.9.6",
25
+ "pytest>=8.3.4",
26
+ ]
27
+
28
+ [tool.hatch.metadata]
29
+ allow-direct-references = true
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/uyeia"]
33
+
34
+ [tool.rye.scripts]
35
+ format = 'ruff format'
36
+ test = 'pytest'
37
+ lint = 'ruff check src/uyeia'
@@ -0,0 +1,35 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+ # generate-hashes: false
10
+ # universal: false
11
+
12
+ -e file:.
13
+ attrs==25.1.0
14
+ # via jsonschema
15
+ # via referencing
16
+ iniconfig==2.0.0
17
+ # via pytest
18
+ jsonschema==4.23.0
19
+ # via uyeia
20
+ jsonschema-specifications==2024.10.1
21
+ # via jsonschema
22
+ packaging==24.2
23
+ # via pytest
24
+ pluggy==1.5.0
25
+ # via pytest
26
+ pytest==8.3.4
27
+ referencing==0.36.2
28
+ # via jsonschema
29
+ # via jsonschema-specifications
30
+ rpds-py==0.22.3
31
+ # via jsonschema
32
+ # via referencing
33
+ ruff==0.9.6
34
+ typing-extensions==4.12.2
35
+ # via referencing
@@ -0,0 +1,27 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+ # generate-hashes: false
10
+ # universal: false
11
+
12
+ -e file:.
13
+ attrs==25.1.0
14
+ # via jsonschema
15
+ # via referencing
16
+ jsonschema==4.23.0
17
+ # via uyeia
18
+ jsonschema-specifications==2024.10.1
19
+ # via jsonschema
20
+ referencing==0.36.2
21
+ # via jsonschema
22
+ # via jsonschema-specifications
23
+ rpds-py==0.22.3
24
+ # via jsonschema
25
+ # via referencing
26
+ typing-extensions==4.12.2
27
+ # via referencing
@@ -0,0 +1,278 @@
1
+ import _pickle as cPickle
2
+ import atexit
3
+ import io
4
+ import json
5
+ import logging
6
+ import os
7
+ import pickle
8
+ import threading
9
+ from copy import deepcopy
10
+ from datetime import datetime, timezone
11
+ from typing import Literal
12
+
13
+ import jsonschema
14
+
15
+ from uyeia.exceptions import UYEIAConfigError
16
+ from uyeia.serializers import errors_config_schema
17
+ from uyeia.type import CommonStatus, Config, Status
18
+
19
+ __all__ = ["Watcher", "set_global_config", "get_errors", "register", "reset"]
20
+
21
+ _lock = threading.RLock()
22
+ _uyeia_config: Config = Config()
23
+ __uyeia_init__ = False
24
+ __default_logger__ = logging.getLogger("__UYEIA__")
25
+ __error_mapper__: dict[str, CommonStatus] = {}
26
+
27
+
28
+ def set_global_config(config: Config):
29
+ global _uyeia_config
30
+
31
+ if error := config.validate():
32
+ if __uyeia_init__ and config.error_config_location != _uyeia_config.error_config_location:
33
+ __default_logger__.warning(
34
+ "Error config location will be ignored. Global config has already been initialized."
35
+ )
36
+ raise UYEIAConfigError(f"Invalid UYEIA config: {error}")
37
+
38
+ _uyeia_config = config
39
+ _init_uyeia_env()
40
+
41
+
42
+ def _load_cache():
43
+ if os.path.exists(_uyeia_config.error_cache_location):
44
+ with io.open(_uyeia_config.error_cache_location, "rb") as db:
45
+ return cPickle.load(db)
46
+ return {}
47
+
48
+
49
+ def _load_config():
50
+ path = _uyeia_config.error_config_location
51
+ if os.path.isfile(path) and os.access(path, os.R_OK):
52
+ try:
53
+ with io.open(path, "r") as db_file:
54
+ data = json.load(db_file)
55
+ jsonschema.validate(data, errors_config_schema)
56
+ return data
57
+ except (jsonschema.ValidationError, json.decoder.JSONDecodeError) as e:
58
+ raise UYEIAConfigError("Invalid errors config:", e)
59
+
60
+ with io.open(path, "w") as db_file:
61
+ json.dump({}, db_file)
62
+ return {}
63
+
64
+
65
+ def _init_uyeia_env():
66
+ global __uyeia_init__, __error_mapper__, __root__
67
+
68
+ with _lock:
69
+ __error_mapper__ = _load_config()
70
+ __uyeia_init__ = True
71
+ __root__.load_cache()
72
+
73
+
74
+ class Watcher:
75
+ def __init__(self, name: str | None = None, logger: logging.Logger | None = None):
76
+ self.logger = logger or __default_logger__
77
+ self.name = name or self.logger.name
78
+ self._cache: Status | None = None
79
+
80
+ if not self.name:
81
+ raise ValueError("Name or logger is required for Watcher instance")
82
+
83
+ global __root__
84
+ self.manager = __root__.register(self)
85
+
86
+ if not __uyeia_init__:
87
+ _init_uyeia_env()
88
+
89
+ def __log(self, status: CommonStatus, config: Config):
90
+ level = config.status.get(status["status"])
91
+ if isinstance(level, str):
92
+ level = logging.getLevelName(level)
93
+
94
+ if level:
95
+ self.logger.log(level, status["message"])
96
+ else:
97
+ raise ValueError(
98
+ f"Invalid status: {status['status']}. Not in UYEIA config!"
99
+ )
100
+
101
+ def __is_empty_or_high(self, status: CommonStatus) -> bool:
102
+ levels = list(_uyeia_config.status.keys())
103
+ return not self._cache or levels.index(status["status"]) > levels.index(self._cache["status"])
104
+
105
+ def get_actual_status(self):
106
+ return self._cache
107
+
108
+ def register(self, error_code: str):
109
+ error = __error_mapper__.get(error_code)
110
+ if not error:
111
+ raise ValueError(f"Invalid error code: {error_code}. Not in UYEIA errors config!")
112
+
113
+ if not _uyeia_config.disable_logging:
114
+ self.__log(error, _uyeia_config)
115
+
116
+ with _lock:
117
+ if self.__is_empty_or_high(error):
118
+ if self._cache and self._cache["status"] != error["status"].upper():
119
+ self.manager.delete_entry_cache(self.name, self._cache)
120
+
121
+ self._cache = {
122
+ "status": error["status"].upper(),
123
+ "message": self.__add_timestamp(error["message"]),
124
+ "solution": error.get("solution", _uyeia_config.default_solution),
125
+ "escalation": 0,
126
+ }
127
+ self.manager.write_entry_cache(self.name, self._cache)
128
+
129
+ def release(self):
130
+ if not self._cache:
131
+ return
132
+
133
+ with _lock:
134
+ self.manager.delete_entry_cache(self.name, self._cache)
135
+ self._cache = None
136
+
137
+ def __del__(self):
138
+ self.manager.unregister(self.name)
139
+
140
+ def __add_timestamp(self, error_log: str) -> str:
141
+ return f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} - {error_log}"
142
+
143
+
144
+ class Manager:
145
+ def __init__(self) -> None:
146
+ self.watcherDict: dict[str, Watcher] = {}
147
+ self.__cache = {}
148
+
149
+
150
+ def getWatcher(self, name: str | None = None, logger: logging.Logger | None = None):
151
+ if name and logger:
152
+ raise ValueError(
153
+ "Name or logger is required for Watcher instance! Not both."
154
+ )
155
+
156
+ rv = None
157
+ name = name or getattr(logger, "name", None)
158
+ if not name:
159
+ raise ValueError("Name or logger is required for Watcher instance")
160
+
161
+ with _lock:
162
+ if name in self.watcherDict:
163
+ rv = self.watcherDict[name]
164
+ else:
165
+ rv = Watcher(name, logger)
166
+ return rv
167
+
168
+ def __find_data_watcher(self, watcher_name: str):
169
+ with _lock:
170
+ return next(
171
+ (
172
+ status
173
+ for entries in self.__cache.values()
174
+ for name, status in entries.items()
175
+ if name == watcher_name
176
+ ),
177
+ None,
178
+ )
179
+
180
+ def delete_entry_cache(self, name: str, old_status: Status):
181
+ with _lock:
182
+ self.__cache.get(old_status["status"], {}).pop(name, None)
183
+
184
+ def write_entry_cache(self, name: str, status: Status):
185
+ with _lock:
186
+ self.__cache.setdefault(status["status"], {})[name] = status
187
+
188
+ def get_cache(self):
189
+ return self.__cache
190
+
191
+ def set_cache(self, new_cache):
192
+ with _lock:
193
+ self.__cache = new_cache
194
+ for watcher in self.watcherDict.values():
195
+ watcher._cache = self.__find_data_watcher(watcher.name)
196
+
197
+ def load_cache(self):
198
+ with _lock:
199
+ self.__cache = _load_cache()
200
+
201
+ def clear_cache(self):
202
+ with _lock:
203
+ for watcher in self.watcherDict.values():
204
+ watcher.release()
205
+
206
+ def register(self, watcher: Watcher):
207
+ with _lock:
208
+ self.watcherDict[watcher.name] = watcher
209
+ watcher._cache = self.__find_data_watcher(watcher.name)
210
+ return self
211
+
212
+ def unregister(self, name: str):
213
+ with _lock:
214
+ self.watcherDict.pop(name, None)
215
+
216
+ __root__ = Manager()
217
+
218
+
219
+ def _persist_cache():
220
+ with _lock:
221
+ if __root__.get_cache():
222
+ with io.open(_uyeia_config.error_cache_location, "wb") as db:
223
+ cPickle.dump(__root__.get_cache(), db, protocol=pickle.HIGHEST_PROTOCOL)
224
+
225
+
226
+ def get_errors(mode: Literal["all", "hot", "cold"] = "all"):
227
+ cache = __root__.get_cache()
228
+ if not cache:
229
+ return None
230
+
231
+ if mode == "all":
232
+ return cache
233
+
234
+ if mode not in {"hot", "cold"}:
235
+ raise ValueError(f"Invalid mode: {mode}. Must be 'all', 'hot' or 'cold'!")
236
+
237
+ for status in (_uyeia_config.status.keys() if mode == "cold" else reversed(_uyeia_config.status.keys())):
238
+ if status in cache:
239
+ return next(iter(cache[status].values()), None)
240
+
241
+ return None
242
+
243
+
244
+ def register(error_code: str):
245
+ __root__.getWatcher(logger=__default_logger__).register(error_code)
246
+
247
+
248
+ def reset():
249
+ __root__.getWatcher(logger=__default_logger__).release()
250
+
251
+
252
+ def escalate():
253
+ if _uyeia_config.disable_escalation:
254
+ __default_logger__.warning("Escalate function is disabled in config.")
255
+ return
256
+
257
+ cache = __root__.get_cache()
258
+ with _lock:
259
+ high_status = _uyeia_config.escalation_status or next(reversed(_uyeia_config.status))
260
+ first_status = next(iter(_uyeia_config.status))
261
+
262
+ new_cache = deepcopy(cache)
263
+ to_escalate = {}
264
+
265
+ for status, entries in cache.items():
266
+ if status not in {high_status, first_status}:
267
+ for name, entry in entries.items():
268
+ entry["escalation"] += 1
269
+ if entry["escalation"] >= _uyeia_config.max_escalation:
270
+ to_escalate[name] = entry
271
+ del new_cache[status][name]
272
+
273
+ new_cache.setdefault(high_status, {}).update(to_escalate)
274
+ __root__.set_cache(new_cache)
275
+
276
+
277
+ atexit.register(_persist_cache)
278
+
@@ -0,0 +1,2 @@
1
+ class UYEIAConfigError(Exception):
2
+ pass
@@ -0,0 +1,32 @@
1
+ import logging
2
+
3
+ errors_config_schema = {
4
+ "type": "object",
5
+ "additionalProperties": {
6
+ "type": "object",
7
+ "properties": {
8
+ "status": {"type": "string"},
9
+ "message": {"type": "string"},
10
+ "solution": {"type": "string"},
11
+ },
12
+ "required": ["status", "message"],
13
+ },
14
+ }
15
+
16
+
17
+ def _validate_status_list(status_list: dict[str, int | str]):
18
+ try:
19
+ logger = logging.getLogger("root")
20
+ old_level = logger.getEffectiveLevel()
21
+ for status in status_list.values():
22
+ if not isinstance(status, (int, str)):
23
+ return f"Invalid status: {status}. Must be int or str!"
24
+ if isinstance(status, str):
25
+ logger.setLevel(status)
26
+ if isinstance(status, int):
27
+ if status not in [0, 10, 20, 30, 40, 50]:
28
+ return f"Invalid logging level: {status}. Must be in [0, 10, 20, 30, 40, 50]!"
29
+ logger.setLevel(old_level)
30
+ except Exception as e:
31
+ logger.setLevel(old_level)
32
+ return str(e)
@@ -0,0 +1,64 @@
1
+ import os
2
+ from copy import deepcopy
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Optional, TypedDict
6
+
7
+ from uyeia.serializers import _validate_status_list
8
+
9
+ ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
10
+
11
+
12
+ class DefaultStatusEnum(str, Enum):
13
+ HEALTHY = "HEALTHY"
14
+ RESCUE = "RESCUE"
15
+ PENDING = "PENDING"
16
+ LIMITED = "LIMITED"
17
+ WARNING = "WARNING"
18
+
19
+
20
+ class CommonStatus(TypedDict):
21
+ status: str
22
+ message: str
23
+ solution: str | None
24
+
25
+
26
+ class Status(CommonStatus):
27
+ escalation: int
28
+
29
+
30
+ @dataclass
31
+ class Config:
32
+ status: dict[str, int | str] = field(
33
+ default_factory=lambda: {
34
+ "HEALTHY": 20,
35
+ "PENDING": 20,
36
+ "LIMITED": 30,
37
+ "WARNING": 40,
38
+ "RESCUE": 50,
39
+ }
40
+ )
41
+ escalation_status: str = DefaultStatusEnum.RESCUE.value
42
+ default_healthy: Optional[str] = DefaultStatusEnum.HEALTHY.value
43
+ default_solution: Optional[str] = "Contact your IT admin."
44
+ disable_escalation: bool = False
45
+ max_escalation: int = 5
46
+ disable_logging: bool = False
47
+ error_config_location: str = field(
48
+ default=os.path.join(ROOT_DIR, "uyeia.errors.json")
49
+ )
50
+ error_cache_location: str = os.path.join(ROOT_DIR, "errors_cache.db")
51
+
52
+ def __post_init__(self):
53
+ self.escalation_status = self.escalation_status.upper()
54
+ if self.default_healthy:
55
+ self.default_healthy = self.default_healthy.upper()
56
+
57
+ old_dict = deepcopy(self.status)
58
+ self.status.clear()
59
+ for status in old_dict:
60
+ self.status[status.upper()] = old_dict[status]
61
+
62
+ def validate(self):
63
+ if status_list := _validate_status_list(self.status):
64
+ return status_list
@@ -0,0 +1,35 @@
1
+
2
+
3
+ import os
4
+ from importlib import reload
5
+
6
+ import pytest
7
+
8
+ import uyeia
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def cleanup_tests_folder():
13
+ def delete_file_config(path):
14
+ if os.path.exists(path):
15
+ os.remove(path)
16
+
17
+
18
+ delete_file_config("./tests/uyeia.errors.json")
19
+ delete_file_config("./tests/errors_cache.db")
20
+ yield
21
+ delete_file_config("./tests/uyeia.errors.json")
22
+ delete_file_config("./tests/errors_cache.db")
23
+
24
+ @pytest.fixture(autouse=True)
25
+ def reload_package():
26
+ global uyeia
27
+ reload(uyeia)
28
+ yield
29
+
30
+ @pytest.fixture
31
+ def sample_config():
32
+ return uyeia.Config(
33
+ error_config_location="./tests/samples/uyeia.errors.json",
34
+ error_cache_location="./tests/errors_cache.db",
35
+ )
@@ -0,0 +1,17 @@
1
+ {
2
+ "T404": {
3
+ "status": "WARNING",
4
+ "message": "Page not found",
5
+ "solution": "Check the URL and try again."
6
+ },
7
+ "T405": {
8
+ "status": "LIMITED",
9
+ "message": "Method not allowed",
10
+ "solution": "Check the HTTP method and try again."
11
+ },
12
+ "T501": {
13
+ "status": "LIMITED",
14
+ "message": "Not authorized",
15
+ "solution": "Check your credentials and try again."
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "T404": {
3
+ "status": "WARNING",
4
+ "solution": "Check the URL and try again."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "T404": {
3
+ "message": "Page not found",
4
+ "solution": "Check the URL and try again."
5
+ }
6
+ }
@@ -0,0 +1,11 @@
1
+ import uyeia
2
+
3
+
4
+ def test_read_cache_error():
5
+ config = uyeia.Config(
6
+ error_cache_location="./tests/samples/errors_cache.db",
7
+ error_config_location="./tests/samples/uyeia.errors.json",
8
+ )
9
+ uyeia.set_global_config(config)
10
+ errors = uyeia.get_errors()
11
+ assert errors and len(errors) == 2
@@ -0,0 +1,45 @@
1
+ import pytest
2
+
3
+ import uyeia
4
+
5
+
6
+ def test_error_config_missing_message():
7
+ config = uyeia.Config(
8
+ error_config_location="./tests/samples/uyeia.errors_missing_message.json",
9
+ )
10
+ with pytest.raises(uyeia.UYEIAConfigError):
11
+ uyeia.set_global_config(config)
12
+
13
+ uyeia.reset()
14
+
15
+
16
+ def test_error_config_missing_status():
17
+ config = uyeia.Config(
18
+ error_config_location="./tests/samples/uyeia.errors_missing_status.json",
19
+ )
20
+ with pytest.raises(uyeia.UYEIAConfigError):
21
+ uyeia.set_global_config(config)
22
+
23
+ uyeia.reset()
24
+
25
+
26
+ def test_global_config_unknow_logging_level_int():
27
+ config = uyeia.Config(
28
+ status={
29
+ "HEALTHY": -1,
30
+ }
31
+ )
32
+
33
+ with pytest.raises(uyeia.UYEIAConfigError):
34
+ uyeia.set_global_config(config)
35
+
36
+
37
+ def test_global_config_unknow_logging_level_str():
38
+ config = uyeia.Config(
39
+ status={
40
+ "HEALTHY": "test",
41
+ }
42
+ )
43
+
44
+ with pytest.raises(uyeia.UYEIAConfigError):
45
+ uyeia.set_global_config(config)
@@ -0,0 +1,54 @@
1
+ import uyeia
2
+
3
+
4
+ def test_errors_register(sample_config):
5
+ uyeia.set_global_config(sample_config)
6
+ watcher = uyeia.Watcher("test")
7
+ watcher.register("T404")
8
+
9
+ errors = uyeia.get_errors()
10
+ assert errors and isinstance(errors, dict) and "test" in errors.get("WARNING") # type: ignore
11
+
12
+
13
+ def test_high_errors_register(sample_config):
14
+ uyeia.set_global_config(sample_config)
15
+ watcher1 = uyeia.Watcher("test")
16
+ watcher2 = uyeia.Watcher("test2")
17
+ watcher1.register("T404")
18
+ watcher2.register("T405")
19
+
20
+ error = uyeia.get_errors("hot")
21
+ assert error and error["status"] == "WARNING"
22
+
23
+
24
+ def test_low_errors_register(sample_config):
25
+ uyeia.set_global_config(sample_config)
26
+ watcher1 = uyeia.Watcher("test")
27
+ watcher2 = uyeia.Watcher("test2")
28
+ watcher1.register("T404")
29
+ watcher2.register("T405")
30
+
31
+ error = uyeia.get_errors("cold")
32
+ assert error and error["status"] == "LIMITED"
33
+
34
+
35
+ def test_errors_override(sample_config):
36
+ uyeia.set_global_config(sample_config)
37
+ watcher1 = uyeia.Watcher("test")
38
+ watcher2 = uyeia.Watcher("test2")
39
+ watcher1.register("T404")
40
+ watcher2.register("T405")
41
+ watcher2.register("T501")
42
+
43
+ errors = uyeia.get_errors()
44
+ assert errors and isinstance(errors, dict) and len(errors["LIMITED"]) == 1 # type: ignore
45
+
46
+ def test_errors_same_level(sample_config):
47
+ uyeia.set_global_config(sample_config)
48
+ watcher1 = uyeia.Watcher("test1")
49
+ watcher2 = uyeia.Watcher("test2")
50
+ watcher1.register("T405")
51
+ watcher2.register("T501")
52
+
53
+ errors = uyeia.get_errors()
54
+ assert errors and isinstance(errors, dict) and len(errors["LIMITED"]) == 2 # type: ignore
@@ -0,0 +1,28 @@
1
+ import uyeia
2
+
3
+
4
+ def test_error_escalation(sample_config):
5
+ sample_config.max_escalation = 1
6
+ uyeia.set_global_config(sample_config)
7
+ watcher = uyeia.Watcher("test")
8
+ watcher.register("T404")
9
+
10
+ uyeia.escalate()
11
+ errors = uyeia.get_errors()
12
+ assert errors and isinstance(errors, dict) and "test" in errors.get("RESCUE") # type: ignore
13
+
14
+
15
+
16
+ def test_multi_errors_escalation(sample_config):
17
+ sample_config.max_escalation = 1
18
+ uyeia.set_global_config(sample_config)
19
+ watcher1 = uyeia.Watcher("test1")
20
+ watcher2 = uyeia.Watcher("test2")
21
+ watcher3 = uyeia.Watcher("test3")
22
+ watcher1.register("T404")
23
+ watcher2.register("T405")
24
+ watcher3.register("T501")
25
+
26
+ uyeia.escalate()
27
+ errors = uyeia.get_errors()
28
+ assert errors and isinstance(errors, dict) and len(errors["RESCUE"]) == 3 # type: ignore
@@ -0,0 +1,26 @@
1
+ import atexit
2
+ import os
3
+
4
+ import uyeia
5
+
6
+
7
+ def test_errors_config_creation():
8
+ config = uyeia.Config(
9
+ error_config_location="./tests/uyeia.errors.json",
10
+ )
11
+ uyeia.set_global_config(config)
12
+ watcher = uyeia.Watcher("test")
13
+ watcher.release()
14
+ assert os.path.exists("./tests/uyeia.errors.json")
15
+
16
+
17
+ def test_errors_cache_creation():
18
+ config = uyeia.Config(
19
+ error_cache_location="./tests/errors_cache.db",
20
+ error_config_location="./tests/samples/uyeia.errors.json",
21
+ )
22
+ uyeia.set_global_config(config)
23
+ watcher = uyeia.Watcher("test")
24
+ watcher.register("T404")
25
+ atexit._run_exitfuncs()
26
+ assert os.path.exists("./tests/errors_cache.db")