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.
- metricallize-0.1.0/LICENSE +21 -0
- metricallize-0.1.0/PKG-INFO +158 -0
- metricallize-0.1.0/README.md +138 -0
- metricallize-0.1.0/metricallize.egg-info/PKG-INFO +158 -0
- metricallize-0.1.0/metricallize.egg-info/SOURCES.txt +9 -0
- metricallize-0.1.0/metricallize.egg-info/dependency_links.txt +1 -0
- metricallize-0.1.0/metricallize.egg-info/requires.txt +5 -0
- metricallize-0.1.0/metricallize.egg-info/top_level.txt +1 -0
- metricallize-0.1.0/metricallize.py +357 -0
- metricallize-0.1.0/pyproject.toml +29 -0
- metricallize-0.1.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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
|
+
|