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.
- uyeia-0.1.0/.github/workflows/lint.yml +25 -0
- uyeia-0.1.0/.github/workflows/packaging.yml +50 -0
- uyeia-0.1.0/.github/workflows/tests.yml +62 -0
- uyeia-0.1.0/.gitignore +10 -0
- uyeia-0.1.0/.python-version +1 -0
- uyeia-0.1.0/PKG-INFO +12 -0
- uyeia-0.1.0/README.md +3 -0
- uyeia-0.1.0/pyproject.toml +37 -0
- uyeia-0.1.0/requirements-dev.lock +35 -0
- uyeia-0.1.0/requirements.lock +27 -0
- uyeia-0.1.0/src/uyeia/__init__.py +278 -0
- uyeia-0.1.0/src/uyeia/exceptions.py +2 -0
- uyeia-0.1.0/src/uyeia/serializers.py +32 -0
- uyeia-0.1.0/src/uyeia/type.py +64 -0
- uyeia-0.1.0/tests/conftest.py +35 -0
- uyeia-0.1.0/tests/samples/errors_cache.db +0 -0
- uyeia-0.1.0/tests/samples/uyeia.errors.json +17 -0
- uyeia-0.1.0/tests/samples/uyeia.errors_missing_message.json +6 -0
- uyeia-0.1.0/tests/samples/uyeia.errors_missing_status.json +6 -0
- uyeia-0.1.0/tests/test_cache_operation.py +11 -0
- uyeia-0.1.0/tests/test_config_validation.py +45 -0
- uyeia-0.1.0/tests/test_errors_register.py +54 -0
- uyeia-0.1.0/tests/test_escalation_operation.py +28 -0
- uyeia-0.1.0/tests/test_files_creation.py +26 -0
|
@@ -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 @@
|
|
|
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,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,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
|
+
)
|
|
Binary file
|
|
@@ -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,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")
|