smartdump 0.1.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.
- client/__init__.py +3 -0
- client/sd.py +156 -0
- server/__init__.py +0 -0
- server/app.py +95 -0
- server/manager.py +43 -0
- smartdump-0.1.0.dist-info/METADATA +172 -0
- smartdump-0.1.0.dist-info/RECORD +10 -0
- smartdump-0.1.0.dist-info/WHEEL +5 -0
- smartdump-0.1.0.dist-info/entry_points.txt +3 -0
- smartdump-0.1.0.dist-info/top_level.txt +2 -0
client/__init__.py
ADDED
client/sd.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SmartDebugger client — ds() function.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from client import sd
|
|
6
|
+
|
|
7
|
+
sd(my_var)
|
|
8
|
+
sd(my_var, label="user object")
|
|
9
|
+
sd(my_var, label="error", level="error")
|
|
10
|
+
|
|
11
|
+
Works in both sync and async FastAPI routes. Non-blocking.
|
|
12
|
+
"""
|
|
13
|
+
import inspect
|
|
14
|
+
import json
|
|
15
|
+
import threading
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
import requests
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Configuration
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_config = {
|
|
26
|
+
"server_url": "http://localhost:8765",
|
|
27
|
+
"timeout": 1.0, # seconds — kept short so it never blocks
|
|
28
|
+
"enabled": True,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def configure(
|
|
32
|
+
server_url: str = "http://localhost:8765",
|
|
33
|
+
timeout: float = 1.0,
|
|
34
|
+
enabled: bool = True,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Override default ds() configuration."""
|
|
37
|
+
_config["server_url"] = server_url.rstrip("/")
|
|
38
|
+
_config["timeout"] = timeout
|
|
39
|
+
_config["enabled"] = enabled
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Serialisation helpers
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def _safe_serialize(obj: Any) -> Any:
|
|
46
|
+
"""
|
|
47
|
+
Recursively convert an arbitrary Python object into something JSON-safe.
|
|
48
|
+
Falls back to repr() for unrecognised types.
|
|
49
|
+
"""
|
|
50
|
+
if obj is None or isinstance(obj, (bool, int, float, str)):
|
|
51
|
+
return obj
|
|
52
|
+
|
|
53
|
+
if isinstance(obj, dict):
|
|
54
|
+
return {str(k): _safe_serialize(v) for k, v in obj.items()}
|
|
55
|
+
|
|
56
|
+
if isinstance(obj, (list, tuple, set, frozenset)):
|
|
57
|
+
return [_safe_serialize(item) for item in obj]
|
|
58
|
+
|
|
59
|
+
if isinstance(obj, bytes):
|
|
60
|
+
try:
|
|
61
|
+
return obj.decode("utf-8")
|
|
62
|
+
except UnicodeDecodeError:
|
|
63
|
+
return obj.hex()
|
|
64
|
+
|
|
65
|
+
if isinstance(obj, datetime):
|
|
66
|
+
return obj.isoformat()
|
|
67
|
+
|
|
68
|
+
if isinstance(obj, BaseException):
|
|
69
|
+
return {
|
|
70
|
+
"__exception__": type(obj).__name__,
|
|
71
|
+
"message": str(obj),
|
|
72
|
+
"args": [_safe_serialize(a) for a in obj.args],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Dataclasses, Pydantic models, plain objects
|
|
76
|
+
if hasattr(obj, "__dict__"):
|
|
77
|
+
return {
|
|
78
|
+
"__type__": type(obj).__qualname__,
|
|
79
|
+
**{k: _safe_serialize(v) for k, v in obj.__dict__.items()
|
|
80
|
+
if not k.startswith("_")},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Anything else → safe string
|
|
84
|
+
return repr(obj)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_payload(
|
|
88
|
+
data: Any,
|
|
89
|
+
label: Optional[str],
|
|
90
|
+
level: str,
|
|
91
|
+
caller_frame,
|
|
92
|
+
) -> dict:
|
|
93
|
+
filename = caller_frame.f_code.co_filename
|
|
94
|
+
return {
|
|
95
|
+
"id": str(uuid.uuid4()),
|
|
96
|
+
"label": label,
|
|
97
|
+
"level": level,
|
|
98
|
+
"type": type(data).__name__,
|
|
99
|
+
"data": _safe_serialize(data),
|
|
100
|
+
"meta": {
|
|
101
|
+
"file": filename,
|
|
102
|
+
"line": caller_frame.f_lineno,
|
|
103
|
+
"function": caller_frame.f_code.co_name,
|
|
104
|
+
},
|
|
105
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Background sender (daemon thread → never blocks the request lifecycle)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _send_to_server(payload: dict) -> None:
|
|
114
|
+
try:
|
|
115
|
+
requests.post(
|
|
116
|
+
f"{_config['server_url']}/dump",
|
|
117
|
+
json=payload,
|
|
118
|
+
timeout=_config["timeout"],
|
|
119
|
+
)
|
|
120
|
+
except Exception:
|
|
121
|
+
# Silently ignore — server may not be running; never crash the app
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Public API
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def sd(
|
|
130
|
+
data: Any,
|
|
131
|
+
label: Optional[str] = None,
|
|
132
|
+
level: str = "info",
|
|
133
|
+
) -> Any:
|
|
134
|
+
"""
|
|
135
|
+
SmartDebugger — send *data* to the SmartDebugger viewer.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
data: Any Python value to inspect.
|
|
139
|
+
label: Optional human-readable label shown in the UI.
|
|
140
|
+
level: One of "info" | "debug" | "warning" | "error".
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
*data* unchanged, so you can inline the call:
|
|
144
|
+
``return sd(result, "final result")``
|
|
145
|
+
"""
|
|
146
|
+
if not _config["enabled"]:
|
|
147
|
+
return data
|
|
148
|
+
|
|
149
|
+
# Capture the caller's frame *before* spawning a thread
|
|
150
|
+
caller_frame = inspect.currentframe().f_back
|
|
151
|
+
payload = _build_payload(data, label, level, caller_frame)
|
|
152
|
+
|
|
153
|
+
# Fire-and-forget on a daemon thread — works in sync and async contexts
|
|
154
|
+
threading.Thread(target=_send_to_server, args=(payload,), daemon=True).start()
|
|
155
|
+
|
|
156
|
+
return data
|
server/__init__.py
ADDED
|
File without changes
|
server/app.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SmartDebugger server.
|
|
3
|
+
|
|
4
|
+
Endpoints:
|
|
5
|
+
POST /dump — receive a dump from the client library
|
|
6
|
+
GET /dumps — return all stored dumps (newest first)
|
|
7
|
+
GET /dumps/clear — clear all stored dumps
|
|
8
|
+
WS /ws — real-time WebSocket feed for the UI
|
|
9
|
+
GET / — serve the web UI
|
|
10
|
+
GET /health — health check
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from collections import deque
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
19
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
|
21
|
+
from fastapi.staticfiles import StaticFiles
|
|
22
|
+
|
|
23
|
+
from .manager import ConnectionManager
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# App setup
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
app = FastAPI(title="SmartDebugger Server", docs_url=None, redoc_url=None)
|
|
30
|
+
|
|
31
|
+
app.add_middleware(
|
|
32
|
+
CORSMiddleware,
|
|
33
|
+
allow_origins=["*"],
|
|
34
|
+
allow_methods=["*"],
|
|
35
|
+
allow_headers=["*"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# In-memory store — keep the 200 most recent dumps
|
|
39
|
+
MAX_DUMPS = 200
|
|
40
|
+
_dumps: deque[dict] = deque(maxlen=MAX_DUMPS)
|
|
41
|
+
|
|
42
|
+
manager = ConnectionManager()
|
|
43
|
+
|
|
44
|
+
STATIC_DIR = Path(__file__).parent.parent / "static"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Routes
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
@app.get("/health")
|
|
52
|
+
async def health() -> dict:
|
|
53
|
+
return {"status": "ok", "clients": manager.client_count, "dumps": len(_dumps)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.post("/dump")
|
|
57
|
+
async def receive_dump(payload: dict) -> dict:
|
|
58
|
+
"""Accept a dump from the client library and broadcast it to all UI tabs."""
|
|
59
|
+
_dumps.appendleft(payload)
|
|
60
|
+
await manager.broadcast({"event": "dump", "payload": payload})
|
|
61
|
+
return {"ok": True}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.get("/dumps")
|
|
65
|
+
async def get_dumps() -> list:
|
|
66
|
+
"""Return all stored dumps, newest first."""
|
|
67
|
+
return list(_dumps)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.delete("/dumps")
|
|
71
|
+
async def clear_dumps() -> dict:
|
|
72
|
+
"""Clear all stored dumps and notify connected clients."""
|
|
73
|
+
_dumps.clear()
|
|
74
|
+
await manager.broadcast({"event": "clear"})
|
|
75
|
+
return {"ok": True}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.websocket("/ws")
|
|
79
|
+
async def websocket_endpoint(ws: WebSocket) -> None:
|
|
80
|
+
await manager.connect(ws)
|
|
81
|
+
try:
|
|
82
|
+
while True:
|
|
83
|
+
# Keep connection alive; client doesn't need to send anything
|
|
84
|
+
await ws.receive_text()
|
|
85
|
+
except WebSocketDisconnect:
|
|
86
|
+
manager.disconnect(ws)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.get("/")
|
|
90
|
+
async def serve_ui() -> FileResponse:
|
|
91
|
+
return FileResponse(STATIC_DIR / "index.html")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Mount static files (JS, CSS) under /static
|
|
95
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
server/manager.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket connection manager.
|
|
3
|
+
Keeps track of every connected browser tab and broadcasts payloads to all.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
from typing import Set
|
|
9
|
+
|
|
10
|
+
from fastapi import WebSocket
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConnectionManager:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._clients: Set[WebSocket] = set()
|
|
16
|
+
|
|
17
|
+
async def connect(self, ws: WebSocket) -> None:
|
|
18
|
+
await ws.accept()
|
|
19
|
+
self._clients.add(ws)
|
|
20
|
+
|
|
21
|
+
def disconnect(self, ws: WebSocket) -> None:
|
|
22
|
+
self._clients.discard(ws)
|
|
23
|
+
|
|
24
|
+
async def broadcast(self, payload: dict) -> None:
|
|
25
|
+
"""Send *payload* to every connected client, removing dead connections."""
|
|
26
|
+
if not self._clients:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
message = json.dumps(payload)
|
|
30
|
+
dead: list[WebSocket] = []
|
|
31
|
+
|
|
32
|
+
for client in list(self._clients):
|
|
33
|
+
try:
|
|
34
|
+
await client.send_text(message)
|
|
35
|
+
except Exception:
|
|
36
|
+
dead.append(client)
|
|
37
|
+
|
|
38
|
+
for ws in dead:
|
|
39
|
+
self._clients.discard(ws)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def client_count(self) -> int:
|
|
43
|
+
return len(self._clients)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartdump
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time variable dumper for FastAPI — inspired by Laradumps
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: fastapi,debug,dump,developer-tools
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: fastapi>=0.110.0
|
|
10
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
11
|
+
Requires-Dist: requests>=2.31.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# ⚡ SmartDebugger
|
|
18
|
+
|
|
19
|
+
Real-time variable dump viewer for FastAPI — inspired by [Laradumps](https://laradumps.dev).
|
|
20
|
+
|
|
21
|
+
Call `sd(variable)` anywhere in your FastAPI app and see it appear instantly in the browser.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
### 1. Install dependencies
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install fastapi uvicorn requests
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Start the debugger server
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python run.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open **http://localhost:8765** in your browser.
|
|
40
|
+
|
|
41
|
+
### 3. Use `sd()` in your FastAPI app
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from client import ds
|
|
45
|
+
|
|
46
|
+
@app.get("/users/{user_id}")
|
|
47
|
+
async def get_user(user_id: int):
|
|
48
|
+
user = db.get_user(user_id)
|
|
49
|
+
sd(user, label="user from DB") # dumps to the UI, non-blocking
|
|
50
|
+
return user
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Project structure
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
smart_debugger/
|
|
59
|
+
├── client/ # Python client library (the sd() function)
|
|
60
|
+
│ ├── __init__.py
|
|
61
|
+
│ └── ds.py
|
|
62
|
+
├── server/ # FastAPI dump server
|
|
63
|
+
│ ├── __init__.py
|
|
64
|
+
│ ├── app.py # Routes: POST /dump, GET /dumps, WS /ws
|
|
65
|
+
│ └── manager.py # WebSocket connection manager
|
|
66
|
+
├── static/ # Web UI (vanilla JS, no build step)
|
|
67
|
+
│ ├── index.html
|
|
68
|
+
│ ├── styles.css
|
|
69
|
+
│ └── app.js
|
|
70
|
+
├── example/
|
|
71
|
+
│ └── app.py # Demo FastAPI app
|
|
72
|
+
├── run.py # Server launcher
|
|
73
|
+
└── pyproject.toml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## `sd()` reference
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
sd(data, label=None, level="info")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Default | Description |
|
|
85
|
+
|-----------|------|---------|-------------|
|
|
86
|
+
| `data` | any | — | Any Python value to inspect |
|
|
87
|
+
| `label` | str | `None` | Human-readable name shown in the UI |
|
|
88
|
+
| `level` | str | `"info"`| One of `info`, `debug`, `warning`, `error` |
|
|
89
|
+
|
|
90
|
+
**Returns** `data` unchanged, so you can inline the call:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
return sd(result, "final result")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Configuration
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from client import configure
|
|
100
|
+
|
|
101
|
+
configure(
|
|
102
|
+
server_url="http://localhost:8765", # default
|
|
103
|
+
timeout=1.0, # seconds — kept short
|
|
104
|
+
enabled=True, # set False in production
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## UI features
|
|
111
|
+
|
|
112
|
+
- Real-time WebSocket feed
|
|
113
|
+
- Collapsible JSON tree viewer (auto-collapses at depth 3)
|
|
114
|
+
- Level badges: `info` / `debug` / `warning` / `error`
|
|
115
|
+
- Filter by level and free-text search (label, file, value)
|
|
116
|
+
- Sort newest / oldest
|
|
117
|
+
- Copy-to-clipboard button
|
|
118
|
+
- File + line number + function name for every dump
|
|
119
|
+
- Dark mode by default
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Example routes (demo app)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Start server
|
|
127
|
+
python run.py
|
|
128
|
+
|
|
129
|
+
# Start demo app in another terminal
|
|
130
|
+
uvicorn example.app:app --port 8000 --reload
|
|
131
|
+
|
|
132
|
+
# Hit the routes and watch the UI
|
|
133
|
+
curl http://localhost:8000/
|
|
134
|
+
curl http://localhost:8000/users/42
|
|
135
|
+
curl http://localhost:8000/error
|
|
136
|
+
curl -X POST http://localhost:8000/items \
|
|
137
|
+
-H "Content-Type: application/json" \
|
|
138
|
+
-d '{"name": "widget", "price": 9.99}'
|
|
139
|
+
curl http://localhost:8000/products
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## How it works
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
FastAPI app SmartDebugger server Browser
|
|
148
|
+
│ │ │
|
|
149
|
+
│ sd(data) │ │
|
|
150
|
+
│─── POST /dump ────────>│ │
|
|
151
|
+
│ (daemon thread) │── WS broadcast ─────>│
|
|
152
|
+
│ │ │ (live update)
|
|
153
|
+
│ (request continues) │ │
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
1. `sd()` captures the caller's file/line/function via `inspect`.
|
|
157
|
+
2. It serialises the value and fires a POST request on a **daemon thread** — completely non-blocking.
|
|
158
|
+
3. The server stores the dump in a `deque(maxlen=200)` and broadcasts it to all connected WebSocket clients.
|
|
159
|
+
4. The browser receives the payload and renders it as a collapsible JSON tree.
|
|
160
|
+
|
|
161
|
+
If the server is not running, `sd()` silently does nothing — it never crashes your app.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Disabling in production
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import os
|
|
169
|
+
from client import configure
|
|
170
|
+
|
|
171
|
+
configure(enabled=os.getenv("DEBUG", "false").lower() == "true")
|
|
172
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
client/__init__.py,sha256=jsqLNPT8_cDaksmGdX3nwG8PKr0dyQEWEnLxFjf9YMs,61
|
|
2
|
+
client/sd.py,sha256=8Yg5WbmAqdRkucvd2R5VqXDELDVEdhfvQAVtq7IjCvI,4534
|
|
3
|
+
server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
server/app.py,sha256=LGBXxFKbo1y4P3POkwUOZ-1qCqUyL1bwUldKf5C5yOI,2718
|
|
5
|
+
server/manager.py,sha256=YezjnZ--MH08YTGntvje6bTZ4LSVNdUYVhdAFFBbtsA,1076
|
|
6
|
+
smartdump-0.1.0.dist-info/METADATA,sha256=8ly9gxijio4o3NK2GMJemwnl5jlpQ9pM3Z539rA6VY4,4511
|
|
7
|
+
smartdump-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
smartdump-0.1.0.dist-info/entry_points.txt,sha256=vAYw4ED5DbZWd7j6j1boT7Ka-CBvtN-N0B3sWW0TU8Q,65
|
|
9
|
+
smartdump-0.1.0.dist-info/top_level.txt,sha256=KwhQx6Np9abTSEPy-jGTR0d0L38g9ZW1Io1VGoS9ejE,14
|
|
10
|
+
smartdump-0.1.0.dist-info/RECORD,,
|