traceact 0.2.0__tar.gz → 0.2.1__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.
Files changed (30) hide show
  1. {traceact-0.2.0/traceact.egg-info → traceact-0.2.1}/PKG-INFO +1 -1
  2. {traceact-0.2.0 → traceact-0.2.1}/USAGE.md +1 -1
  3. {traceact-0.2.0 → traceact-0.2.1}/pyproject.toml +1 -1
  4. {traceact-0.2.0 → traceact-0.2.1}/traceact/__init__.py +1 -1
  5. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/reader.py +59 -12
  6. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/server.py +9 -5
  7. {traceact-0.2.0 → traceact-0.2.1/traceact.egg-info}/PKG-INFO +1 -1
  8. {traceact-0.2.0 → traceact-0.2.1}/LICENSE +0 -0
  9. {traceact-0.2.0 → traceact-0.2.1}/MANIFEST.in +0 -0
  10. {traceact-0.2.0 → traceact-0.2.1}/README.md +0 -0
  11. {traceact-0.2.0 → traceact-0.2.1}/setup.cfg +0 -0
  12. {traceact-0.2.0 → traceact-0.2.1}/traceact/budget.py +0 -0
  13. {traceact-0.2.0 → traceact-0.2.1}/traceact/config.py +0 -0
  14. {traceact-0.2.0 → traceact-0.2.1}/traceact/context.py +0 -0
  15. {traceact-0.2.0 → traceact-0.2.1}/traceact/decorators.py +0 -0
  16. {traceact-0.2.0 → traceact-0.2.1}/traceact/helpers.py +0 -0
  17. {traceact-0.2.0 → traceact-0.2.1}/traceact/ids.py +0 -0
  18. {traceact-0.2.0 → traceact-0.2.1}/traceact/sinks.py +0 -0
  19. {traceact-0.2.0 → traceact-0.2.1}/traceact/trace.py +0 -0
  20. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/__init__.py +0 -0
  21. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/cli.py +0 -0
  22. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/instance.py +0 -0
  23. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/static/app.js +0 -0
  24. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/static/index.html +0 -0
  25. {traceact-0.2.0 → traceact-0.2.1}/traceact/viewer/static/styles.css +0 -0
  26. {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/SOURCES.txt +0 -0
  27. {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/dependency_links.txt +0 -0
  28. {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/entry_points.txt +0 -0
  29. {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/requires.txt +0 -0
  30. {traceact-0.2.0 → traceact-0.2.1}/traceact.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: traceact
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: X-ray vision for your code. Lightweight action-level tracing for Python.
5
5
  Author: Mohammed Shehu
6
6
  License-Expression: MIT
@@ -745,7 +745,7 @@ These endpoints are available while a viewer is running. Apps and scripts can ca
745
745
 
746
746
  | Method | Path | Request | Response |
747
747
  |---|---|---|---|
748
- | `GET` | `/api/health` | — | `{"status":"ok","version":"0.2.0","sources":N}` |
748
+ | `GET` | `/api/health` | — | `{"status":"ok","version":"0.2.1","sources":N}` |
749
749
  | `GET` | `/api/sources` | — | `[{"name":"...","path":"..."}]` |
750
750
  | `POST` | `/api/sources` | `{"path":"..."}` | `{"name":"...","path":"..."}` |
751
751
  | `GET` | `/api/pick?type=file\|folder` | — | `{"path":"...","cancelled":bool}` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "traceact"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "X-ray vision for your code. Lightweight action-level tracing for Python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -25,7 +25,7 @@
25
25
  #
26
26
  # ConsoleSink — prints traces to stdout.
27
27
 
28
- __version__ = "0.2.0"
28
+ __version__ = "0.2.1"
29
29
 
30
30
  from traceact.config import configure, reset_config, TraceConfig
31
31
  from traceact.budget import TraceBudget
@@ -84,6 +84,13 @@ class SourceReader:
84
84
  # New files that appear later (e.g. a new shard) start at offset 0 and
85
85
  # so are read from their beginning on the next poll.
86
86
  self._offsets: Dict[str, int] = {}
87
+ # Per-file inode number. When a file is deleted and recreated at the same
88
+ # path the inode changes even if the new file is larger than the old one,
89
+ # so a size-only check would seek into the wrong position. Tracking the
90
+ # inode lets us detect replacement (not just truncation) reliably.
91
+ # st_ino is 0 on Windows (inodes unavailable); the check is a safe no-op
92
+ # there and falls back to the existing truncation-only detection.
93
+ self._inodes: Dict[str, int] = {}
87
94
 
88
95
  # -- initial load ------------------------------------------------------
89
96
 
@@ -110,6 +117,9 @@ class SourceReader:
110
117
  ring.append(obj)
111
118
  # Record where we stopped so poll() reads only new bytes.
112
119
  self._offsets[filepath] = _file_size(filepath)
120
+ # Record which physical file we just read, so poll() can
121
+ # tell a delete+recreate apart from a plain append.
122
+ self._inodes[filepath] = _file_inode(filepath)
113
123
  except OSError:
114
124
  # A file that vanished or can't be opened is simply skipped.
115
125
  continue
@@ -120,22 +130,37 @@ class SourceReader:
120
130
 
121
131
  # -- live tail ---------------------------------------------------------
122
132
 
123
- def poll(self) -> List[Dict[str, Any]]:
133
+ def poll(self, limit: int) -> Dict[str, Any]:
124
134
  """
125
- Return valid traces appended since the last snapshot() or poll().
126
-
127
- Handles three cases per file:
128
- - new bytes appended → read and parse them from the stored offset.
129
- - a brand-new file → offset defaults to 0, so it's read in full.
130
- - a truncated/rotated file (size < stored offset) reset to 0 and
131
- re-read, since the previous contents are gone.
132
-
133
- Returns newest-last (arrival order), which the viewer prepends to the
134
- top of the log as they come in.
135
+ Return traces that arrived since the last snapshot() or poll().
136
+
137
+ Handles four cases per file:
138
+ - new bytes appended → read and parse them from the stored offset.
139
+ - a brand-new file → offset defaults to 0, so it's read in full.
140
+ - a truncated file size < stored offset; reset to 0 and re-read.
141
+ - a deleted+recreated file at the same path its inode changes even
142
+ though the new file may be larger than the old offset, so a
143
+ size-only check can't detect it: it would silently seek into the
144
+ middle of unrelated new content and drop everything before that
145
+ byte position. Detected here by comparing `st_ino` across calls.
146
+
147
+ On a plain append, returns {"kind": "append", "traces": [...]}
148
+ (newest-last, arrival order — the caller prepends these to the top of
149
+ the log). If any file was replaced, the reader instead rebuilds a full
150
+ snapshot from scratch (so no earlier trace is lost) and returns
151
+ {"kind": "snapshot", "traces": [...]} (newest-first) — the caller
152
+ should replace its trace list rather than prepend onto it.
135
153
  """
136
154
  fresh: List[Dict[str, Any]] = []
155
+ replaced = False
137
156
 
138
157
  for filepath in _jsonl_files(self.path):
158
+ inode = _file_inode(filepath)
159
+ prior_inode = self._inodes.get(filepath)
160
+ if prior_inode is not None and inode is not None and inode != prior_inode:
161
+ replaced = True
162
+ self._inodes[filepath] = inode
163
+
139
164
  size = _file_size(filepath)
140
165
  offset = self._offsets.get(filepath, 0)
141
166
 
@@ -159,7 +184,13 @@ class SourceReader:
159
184
  except OSError:
160
185
  continue
161
186
 
162
- return fresh
187
+ if replaced:
188
+ # Discard whatever `fresh` picked up from the wrong byte offset
189
+ # and rebuild cleanly. snapshot() re-reads every file from the
190
+ # start and refreshes both _offsets and _inodes.
191
+ return {"kind": "snapshot", "traces": self.snapshot(limit)}
192
+
193
+ return {"kind": "append", "traces": fresh}
163
194
 
164
195
 
165
196
  # ---------------------------------------------------------------------------
@@ -191,6 +222,22 @@ def _file_size(filepath: str) -> int:
191
222
  return 0
192
223
 
193
224
 
225
+ def _file_inode(filepath: str) -> Optional[int]:
226
+ """
227
+ Inode number for a file, or None if it can't be determined.
228
+
229
+ st_ino is 0 on some Windows filesystems where Python can't populate it;
230
+ treated as unknown there so replacement detection simply doesn't trigger
231
+ (falling back to the existing truncation-only check) rather than firing
232
+ false positives on every poll.
233
+ """
234
+ try:
235
+ ino = os.stat(filepath).st_ino
236
+ except OSError:
237
+ return None
238
+ return ino or None
239
+
240
+
194
241
  def _started_at_key(trace: Dict[str, Any]) -> str:
195
242
  """
196
243
  Sort key for ordering traces by start time. started_at is an ISO 8601
@@ -6,7 +6,7 @@
6
6
  # What it serves:
7
7
  # GET / the single-page app (static/index.html)
8
8
  # GET /static/<file> the app's CSS and JS
9
- # GET /api/health {"status":"ok","version":"0.2.0","sources":N}
9
+ # GET /api/health {"status":"ok","version":"<pkg version>","sources":N}
10
10
  # GET /api/sources list configured sources (JSON)
11
11
  # POST /api/sources add a source by path (JSON body)
12
12
  # GET /api/pick?type=file|folder open a native OS dialog; returns {"path":"..."}
@@ -41,6 +41,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
41
41
  from typing import Dict, Optional
42
42
  from urllib.parse import parse_qs, urlparse
43
43
 
44
+ from traceact import __version__
44
45
  from traceact.viewer.reader import SourceReader
45
46
 
46
47
  # Directory where drag-dropped files are saved so they can be tailed.
@@ -173,7 +174,7 @@ class _Handler(BaseHTTPRequestHandler):
173
174
  def _serve_health(self) -> None:
174
175
  self._send_json(200, {
175
176
  "status": "ok",
176
- "version": "0.2.0",
177
+ "version": __version__,
177
178
  "sources": len(self.server.state.sources), # type: ignore[attr-defined]
178
179
  })
179
180
 
@@ -274,11 +275,14 @@ class _Handler(BaseHTTPRequestHandler):
274
275
 
275
276
  # Phase 2: the live tail — poll for appended traces forever, until
276
277
  # the browser disconnects (which surfaces as a write error).
278
+ # poll() returns kind="snapshot" instead of "append" when a source
279
+ # file was deleted and recreated mid-stream, so the client replaces
280
+ # its trace list instead of prepending onto stale data.
277
281
  while True:
278
282
  time.sleep(_POLL_INTERVAL_SECONDS)
279
- fresh = reader.poll()
280
- if fresh:
281
- self._send_event({"kind": "append", "traces": fresh})
283
+ result = reader.poll(limit)
284
+ if result["traces"]:
285
+ self._send_event(result)
282
286
  else:
283
287
  # A comment line acts as a heartbeat: it keeps the
284
288
  # connection alive and lets us notice a dropped client.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: traceact
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: X-ray vision for your code. Lightweight action-level tracing for Python.
5
5
  Author: Mohammed Shehu
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes