limitsdb 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- limitsdb/__init__.py +0 -0
- limitsdb/cli/__init__.py +0 -0
- limitsdb/cli/ldb_crypt.py +44 -0
- limitsdb/cli/ldb_impl.py +207 -0
- limitsdb/cli/ldb_init.py +60 -0
- limitsdb/cli/ldb_run.py +53 -0
- limitsdb/core/__init__.py +0 -0
- limitsdb/core/ldb_config_loader.py +198 -0
- limitsdb/core/ldb_crypto.py +91 -0
- limitsdb/core/ldb_errors.py +30 -0
- limitsdb/core/ldb_ilm_config.py +228 -0
- limitsdb/core/ldb_logger.py +57 -0
- limitsdb/core/ldb_meta.py +31 -0
- limitsdb/core/ldb_params_config.py +459 -0
- limitsdb/core/ldb_runner.py +810 -0
- limitsdb/core/ldb_status.py +8 -0
- limitsdb/core/ldb_utils.py +332 -0
- limitsdb/db/__init__.py +0 -0
- limitsdb/db/ldb_engine_loader.py +16 -0
- limitsdb/db/ldb_engines.py +347 -0
- limitsdb/db/oracle/__init__.py +0 -0
- limitsdb/db/oracle/ldb_conf.sql +151 -0
- limitsdb/db/oracle/ldb_engine_impl.py +1505 -0
- limitsdb/resources/__init__.py +0 -0
- limitsdb/resources/ilm.example.yml +71 -0
- limitsdb-0.5.0.dist-info/METADATA +586 -0
- limitsdb-0.5.0.dist-info/RECORD +32 -0
- limitsdb-0.5.0.dist-info/WHEEL +4 -0
- limitsdb-0.5.0.dist-info/entry_points.txt +6 -0
- limitsdb-0.5.0.dist-info/licenses/LICENSE +118 -0
- limitsdb-0.5.0.dist-info/licenses/LICENSE-DUAL.md +59 -0
- limitsdb-0.5.0.dist-info/licenses/NOTICE +12 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Domain errors exposed by LimitsDb boundaries.
|
|
2
|
+
|
|
3
|
+
All errors retain their original exception through ``raise ... from exc`` at
|
|
4
|
+
the boundary where low-level parsing, cryptography, database, or worker errors
|
|
5
|
+
are translated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LimitsDbError(Exception):
|
|
10
|
+
"""Base class for recoverable LimitsDb failures."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigurationError(LimitsDbError, ValueError):
|
|
14
|
+
"""Configuration could not be read or interpreted."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SecretError(ConfigurationError):
|
|
18
|
+
"""Secret storage, key management, or decryption failed."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatabaseConnectionError(LimitsDbError, ConnectionError):
|
|
22
|
+
"""A database connection could not be established or checked."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ValidationError(LimitsDbError, ValueError):
|
|
26
|
+
"""Configuration or runtime state failed domain validation."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExecutionError(LimitsDbError, RuntimeError):
|
|
30
|
+
"""An ILM or administrative operation failed."""
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# limitsdb/core/ldb_ilm_config.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import ItemsView, Mapping
|
|
5
|
+
from typing import Any, NotRequired, TypedDict, cast
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from limitsdb.core.ldb_config_loader import load_ilm_config
|
|
10
|
+
|
|
11
|
+
_id_counter = 0
|
|
12
|
+
_loaded_tables: set[tuple[str, str]] = set()
|
|
13
|
+
|
|
14
|
+
_VALID_ROOT_KEYS: set[str] = {"tables"}
|
|
15
|
+
|
|
16
|
+
_VALID_TABLE_KEYS: set[str] = {
|
|
17
|
+
"source_owner",
|
|
18
|
+
"history_owner",
|
|
19
|
+
"table_name",
|
|
20
|
+
"retain_months_source",
|
|
21
|
+
"retain_months_history",
|
|
22
|
+
"exec_day",
|
|
23
|
+
"frecuency",
|
|
24
|
+
"purge_date_expr",
|
|
25
|
+
"additional_filter_expr",
|
|
26
|
+
"history_addtl_filter_expr",
|
|
27
|
+
"history_hint_expr",
|
|
28
|
+
"orphan_check_column",
|
|
29
|
+
"referencing_tables",
|
|
30
|
+
"join_expr",
|
|
31
|
+
"hint_expr",
|
|
32
|
+
"long_columns",
|
|
33
|
+
"source_orphan_purge",
|
|
34
|
+
"has_lob_columns",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_VALID_COND_KEYS: set[str] = _VALID_TABLE_KEYS | {"is_active"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IlmRule(TypedDict):
|
|
41
|
+
"""Normalized engine-neutral ILM rule consumed by the runner."""
|
|
42
|
+
|
|
43
|
+
id: int
|
|
44
|
+
source_owner: str
|
|
45
|
+
history_owner: str
|
|
46
|
+
table_name: str
|
|
47
|
+
retain_months_source: int | None
|
|
48
|
+
retain_months_history: int | None
|
|
49
|
+
exec_day: int | None
|
|
50
|
+
frecuency: str | None
|
|
51
|
+
is_active: str
|
|
52
|
+
purge_date_expr: str | None
|
|
53
|
+
additional_filter_expr: str | None
|
|
54
|
+
history_addtl_filter_expr: str | None
|
|
55
|
+
history_hint_expr: str | None
|
|
56
|
+
source_orphan_purge: str
|
|
57
|
+
orphan_check_column: str | None
|
|
58
|
+
has_lob_columns: str
|
|
59
|
+
referencing_tables: str | None
|
|
60
|
+
join_expr: str | None
|
|
61
|
+
hint_expr: str | None
|
|
62
|
+
long_columns: str | None
|
|
63
|
+
ctl_status: str | None
|
|
64
|
+
cond_expr: NotRequired[str]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _assert_valid_keys(keys: set[str], *, where: str, allowed: set[str]) -> None:
|
|
68
|
+
"""Raise if any key in `keys` is not present in `allowed`."""
|
|
69
|
+
invalid = sorted(k for k in keys if k not in allowed)
|
|
70
|
+
if invalid:
|
|
71
|
+
allowed_sorted = ", ".join(sorted(allowed))
|
|
72
|
+
invalid_sorted = ", ".join(invalid)
|
|
73
|
+
raise KeyError(f"Invalid key(s) in ILM config at {where}: {invalid_sorted}. Expected only: {allowed_sorted}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _normalize_bool(val: Any) -> Any:
|
|
77
|
+
return "Y" if val else "N" if val is not None else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def normalize_table_row(table: dict[str, Any], cond: dict[str, Any]) -> IlmRule:
|
|
81
|
+
global _id_counter
|
|
82
|
+
row: dict[str, Any] = {}
|
|
83
|
+
# Merge the table definition with the condition (excluding nested "conds").
|
|
84
|
+
for key, value in list(table.items()) + list(cond.items()):
|
|
85
|
+
if key == "conds":
|
|
86
|
+
continue
|
|
87
|
+
row[key] = _normalize_bool(value) if isinstance(value, bool) else value
|
|
88
|
+
row["ctl_status"] = None
|
|
89
|
+
# Ensure default flags are present when omitted.
|
|
90
|
+
for key in ["is_active", "source_orphan_purge", "has_lob_columns"]:
|
|
91
|
+
if key not in row:
|
|
92
|
+
row[key] = "N" if key != "is_active" else "Y"
|
|
93
|
+
for key in [
|
|
94
|
+
"source_owner",
|
|
95
|
+
"history_owner",
|
|
96
|
+
"table_name",
|
|
97
|
+
"retain_months_source",
|
|
98
|
+
"retain_months_history",
|
|
99
|
+
"exec_day",
|
|
100
|
+
"frecuency",
|
|
101
|
+
"purge_date_expr",
|
|
102
|
+
"additional_filter_expr",
|
|
103
|
+
"history_addtl_filter_expr",
|
|
104
|
+
"history_hint_expr",
|
|
105
|
+
"orphan_check_column",
|
|
106
|
+
"referencing_tables",
|
|
107
|
+
"join_expr",
|
|
108
|
+
"hint_expr",
|
|
109
|
+
"long_columns",
|
|
110
|
+
]:
|
|
111
|
+
if key not in row:
|
|
112
|
+
row[key] = None
|
|
113
|
+
row["id"] = _id_counter
|
|
114
|
+
_id_counter += 1
|
|
115
|
+
return cast(IlmRule, row)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _ensure_mapping(obj: Any, where: str) -> dict[str, Any]:
|
|
119
|
+
"""Ensure obj is a mapping[str, Any] and return a plain dict[str, Any]."""
|
|
120
|
+
if obj is None:
|
|
121
|
+
return {}
|
|
122
|
+
if not isinstance(obj, Mapping):
|
|
123
|
+
raise TypeError(f"YAML root at {where} must be a mapping (dict). Got {type(obj).__name__}")
|
|
124
|
+
out: dict[str, Any] = {}
|
|
125
|
+
# Explicitly type the ItemsView iterator for mypy.
|
|
126
|
+
obj_items: ItemsView[Any, Any] = obj.items()
|
|
127
|
+
for k_any, v_any in obj_items:
|
|
128
|
+
key: str = str(k_any)
|
|
129
|
+
val: Any = v_any
|
|
130
|
+
out[key] = val
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _ensure_list_of_mappings(obj: Any, where: str) -> list[dict[str, Any]]:
|
|
135
|
+
"""Ensure obj is a list of mapping-like items and normalize each to dict[str, Any]."""
|
|
136
|
+
if obj is None:
|
|
137
|
+
return []
|
|
138
|
+
if not isinstance(obj, list):
|
|
139
|
+
raise TypeError(f"'{where}' must be a list. Got {type(obj).__name__}")
|
|
140
|
+
out: list[dict[str, Any]] = []
|
|
141
|
+
# Cast the heterogeneous list so we can iterate with type checking.
|
|
142
|
+
obj_list: list[Any] = obj
|
|
143
|
+
for i, el_any in enumerate(obj_list):
|
|
144
|
+
if not isinstance(el_any, Mapping):
|
|
145
|
+
raise TypeError(f"'{where}[{i}]' must be a mapping (dict). Got {type(el_any).__name__}")
|
|
146
|
+
el_map: Mapping[Any, Any] = el_any
|
|
147
|
+
d: dict[str, Any] = {}
|
|
148
|
+
el_items: ItemsView[Any, Any] = el_map.items()
|
|
149
|
+
for k_any, v_any in el_items:
|
|
150
|
+
key: str = str(k_any)
|
|
151
|
+
val: Any = v_any
|
|
152
|
+
d[key] = val
|
|
153
|
+
out.append(d)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def load_rows_from_yaml(yaml_path: str) -> list[IlmRule]:
|
|
158
|
+
_loaded_tables: set[tuple[str, str]] = set()
|
|
159
|
+
with open(yaml_path, encoding="utf-8") as f:
|
|
160
|
+
data_any: Any = yaml.safe_load(f)
|
|
161
|
+
data: dict[str, Any] = _ensure_mapping(data_any, yaml_path)
|
|
162
|
+
_assert_valid_keys(set(data.keys()), where=f"{yaml_path} (root)", allowed=_VALID_ROOT_KEYS)
|
|
163
|
+
rows: list[IlmRule] = []
|
|
164
|
+
tables: list[dict[str, Any]] = _ensure_list_of_mappings(data.get("tables"), "tables")
|
|
165
|
+
for idx, table in enumerate(tables):
|
|
166
|
+
table_keys = set(table.keys()) - {"conds"}
|
|
167
|
+
_assert_valid_keys(table_keys, where=f"{yaml_path} tables[{idx}]", allowed=_VALID_TABLE_KEYS)
|
|
168
|
+
owner: str = str(table.get("source_owner", "") or "")
|
|
169
|
+
table_name: str = str(table.get("table_name", "") or "")
|
|
170
|
+
if (owner, table_name) in _loaded_tables:
|
|
171
|
+
raise ValueError(f"Duplicate table in ilm-config-file: {owner}.{table_name}")
|
|
172
|
+
# Normalize nested "conds" entries for the table.
|
|
173
|
+
conds_any = table.get("conds")
|
|
174
|
+
conds: list[dict[str, Any]] = _ensure_list_of_mappings(conds_any, "conds") if conds_any is not None else []
|
|
175
|
+
if not conds:
|
|
176
|
+
conds = [{"is_active": True}]
|
|
177
|
+
active_conds: list[dict[str, Any]] = []
|
|
178
|
+
for c_idx, cond in enumerate(conds):
|
|
179
|
+
_assert_valid_keys(
|
|
180
|
+
set(cond.keys()), where=f"{yaml_path} tables[{idx}].conds[{c_idx}]", allowed=_VALID_COND_KEYS
|
|
181
|
+
)
|
|
182
|
+
if bool(cond.get("is_active")) is True:
|
|
183
|
+
active_conds.append(cond)
|
|
184
|
+
for cond in active_conds:
|
|
185
|
+
rows.append(normalize_table_row(table, cond))
|
|
186
|
+
if active_conds:
|
|
187
|
+
_loaded_tables.add((owner, table_name))
|
|
188
|
+
return rows
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def resolve_and_load_ilm_rows(*, schema: str, profile: str | None, config_dir: str | None = None) -> list[IlmRule]:
|
|
192
|
+
"""Load merged ILM for schema/profile and return normalized rows."""
|
|
193
|
+
ilm_dict: dict[str, Any] = load_ilm_config(schema=schema, profile=profile, explicit_config_dir=config_dir)
|
|
194
|
+
rows: list[IlmRule] = []
|
|
195
|
+
global _loaded_tables, _id_counter
|
|
196
|
+
_loaded_tables = set()
|
|
197
|
+
_id_counter = 0
|
|
198
|
+
_assert_valid_keys(
|
|
199
|
+
set(ilm_dict.keys()), where=f"ILM config for schema={schema} profile={profile} (root)", allowed=_VALID_ROOT_KEYS
|
|
200
|
+
)
|
|
201
|
+
tables: list[dict[str, Any]] = _ensure_list_of_mappings(ilm_dict.get("tables"), "tables")
|
|
202
|
+
for idx, table in enumerate(tables):
|
|
203
|
+
table_keys = set(table.keys()) - {"conds"}
|
|
204
|
+
_assert_valid_keys(
|
|
205
|
+
table_keys, where=f"ILM config tables[{idx}] (schema={schema} profile={profile})", allowed=_VALID_TABLE_KEYS
|
|
206
|
+
)
|
|
207
|
+
owner: str = str(table.get("source_owner", "") or "")
|
|
208
|
+
table_name: str = str(table.get("table_name", "") or "")
|
|
209
|
+
if (owner, table_name) in _loaded_tables:
|
|
210
|
+
raise ValueError(f"Duplicate table in ILM config: {owner}.{table_name}")
|
|
211
|
+
conds_any = table.get("conds")
|
|
212
|
+
conds: list[dict[str, Any]] = _ensure_list_of_mappings(conds_any, "conds") if conds_any is not None else []
|
|
213
|
+
if not conds:
|
|
214
|
+
conds = [{"is_active": True}]
|
|
215
|
+
active_conds: list[dict[str, Any]] = []
|
|
216
|
+
for c_idx, cond in enumerate(conds):
|
|
217
|
+
_assert_valid_keys(
|
|
218
|
+
set(cond.keys()),
|
|
219
|
+
where=f"ILM config tables[{idx}].conds[{c_idx}] (schema={schema} profile={profile})",
|
|
220
|
+
allowed=_VALID_COND_KEYS,
|
|
221
|
+
)
|
|
222
|
+
if bool(cond.get("is_active")) is True:
|
|
223
|
+
active_conds.append(cond)
|
|
224
|
+
for cond in active_conds:
|
|
225
|
+
rows.append(normalize_table_row(table, cond))
|
|
226
|
+
if active_conds:
|
|
227
|
+
_loaded_tables.add((owner, table_name))
|
|
228
|
+
return rows
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ldb_logger.py
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
_base_name = "ldb"
|
|
6
|
+
_configured = False
|
|
7
|
+
_lock = threading.RLock()
|
|
8
|
+
|
|
9
|
+
# Un único formato legible (sin JSON, sin ambientes)
|
|
10
|
+
_FORMAT = logging.Formatter(
|
|
11
|
+
fmt="%(asctime)s [%(levelname)s] %(processName)s-%(name)s: %(message)s",
|
|
12
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configure_logger(level: str = "INFO", base_name: str = _base_name) -> logging.Logger:
|
|
17
|
+
"""Configura el logger base una sola vez (handlers/format)."""
|
|
18
|
+
global _configured, _base_name
|
|
19
|
+
with _lock:
|
|
20
|
+
_base_name = base_name
|
|
21
|
+
base = logging.getLogger(_base_name)
|
|
22
|
+
lvl = getattr(logging, level.upper(), logging.INFO)
|
|
23
|
+
|
|
24
|
+
if not _configured:
|
|
25
|
+
base.setLevel(lvl)
|
|
26
|
+
base.propagate = False # evita duplicar en root
|
|
27
|
+
ch = logging.StreamHandler()
|
|
28
|
+
ch.setLevel(lvl)
|
|
29
|
+
ch.setFormatter(_FORMAT)
|
|
30
|
+
base.addHandler(ch)
|
|
31
|
+
_configured = True
|
|
32
|
+
else:
|
|
33
|
+
base.setLevel(lvl)
|
|
34
|
+
for h in base.handlers:
|
|
35
|
+
h.setLevel(lvl)
|
|
36
|
+
h.setFormatter(_FORMAT)
|
|
37
|
+
return base
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_logger(name: str = _base_name) -> logging.Logger:
|
|
41
|
+
"""Devuelve el logger base o un hijo ('limitsdb.<name>')."""
|
|
42
|
+
if not _configured:
|
|
43
|
+
configure_logger()
|
|
44
|
+
if name == _base_name or name == "":
|
|
45
|
+
return logging.getLogger(_base_name)
|
|
46
|
+
return logging.getLogger(f"{_base_name}.{name}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def reconfigure_logger(level: str = "INFO") -> None:
|
|
50
|
+
"""Ajusta el nivel del logger base y sus handlers."""
|
|
51
|
+
if not _configured:
|
|
52
|
+
return
|
|
53
|
+
lvl = getattr(logging, level.upper(), logging.INFO)
|
|
54
|
+
base = logging.getLogger(_base_name)
|
|
55
|
+
base.setLevel(lvl)
|
|
56
|
+
for h in base.handlers:
|
|
57
|
+
h.setLevel(lvl)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# limitsdb/core/ldb_meta.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Help:
|
|
9
|
+
text: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Cli:
|
|
14
|
+
flag: str # e.g. "--parallel-max"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Secret:
|
|
19
|
+
enabled: bool = True # default marks the field as secret
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Env:
|
|
24
|
+
name: str # e.g. "LDB_PARALLEL_MAX"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class CliOnly:
|
|
29
|
+
"""Marker for fields that are used only through CLI (never stored in config.yml)."""
|
|
30
|
+
|
|
31
|
+
enabled: bool = True
|