astreum 0.2.46__py3-none-any.whl → 0.2.48__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.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

astreum/_node.py CHANGED
@@ -4,7 +4,17 @@ import uuid
4
4
  import threading
5
5
 
6
6
  from ._storage.atom import Atom
7
- from ._lispeum import Env, Expr, low_eval, parse, tokenize, ParseError
7
+ from ._lispeum import Env, Expr, Meter, low_eval, parse, tokenize, ParseError
8
+ from .utils.logging import logging_setup
9
+
10
+ __all__ = [
11
+ "Node",
12
+ "Env",
13
+ "Expr",
14
+ "Meter",
15
+ "parse",
16
+ "tokenize",
17
+ ]
8
18
 
9
19
  def bytes_touched(*vals: bytes) -> int:
10
20
  """For metering: how many bytes were manipulated (max of operands)."""
@@ -12,6 +22,8 @@ def bytes_touched(*vals: bytes) -> int:
12
22
 
13
23
  class Node:
14
24
  def __init__(self, config: dict):
25
+ self.logger = logging_setup(config)
26
+ self.logger.info("Starting Astreum Node")
15
27
  # Storage Setup
16
28
  self.in_memory_storage: Dict[bytes, Atom] = {}
17
29
  self.in_memory_storage_lock = threading.RLock()
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import inspect
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import logging.handlers
9
+ import os
10
+ import pathlib
11
+ import platform
12
+ import queue
13
+ import shutil
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, Optional
16
+
17
+ from blake3 import blake3
18
+
19
+ LOG_ENV_VARS = ("ASTREUM_LOG_DIR", "YOUR_LIB_LOG_DIR")
20
+
21
+ # Fixed identity for all loggers in this library
22
+ _ORG_NAME = "Astreum"
23
+ _PRODUCT_NAME = "lib-py"
24
+
25
+
26
+ def _safe_path(path_str: str) -> Optional[pathlib.Path]:
27
+ try:
28
+ return pathlib.Path(path_str).resolve()
29
+ except Exception:
30
+ try:
31
+ return pathlib.Path(path_str).absolute()
32
+ except Exception:
33
+ return None
34
+
35
+
36
+ def _hash_path(path: pathlib.Path) -> str:
37
+ try:
38
+ data = str(path).encode("utf-8", errors="ignore")
39
+ except Exception:
40
+ data = repr(path).encode("utf-8", errors="ignore")
41
+ return blake3(data).hexdigest()
42
+
43
+
44
+ def _find_caller_path() -> pathlib.Path:
45
+ stack = inspect.stack()
46
+ candidates: list[pathlib.Path] = []
47
+ for frame_info in stack[2:]:
48
+ filename = frame_info.filename
49
+ if not filename:
50
+ continue
51
+ path = _safe_path(filename)
52
+ if path is None:
53
+ continue
54
+ candidates.append(path)
55
+ if "astreum" not in path.parts:
56
+ return path
57
+
58
+ if candidates:
59
+ return candidates[0]
60
+ return pathlib.Path.cwd()
61
+
62
+
63
+ def _derive_instance_id() -> str:
64
+ return _hash_path(_find_caller_path())[:16]
65
+
66
+
67
+ def _log_root(org: str, product: str, instance_id: str) -> pathlib.Path:
68
+ """Resolve the base directory for logs, allowing an override via env vars."""
69
+ for env_var in LOG_ENV_VARS:
70
+ override = os.getenv(env_var)
71
+ if override:
72
+ return pathlib.Path(override) / org / product / "logs" / instance_id
73
+
74
+ if platform.system() == "Windows":
75
+ base = os.getenv("LOCALAPPDATA") or str(pathlib.Path.home())
76
+ return pathlib.Path(base) / org / product / "logs" / instance_id
77
+
78
+ xdg_state = os.getenv("XDG_STATE_HOME")
79
+ base_path = pathlib.Path(xdg_state) if xdg_state else pathlib.Path.home() / ".local" / "state"
80
+ return base_path / org / product / "logs" / instance_id
81
+
82
+
83
+ class JSONFormatter(logging.Formatter):
84
+ """Log record formatter that emits JSON objects per line."""
85
+
86
+ def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
87
+ payload: Dict[str, Any] = {
88
+ "ts": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
89
+ "level": record.levelname,
90
+ "logger": record.name,
91
+ "msg": record.getMessage(),
92
+ "pid": record.process,
93
+ "thread": record.threadName,
94
+ "module": record.module,
95
+ "func": record.funcName,
96
+ "instance_id": getattr(record, "instance_id", None),
97
+ }
98
+
99
+ for key, value in record.__dict__.items():
100
+ if key in payload or key.startswith(("_", "msecs", "relativeCreated")):
101
+ continue
102
+ try:
103
+ json.dumps(value)
104
+ except Exception:
105
+ continue
106
+ payload[key] = value
107
+
108
+ return json.dumps(payload, ensure_ascii=False)
109
+
110
+
111
+ def _gzip_rotator(src: str, dst: str) -> None:
112
+ """Rotate the log file by gzipping it and removing the original."""
113
+ with open(src, "rb") as source, gzip.open(f"{dst}.gz", "wb") as target:
114
+ shutil.copyfileobj(source, target)
115
+ os.remove(src)
116
+
117
+
118
+ def _namer(default_name: str) -> str:
119
+ """Custom name for rotated logs: node-YYYY-MM-DD.log."""
120
+ path = pathlib.Path(default_name)
121
+ parent = path.parent
122
+ name = path.name
123
+ fragments = name.split(".log.")
124
+ if len(fragments) != 2:
125
+ return default_name
126
+ stem, date_part = fragments
127
+ return str(parent / f"{stem}-{date_part}.log")
128
+
129
+
130
+ def _human_line(record: logging.LogRecord) -> str:
131
+ """Format a record as a concise human-readable line."""
132
+ dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
133
+ stamp = f"{dt:%Y-%m-%d}-{dt:%S}-{dt:%M}"
134
+ return f"[{stamp}] [{record.levelname.lower()}] {record.getMessage()}"
135
+
136
+
137
+ class HumanFormatter(logging.Formatter):
138
+ """Simple formatter for optional verbose console output."""
139
+
140
+ def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
141
+ return _human_line(record)
142
+
143
+
144
+ def _shutdown_listener(listener: logging.handlers.QueueListener, handlers: list[logging.Handler]) -> None:
145
+ """Stop the queue listener and close handlers on interpreter exit."""
146
+ try:
147
+ listener.stop()
148
+ except Exception:
149
+ pass
150
+ finally:
151
+ for handler in handlers:
152
+ try:
153
+ handler.close()
154
+ except Exception:
155
+ pass
156
+
157
+
158
+ def logging_setup(config: dict) -> logging.LoggerAdapter:
159
+ """Configure logging according to the runtime config and return an adapter."""
160
+ if config is None:
161
+ config = {}
162
+ elif not isinstance(config, dict):
163
+ config = dict(config)
164
+
165
+ org = _ORG_NAME
166
+ product = _PRODUCT_NAME
167
+ instance_id = _derive_instance_id()
168
+
169
+ retention_value = config.get("retention_days")
170
+ retention_days = int(retention_value) if retention_value is not None else 90
171
+
172
+ verbose = bool(config.get("verbose", False))
173
+
174
+ log_dir = _log_root(org, product, instance_id)
175
+ log_dir.mkdir(parents=True, exist_ok=True)
176
+
177
+ base_file = log_dir / "node.log"
178
+ file_handler = logging.handlers.TimedRotatingFileHandler(
179
+ filename=str(base_file),
180
+ when="midnight",
181
+ interval=1,
182
+ backupCount=max(retention_days, 0),
183
+ utc=True,
184
+ encoding="utf-8",
185
+ delay=True,
186
+ )
187
+ file_handler.setFormatter(JSONFormatter())
188
+ file_handler.rotator = _gzip_rotator
189
+ file_handler.namer = _namer
190
+
191
+ handler_list: list[logging.Handler] = [file_handler]
192
+
193
+ if verbose:
194
+ console_handler = logging.StreamHandler()
195
+ console_handler.setLevel(logging.INFO)
196
+ console_handler.setFormatter(HumanFormatter())
197
+ handler_list.append(console_handler)
198
+
199
+ log_queue: queue.Queue[logging.LogRecord] = queue.Queue(-1)
200
+ queue_handler = logging.handlers.QueueHandler(log_queue)
201
+
202
+ base_logger = logging.getLogger(f"{product}.{instance_id}")
203
+ base_logger.setLevel(logging.INFO)
204
+ base_logger.handlers.clear()
205
+ base_logger.propagate = False
206
+ base_logger.addHandler(queue_handler)
207
+
208
+ listener = logging.handlers.QueueListener(
209
+ log_queue, *handler_list, respect_handler_level=True
210
+ )
211
+ listener.daemon = True
212
+ listener.start()
213
+ atexit.register(_shutdown_listener, listener, handler_list)
214
+
215
+ adapter = logging.LoggerAdapter(base_logger, {"instance_id": instance_id})
216
+ setattr(adapter, "_queue_listener", listener)
217
+ setattr(adapter, "_handlers", handler_list)
218
+
219
+ return adapter
220
+
221
+
222
+ __all__ = [
223
+ "HumanFormatter",
224
+ "JSONFormatter",
225
+ "LOG_ENV_VARS",
226
+ "logging_setup",
227
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.46
3
+ Version: 0.2.48
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -1,5 +1,5 @@
1
1
  astreum/__init__.py,sha256=9tzA27B_eG5wRF1SAWJIV7xTmCcR1QFc123b_cvFOa4,345
2
- astreum/_node.py,sha256=dTwXjnCJZHjUw8dPmH7neaL1-M4rso77Aogo0935vsY,3796
2
+ astreum/_node.py,sha256=ZUeBsvF9C8c8Cnjbpwpsnd-ItdQrgnqRkYlX99-JaZs,4040
3
3
  astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
4
4
  astreum/node.py,sha256=MmlK3jaANTMB3ZAxR8IaSc82OS9meJmVawYIVURADbg,39689
5
5
  astreum/_communication/__init__.py,sha256=XJui0yOcfAur4HKt-8sSRlwB-MSU1rchkuOAY-nKDOE,207
@@ -48,8 +48,9 @@ astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,204
48
48
  astreum/storage/setup.py,sha256=1-9ztEFI_BvRDvAA0lAn4mFya8iq65THTArlj--M3Hg,626
49
49
  astreum/utils/bytes.py,sha256=9QTWC2JCdwWLB5R2mPtmjPro0IUzE58DL3uEul4AheE,846
50
50
  astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
51
- astreum-0.2.46.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
52
- astreum-0.2.46.dist-info/METADATA,sha256=o454zFl45Clnos8EwXm0PCSivnHw8fkoOC3C2mCFXYo,6181
53
- astreum-0.2.46.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- astreum-0.2.46.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
55
- astreum-0.2.46.dist-info/RECORD,,
51
+ astreum/utils/logging.py,sha256=Lg_rQfk-evDH6_k946PLb27YphK7dz68lH_Fxa0Use8,6945
52
+ astreum-0.2.48.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
53
+ astreum-0.2.48.dist-info/METADATA,sha256=fEiABAZ2zx779EOtQl92fhwqYdJjGLKL37trDt9SmfM,6181
54
+ astreum-0.2.48.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ astreum-0.2.48.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
56
+ astreum-0.2.48.dist-info/RECORD,,