metricallize 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 migue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: metricallize
3
+ Version: 0.1.0
4
+ Summary: TinyDB-based document storage for simple telemetry/logging with sync, cached and serialized async modes.
5
+ Author: migue
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/metricallize/
8
+ Keywords: tinydb,telemetry,logging,metrics,document-store
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: tinydb>=4.0.0
16
+ Provides-Extra: serialized-async
17
+ Requires-Dist: orjson>=3.9.0; extra == "serialized-async"
18
+ Requires-Dist: blosc2>=2.0.0; extra == "serialized-async"
19
+ Dynamic: license-file
20
+
21
+ # metricallize
22
+
23
+ Uma forma simples de armazenar telemetria/logs em “banco de documentos” usando TinyDB, com 3 modos de persistência:
24
+
25
+ - `readable_sync`: JSON legível (escreve no disco de forma direta)
26
+ - `in_memory_async`: JSON legível com cache em memória (escreve em batch, no flush/close)
27
+ - `serialized_async`: arquivo binário/comprimido (mais rápido/menor, escreve em background; exige flush/close)
28
+
29
+ ## Instalação
30
+
31
+ ```bash
32
+ pip install metricallize
33
+ ```
34
+
35
+ Para usar o modo `serialized_async` (serialização/compressão):
36
+
37
+ ```bash
38
+ pip install "metricallize[serialized_async]"
39
+ ```
40
+
41
+ ## Conceitos
42
+
43
+ - Você cria um `DocumentStorage`
44
+ - Você acessa qualquer table com `storage.db.table("nome")`
45
+ - O decorator `storage.capture(table="...")` registra uma “execução” (status/duração/erro) nessa table
46
+
47
+ Campos automáticos gravados pelo `capture`:
48
+
49
+ - `ts`: `YYYY-MM-DD HH:MM:SS.xxx` (UTC)
50
+ - `name`: `module.qualname` da função
51
+ - `status`: `ok` ou `error`
52
+ - `duration`: `HH:MM:SS`
53
+ - Em caso de erro: `exc_type`, `exc_msg`
54
+
55
+ ## Path e versionamento (por mês)
56
+
57
+ Se você passar `path` como diretório (sem extensão), o arquivo é criado automaticamente em:
58
+
59
+ `<path>/storage/YYYY_MM/day_DD.json` (modos legíveis)
60
+ `<path>/storage/YYYY_MM/day_DD.db` (modo serialized)
61
+
62
+ Você pode controlar o nome base com `name="..."`.
63
+
64
+ ## Uso: readable_sync (JSON legível)
65
+
66
+ ```python
67
+ from pathlib import Path
68
+ from metricallize import DocumentStorage
69
+
70
+ storage_dir = Path.cwd() / "data"
71
+ storage = DocumentStorage(path=storage_dir, mode="readable_sync", name="logger")
72
+
73
+ logger = storage.db.table("loggers")
74
+ logger.insert({"event": "startup"})
75
+ ```
76
+
77
+ ## Uso: in_memory_async (cache em memória, JSON legível)
78
+
79
+ ```python
80
+ from pathlib import Path
81
+ from metricallize import DocumentStorage
82
+
83
+ storage_dir = Path.cwd() / "data"
84
+ storage = DocumentStorage(
85
+ path=storage_dir,
86
+ mode="in_memory_async",
87
+ name="trace",
88
+ write_cache_size=100_000,
89
+ )
90
+
91
+ trace = storage.db.table("trace")
92
+ trace.insert({"event": "step"})
93
+
94
+ storage.flush()
95
+ storage.close()
96
+ ```
97
+
98
+ ## Uso: serialized_async (binário, assíncrono)
99
+
100
+ ```python
101
+ from pathlib import Path
102
+ from metricallize import DocumentStorage
103
+
104
+ storage_dir = Path.cwd() / "data"
105
+ storage = DocumentStorage(path=storage_dir, mode="serialized_async", name="trace")
106
+
107
+ trace = storage.db.table("trace")
108
+ trace.insert({"event": "step"})
109
+
110
+ storage.flush()
111
+ storage.close()
112
+ ```
113
+
114
+ ## Decorator: capture
115
+
116
+ ```python
117
+ from pathlib import Path
118
+ from metricallize import DocumentStorage
119
+
120
+ storage = DocumentStorage(path=Path.cwd() / "data", mode="readable_sync")
121
+ table_name = "trace"
122
+
123
+ @storage.capture(table=table_name)
124
+ def soma(a: int, b: int) -> int:
125
+ return a + b
126
+
127
+ @storage.capture(table=table_name)
128
+ def falha() -> None:
129
+ raise RuntimeError("boom")
130
+
131
+ soma(1, 2)
132
+ try:
133
+ falha()
134
+ except RuntimeError:
135
+ pass
136
+ ```
137
+
138
+ ## Publicação (TestPyPI e PyPI)
139
+
140
+ Build:
141
+
142
+ ```bash
143
+ python -m pip install --upgrade build twine
144
+ python -m build
145
+ ```
146
+
147
+ TestPyPI:
148
+
149
+ ```bash
150
+ python -m twine upload --repository testpypi dist/*
151
+ ```
152
+
153
+ PyPI:
154
+
155
+ ```bash
156
+ python -m twine upload dist/*
157
+ ```
158
+
@@ -0,0 +1,138 @@
1
+ # metricallize
2
+
3
+ Uma forma simples de armazenar telemetria/logs em “banco de documentos” usando TinyDB, com 3 modos de persistência:
4
+
5
+ - `readable_sync`: JSON legível (escreve no disco de forma direta)
6
+ - `in_memory_async`: JSON legível com cache em memória (escreve em batch, no flush/close)
7
+ - `serialized_async`: arquivo binário/comprimido (mais rápido/menor, escreve em background; exige flush/close)
8
+
9
+ ## Instalação
10
+
11
+ ```bash
12
+ pip install metricallize
13
+ ```
14
+
15
+ Para usar o modo `serialized_async` (serialização/compressão):
16
+
17
+ ```bash
18
+ pip install "metricallize[serialized_async]"
19
+ ```
20
+
21
+ ## Conceitos
22
+
23
+ - Você cria um `DocumentStorage`
24
+ - Você acessa qualquer table com `storage.db.table("nome")`
25
+ - O decorator `storage.capture(table="...")` registra uma “execução” (status/duração/erro) nessa table
26
+
27
+ Campos automáticos gravados pelo `capture`:
28
+
29
+ - `ts`: `YYYY-MM-DD HH:MM:SS.xxx` (UTC)
30
+ - `name`: `module.qualname` da função
31
+ - `status`: `ok` ou `error`
32
+ - `duration`: `HH:MM:SS`
33
+ - Em caso de erro: `exc_type`, `exc_msg`
34
+
35
+ ## Path e versionamento (por mês)
36
+
37
+ Se você passar `path` como diretório (sem extensão), o arquivo é criado automaticamente em:
38
+
39
+ `<path>/storage/YYYY_MM/day_DD.json` (modos legíveis)
40
+ `<path>/storage/YYYY_MM/day_DD.db` (modo serialized)
41
+
42
+ Você pode controlar o nome base com `name="..."`.
43
+
44
+ ## Uso: readable_sync (JSON legível)
45
+
46
+ ```python
47
+ from pathlib import Path
48
+ from metricallize import DocumentStorage
49
+
50
+ storage_dir = Path.cwd() / "data"
51
+ storage = DocumentStorage(path=storage_dir, mode="readable_sync", name="logger")
52
+
53
+ logger = storage.db.table("loggers")
54
+ logger.insert({"event": "startup"})
55
+ ```
56
+
57
+ ## Uso: in_memory_async (cache em memória, JSON legível)
58
+
59
+ ```python
60
+ from pathlib import Path
61
+ from metricallize import DocumentStorage
62
+
63
+ storage_dir = Path.cwd() / "data"
64
+ storage = DocumentStorage(
65
+ path=storage_dir,
66
+ mode="in_memory_async",
67
+ name="trace",
68
+ write_cache_size=100_000,
69
+ )
70
+
71
+ trace = storage.db.table("trace")
72
+ trace.insert({"event": "step"})
73
+
74
+ storage.flush()
75
+ storage.close()
76
+ ```
77
+
78
+ ## Uso: serialized_async (binário, assíncrono)
79
+
80
+ ```python
81
+ from pathlib import Path
82
+ from metricallize import DocumentStorage
83
+
84
+ storage_dir = Path.cwd() / "data"
85
+ storage = DocumentStorage(path=storage_dir, mode="serialized_async", name="trace")
86
+
87
+ trace = storage.db.table("trace")
88
+ trace.insert({"event": "step"})
89
+
90
+ storage.flush()
91
+ storage.close()
92
+ ```
93
+
94
+ ## Decorator: capture
95
+
96
+ ```python
97
+ from pathlib import Path
98
+ from metricallize import DocumentStorage
99
+
100
+ storage = DocumentStorage(path=Path.cwd() / "data", mode="readable_sync")
101
+ table_name = "trace"
102
+
103
+ @storage.capture(table=table_name)
104
+ def soma(a: int, b: int) -> int:
105
+ return a + b
106
+
107
+ @storage.capture(table=table_name)
108
+ def falha() -> None:
109
+ raise RuntimeError("boom")
110
+
111
+ soma(1, 2)
112
+ try:
113
+ falha()
114
+ except RuntimeError:
115
+ pass
116
+ ```
117
+
118
+ ## Publicação (TestPyPI e PyPI)
119
+
120
+ Build:
121
+
122
+ ```bash
123
+ python -m pip install --upgrade build twine
124
+ python -m build
125
+ ```
126
+
127
+ TestPyPI:
128
+
129
+ ```bash
130
+ python -m twine upload --repository testpypi dist/*
131
+ ```
132
+
133
+ PyPI:
134
+
135
+ ```bash
136
+ python -m twine upload dist/*
137
+ ```
138
+
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: metricallize
3
+ Version: 0.1.0
4
+ Summary: TinyDB-based document storage for simple telemetry/logging with sync, cached and serialized async modes.
5
+ Author: migue
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/metricallize/
8
+ Keywords: tinydb,telemetry,logging,metrics,document-store
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: tinydb>=4.0.0
16
+ Provides-Extra: serialized-async
17
+ Requires-Dist: orjson>=3.9.0; extra == "serialized-async"
18
+ Requires-Dist: blosc2>=2.0.0; extra == "serialized-async"
19
+ Dynamic: license-file
20
+
21
+ # metricallize
22
+
23
+ Uma forma simples de armazenar telemetria/logs em “banco de documentos” usando TinyDB, com 3 modos de persistência:
24
+
25
+ - `readable_sync`: JSON legível (escreve no disco de forma direta)
26
+ - `in_memory_async`: JSON legível com cache em memória (escreve em batch, no flush/close)
27
+ - `serialized_async`: arquivo binário/comprimido (mais rápido/menor, escreve em background; exige flush/close)
28
+
29
+ ## Instalação
30
+
31
+ ```bash
32
+ pip install metricallize
33
+ ```
34
+
35
+ Para usar o modo `serialized_async` (serialização/compressão):
36
+
37
+ ```bash
38
+ pip install "metricallize[serialized_async]"
39
+ ```
40
+
41
+ ## Conceitos
42
+
43
+ - Você cria um `DocumentStorage`
44
+ - Você acessa qualquer table com `storage.db.table("nome")`
45
+ - O decorator `storage.capture(table="...")` registra uma “execução” (status/duração/erro) nessa table
46
+
47
+ Campos automáticos gravados pelo `capture`:
48
+
49
+ - `ts`: `YYYY-MM-DD HH:MM:SS.xxx` (UTC)
50
+ - `name`: `module.qualname` da função
51
+ - `status`: `ok` ou `error`
52
+ - `duration`: `HH:MM:SS`
53
+ - Em caso de erro: `exc_type`, `exc_msg`
54
+
55
+ ## Path e versionamento (por mês)
56
+
57
+ Se você passar `path` como diretório (sem extensão), o arquivo é criado automaticamente em:
58
+
59
+ `<path>/storage/YYYY_MM/day_DD.json` (modos legíveis)
60
+ `<path>/storage/YYYY_MM/day_DD.db` (modo serialized)
61
+
62
+ Você pode controlar o nome base com `name="..."`.
63
+
64
+ ## Uso: readable_sync (JSON legível)
65
+
66
+ ```python
67
+ from pathlib import Path
68
+ from metricallize import DocumentStorage
69
+
70
+ storage_dir = Path.cwd() / "data"
71
+ storage = DocumentStorage(path=storage_dir, mode="readable_sync", name="logger")
72
+
73
+ logger = storage.db.table("loggers")
74
+ logger.insert({"event": "startup"})
75
+ ```
76
+
77
+ ## Uso: in_memory_async (cache em memória, JSON legível)
78
+
79
+ ```python
80
+ from pathlib import Path
81
+ from metricallize import DocumentStorage
82
+
83
+ storage_dir = Path.cwd() / "data"
84
+ storage = DocumentStorage(
85
+ path=storage_dir,
86
+ mode="in_memory_async",
87
+ name="trace",
88
+ write_cache_size=100_000,
89
+ )
90
+
91
+ trace = storage.db.table("trace")
92
+ trace.insert({"event": "step"})
93
+
94
+ storage.flush()
95
+ storage.close()
96
+ ```
97
+
98
+ ## Uso: serialized_async (binário, assíncrono)
99
+
100
+ ```python
101
+ from pathlib import Path
102
+ from metricallize import DocumentStorage
103
+
104
+ storage_dir = Path.cwd() / "data"
105
+ storage = DocumentStorage(path=storage_dir, mode="serialized_async", name="trace")
106
+
107
+ trace = storage.db.table("trace")
108
+ trace.insert({"event": "step"})
109
+
110
+ storage.flush()
111
+ storage.close()
112
+ ```
113
+
114
+ ## Decorator: capture
115
+
116
+ ```python
117
+ from pathlib import Path
118
+ from metricallize import DocumentStorage
119
+
120
+ storage = DocumentStorage(path=Path.cwd() / "data", mode="readable_sync")
121
+ table_name = "trace"
122
+
123
+ @storage.capture(table=table_name)
124
+ def soma(a: int, b: int) -> int:
125
+ return a + b
126
+
127
+ @storage.capture(table=table_name)
128
+ def falha() -> None:
129
+ raise RuntimeError("boom")
130
+
131
+ soma(1, 2)
132
+ try:
133
+ falha()
134
+ except RuntimeError:
135
+ pass
136
+ ```
137
+
138
+ ## Publicação (TestPyPI e PyPI)
139
+
140
+ Build:
141
+
142
+ ```bash
143
+ python -m pip install --upgrade build twine
144
+ python -m build
145
+ ```
146
+
147
+ TestPyPI:
148
+
149
+ ```bash
150
+ python -m twine upload --repository testpypi dist/*
151
+ ```
152
+
153
+ PyPI:
154
+
155
+ ```bash
156
+ python -m twine upload dist/*
157
+ ```
158
+
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ metricallize.py
4
+ pyproject.toml
5
+ metricallize.egg-info/PKG-INFO
6
+ metricallize.egg-info/SOURCES.txt
7
+ metricallize.egg-info/dependency_links.txt
8
+ metricallize.egg-info/requires.txt
9
+ metricallize.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ tinydb>=4.0.0
2
+
3
+ [serialized_async]
4
+ orjson>=3.9.0
5
+ blosc2>=2.0.0
@@ -0,0 +1 @@
1
+ metricallize
@@ -0,0 +1,357 @@
1
+ import _thread as Thread
2
+ import atexit
3
+ from collections.abc import Callable
4
+ from datetime import datetime, timezone
5
+ from io import BufferedRandom, BufferedWriter
6
+ from pathlib import Path
7
+ from time import sleep
8
+ from typing import Any, Literal, Mapping, Optional, Set, TypeVar, Union, cast
9
+
10
+ from tinydb import TinyDB
11
+ from tinydb.middlewares import CachingMiddleware
12
+ from tinydb.storages import JSONStorage
13
+
14
+ __all__ = ["BetterJSONStoragePatched", "DocumentStorage"]
15
+ __version__ = "0.1.0"
16
+
17
+ JsonDict = dict[str, Any]
18
+ PathLike = str | Path
19
+ F = TypeVar("F", bound=Callable[..., Any])
20
+
21
+
22
+ def _utc_now_iso() -> str:
23
+ dt = datetime.now(timezone.utc)
24
+ return dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
25
+
26
+
27
+ try:
28
+ from blosc2 import compress as _blosc_compress
29
+ from blosc2 import decompress as _blosc_decompress
30
+ except Exception:
31
+ _blosc_compress = None
32
+ _blosc_decompress = None
33
+
34
+ try:
35
+ from orjson import dumps as _orjson_dumps
36
+ from orjson import loads as _orjson_loads
37
+ except Exception:
38
+ _orjson_dumps = None
39
+ _orjson_loads = None
40
+
41
+ try:
42
+ from BetterJSONStorage import BetterJSONStorage as BetterJSONStorage
43
+ except Exception:
44
+ BetterJSONStorage = None
45
+
46
+
47
+ class BetterJSONStoragePatched:
48
+ __slots__ = (
49
+ "_hash",
50
+ "_access_mode",
51
+ "_path",
52
+ "_data",
53
+ "_dump_kwargs",
54
+ "_changed",
55
+ "_running",
56
+ "_shutdown_lock",
57
+ "_handle",
58
+ )
59
+
60
+ _paths: Set[int] = set()
61
+
62
+ def __new__(cls, path: Path, *args: Any, **kwargs: Any):
63
+ h = hash(path)
64
+ if h in cls._paths:
65
+ raise AttributeError(
66
+ f'A BetterJSONStorage object already exists with path < "{path}" >'
67
+ )
68
+ cls._paths.add(h)
69
+ return object.__new__(cls)
70
+
71
+ def __init__(self, path: Path = Path(), access_mode: Literal["r", "r+"] = "r", **kwargs: Any):
72
+ if (
73
+ _orjson_dumps is None
74
+ or _orjson_loads is None
75
+ or _blosc_compress is None
76
+ or _blosc_decompress is None
77
+ ):
78
+ raise ModuleNotFoundError("BetterJSONStoragePatched requer orjson e blosc2 instalados")
79
+
80
+ self._shutdown_lock = Thread.allocate_lock()
81
+ self._running = True
82
+ self._changed = False
83
+
84
+ self._hash = hash(path)
85
+ self._handle: Optional[Union[BufferedWriter, BufferedRandom]] = None
86
+
87
+ if access_mode not in {"r", "r+"}:
88
+ self.close()
89
+ raise AttributeError(f'access_mode is not one of ("r", "r+"), :{access_mode}')
90
+
91
+ if not isinstance(path, Path):
92
+ self.close()
93
+ raise TypeError("path is not an instance of pathlib.Path")
94
+
95
+ if not path.exists():
96
+ if access_mode == "r":
97
+ self.close()
98
+ raise FileNotFoundError(
99
+ f"File can't be found, use access_mode='r+' if you want to create it. Path: <{path.absolute()}>"
100
+ )
101
+ path.parent.mkdir(parents=True, exist_ok=True)
102
+ self._handle = path.open("wb+")
103
+
104
+ if not path.is_file():
105
+ self.close()
106
+ raise FileNotFoundError(f"path does not lead to a file: <{path.absolute()}>.")
107
+ else:
108
+ self._handle = path.open("rb+")
109
+
110
+ self._access_mode = access_mode
111
+ self._path = path
112
+ self._dump_kwargs = {k: v for k, v in kwargs.items() if k in {"default", "option"}}
113
+ self._data: Mapping[str, Any]
114
+
115
+ self.load()
116
+
117
+ if access_mode == "r+":
118
+ Thread.start_new_thread(self.__file_writer, ())
119
+
120
+ def read(self) -> Mapping[str, Any]:
121
+ return self._data
122
+
123
+ def __file_writer(self) -> None:
124
+ self._shutdown_lock.acquire()
125
+ while self._running:
126
+ if self._changed:
127
+ self._changed = False
128
+ self._handle.seek(0)
129
+ payload = _orjson_dumps(self._data, **self._dump_kwargs)
130
+ compressed = _blosc_compress(payload, typesize=1)
131
+ self._handle.write(compressed)
132
+ self._handle.truncate()
133
+ self._handle.flush()
134
+ else:
135
+ sleep(0.001)
136
+ self._shutdown_lock.release()
137
+
138
+ def write(self, data: Mapping[str, Any]) -> None:
139
+ if self._access_mode != "r+":
140
+ raise PermissionError("Storage is openend as read only")
141
+ self._data = data
142
+ self._changed = True
143
+
144
+ def load(self) -> None:
145
+ db_bytes = self._path.read_bytes()
146
+ if len(db_bytes):
147
+ self._data = cast(Mapping[str, Any], _orjson_loads(_blosc_decompress(db_bytes)))
148
+ else:
149
+ self._data = {}
150
+
151
+ def close(self) -> None:
152
+ while self._changed:
153
+ sleep(0.001)
154
+ self._running = False
155
+ self._shutdown_lock.acquire()
156
+ if self._handle is not None:
157
+ self._handle.flush()
158
+ self._handle.close()
159
+ self.__class__._paths.discard(self._hash)
160
+
161
+ def flush(self) -> None:
162
+ while self._changed:
163
+ sleep(0.001)
164
+
165
+
166
+ StorageMode = Literal[
167
+ "readable_sync",
168
+ "in_memory_async",
169
+ "serialized_async",
170
+ ]
171
+
172
+
173
+ class DocumentStorage:
174
+ """Example:
175
+ import os
176
+ from datetime import datetime
177
+ from pathlib import Path
178
+
179
+ from metricallize import DocumentStorage
180
+
181
+ storage_dir = Path(os.getcwd()) / "data"
182
+ process_id = datetime.now().strftime("%H%M%S")
183
+ storage = DocumentStorage(
184
+ path=storage_dir,
185
+ mode="readable_sync",
186
+ )
187
+
188
+ metrics = storage.db.table(process_id)
189
+ """
190
+
191
+ def __init__(
192
+ self,
193
+ path: PathLike,
194
+ *,
195
+ mode: StorageMode = "readable_sync",
196
+ access_mode: str = "r+",
197
+ storage: Any | None = None,
198
+ storage_kwargs: Mapping[str, Any] | None = None,
199
+ write_cache_size: int = 10000,
200
+ name: str = "storage",
201
+ auto_close: bool = True,
202
+ ) -> None:
203
+ self._closed = False
204
+ self._mode = mode
205
+
206
+ raw_path = Path(path)
207
+ if raw_path.suffix:
208
+ final_path = raw_path
209
+ else:
210
+ base_dir = raw_path
211
+ now = datetime.now()
212
+ month_dir = base_dir / "storage" / now.strftime("%Y_%m")
213
+ month_dir.mkdir(parents=True, exist_ok=True)
214
+ final_path = (
215
+ month_dir / f"day_{now.strftime('%d')}{self._default_extension(self._mode)}"
216
+ )
217
+
218
+ self._path = final_path
219
+ self._path.parent.mkdir(parents=True, exist_ok=True)
220
+
221
+ if storage is None:
222
+ merged_storage_kwargs: dict[str, Any] = dict(storage_kwargs) if storage_kwargs else {}
223
+ if self._mode in {"readable_sync", "in_memory_async"}:
224
+ merged_storage_kwargs.setdefault("indent", 4)
225
+ merged_storage_kwargs.setdefault("ensure_ascii", False)
226
+
227
+ if self._mode == "in_memory_async":
228
+ caching = CachingMiddleware(JSONStorage)
229
+ caching.WRITE_CACHE_SIZE = int(write_cache_size)
230
+ storage = caching
231
+ elif self._mode == "serialized_async":
232
+ if _orjson_dumps is not None and _blosc_compress is not None:
233
+ storage = BetterJSONStoragePatched
234
+ elif BetterJSONStorage is not None:
235
+ storage = BetterJSONStorage
236
+ else:
237
+ raise ModuleNotFoundError(
238
+ "Modo serialized_async requer BetterJSONStorage (ou orjson+blosc2 para BetterJSONStoragePatched)"
239
+ )
240
+ else:
241
+ storage = JSONStorage
242
+ storage_kwargs = merged_storage_kwargs
243
+ self._db = TinyDB(
244
+ self._path,
245
+ storage=storage,
246
+ access_mode=access_mode,
247
+ **(dict(storage_kwargs) if storage_kwargs else {}),
248
+ )
249
+
250
+ if auto_close:
251
+ atexit.register(self.close)
252
+
253
+ @staticmethod
254
+ def _default_extension(mode: str) -> str:
255
+ if mode == "serialized_async":
256
+ return ".db"
257
+ return ".json"
258
+
259
+ @property
260
+ def path(self) -> Path:
261
+ return self._path
262
+
263
+ @property
264
+ def db(self) -> TinyDB:
265
+ return self._db
266
+
267
+ def close(self) -> None:
268
+ if self._closed:
269
+ return
270
+ self._closed = True
271
+ self._db.close()
272
+
273
+ def flush(self) -> None:
274
+ storage = getattr(self._db, "_storage", None)
275
+ if storage is None:
276
+ return
277
+ flush = getattr(storage, "flush", None)
278
+ if flush is None:
279
+ return
280
+ flush()
281
+
282
+ def __enter__(self) -> "DocumentStorage":
283
+ return self
284
+
285
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
286
+ self.close()
287
+
288
+ def __span(
289
+ self,
290
+ name: str,
291
+ *,
292
+ fields: Mapping[str, Any] | None = None,
293
+ status: str = "ok",
294
+ duration_s: float | None = None,
295
+ duration_ms: float | None = None,
296
+ table: str = "spans",
297
+ ) -> int:
298
+ if duration_s is not None and duration_ms is not None:
299
+ raise ValueError("Use apenas um: duration_s ou duration_ms")
300
+
301
+ doc: JsonDict = {
302
+ "ts": _utc_now_iso(),
303
+ "name": name,
304
+ "status": status,
305
+ **(dict(fields) if fields else {}),
306
+ }
307
+
308
+ total_s: float | None
309
+ if duration_s is not None:
310
+ total_s = float(duration_s)
311
+ elif duration_ms is not None:
312
+ total_s = float(duration_ms) / 1000.0
313
+ else:
314
+ total_s = None
315
+
316
+ if total_s is not None:
317
+ total_s = max(0.0, total_s)
318
+ h = int(total_s // 3600)
319
+ m = int((total_s % 3600) // 60)
320
+ s = int(total_s % 60)
321
+ doc["duration"] = f"{h:02d}:{m:02d}:{s:02d}"
322
+ return int(self._db.table(table).insert(doc))
323
+
324
+ def capture(self, table: str = "traces") -> Callable[[F], F]:
325
+ def decorator(func: F) -> F:
326
+ span_name = f"{func.__module__}.{getattr(func, '__qualname__', func.__name__)}"
327
+
328
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
329
+ start = datetime.now(timezone.utc)
330
+ try:
331
+ result = func(*args, **kwargs)
332
+ except Exception as exc:
333
+ end = datetime.now(timezone.utc)
334
+ self.__span(
335
+ span_name,
336
+ status="error",
337
+ duration_s=(end - start).total_seconds(),
338
+ table=table,
339
+ fields={
340
+ "exc_type": type(exc).__name__,
341
+ "exc_msg": str(exc),
342
+ },
343
+ )
344
+ raise
345
+ else:
346
+ end = datetime.now(timezone.utc)
347
+ self.__span(
348
+ span_name,
349
+ status="ok",
350
+ duration_s=(end - start).total_seconds(),
351
+ table=table,
352
+ )
353
+ return result
354
+
355
+ return cast(F, wrapper)
356
+
357
+ return decorator
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "metricallize"
7
+ version = "0.1.0"
8
+ description = "TinyDB-based document storage for simple telemetry/logging with sync, cached and serialized async modes."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "migue" }]
12
+ license = { text = "MIT" }
13
+ dependencies = ["tinydb>=4.0.0"]
14
+ keywords = ["tinydb", "telemetry", "logging", "metrics", "document-store"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ serialized_async = ["orjson>=3.9.0", "blosc2>=2.0.0"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://pypi.org/project/metricallize/"
26
+
27
+ [tool.setuptools]
28
+ py-modules = ["metricallize"]
29
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+