pyquerytracker 0.1.0__py3-none-any.whl → 0.1.1__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.
Files changed (40) hide show
  1. examples/async_example.py +63 -0
  2. examples/core/async_usage.py +53 -0
  3. examples/core/basic_usage.py +17 -0
  4. examples/core/error_handling.py +24 -0
  5. examples/core/quick_test.py +39 -0
  6. examples/core/with_arguments.py +27 -0
  7. examples/core/with_config.py +30 -0
  8. examples/exporter/csv_exporter_1.py +36 -0
  9. examples/exporter/json_exporter.py +36 -0
  10. examples/fastapi_app.py +119 -0
  11. examples/test_endpoints.py +73 -0
  12. pyquerytracker/__init__.py +3 -2
  13. pyquerytracker/api.py +72 -0
  14. pyquerytracker/config.py +26 -10
  15. pyquerytracker/core.py +122 -58
  16. pyquerytracker/db/models.py +20 -0
  17. pyquerytracker/db/session.py +8 -0
  18. pyquerytracker/db/writer.py +64 -0
  19. pyquerytracker/exporter/__init__.py +0 -0
  20. pyquerytracker/exporter/base.py +25 -0
  21. pyquerytracker/exporter/csv_exporter.py +52 -0
  22. pyquerytracker/exporter/json_exporter.py +47 -0
  23. pyquerytracker/exporter/manager.py +32 -0
  24. pyquerytracker/main.py +6 -0
  25. pyquerytracker/tracker.py +17 -0
  26. pyquerytracker/utils/logger.py +18 -0
  27. pyquerytracker/websocket.py +33 -0
  28. {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/METADATA +93 -12
  29. pyquerytracker-0.1.1.dist-info/RECORD +39 -0
  30. {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/WHEEL +1 -1
  31. {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/top_level.txt +2 -0
  32. tests/exporter/test_json_exporter.py +182 -0
  33. tests/test_async_core.py +93 -0
  34. tests/test_config.py +40 -0
  35. tests/test_core.py +72 -0
  36. tests/test_dashboard.py +31 -0
  37. tests/test_persist.py +9 -0
  38. tests/test_websocket.py +58 -0
  39. pyquerytracker-0.1.0.dist-info/RECORD +0 -8
  40. {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyquerytracker
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A lightweight, decorator-based query performance tracking library for Python applications. Monitor and analyze database query performance with ease.
5
5
  Author-email: MuddyHope <daktarisun@gmail.com>
6
6
  License: MIT
@@ -20,6 +20,16 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Dynamic: license-file
22
22
 
23
+
24
+ ![GitHub Release](https://img.shields.io/github/v/release/MuddyHope/pyquerytracker)
25
+ ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/MuddyHope/pyquerytracker)
26
+
27
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyquerytracker)
28
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/MuddyHope/pyquerytracker/.github%2Fworkflows%2Fpublish.yml)
29
+ ![GitHub forks](https://img.shields.io/github/forks/MuddyHope/pyquerytracker)
30
+
31
+ ![PyPI - License](https://img.shields.io/pypi/l/pyquerytracker)
32
+
23
33
  # 🐍 pyquerytracker
24
34
 
25
35
  **pyquerytracker** is a lightweight Python utility to **track and analyze database query performance** using simple decorators. It enables developers to gain visibility into SQL execution time, log metadata, and export insights in JSON format — with optional FastAPI integration and scheduled reporting.
@@ -46,8 +56,24 @@ Dynamic: license-file
46
56
  pip install pyquerytracker
47
57
  ```
48
58
 
49
- ## Usage
59
+ ## 🔧 Configuration
60
+
61
+ ```python
62
+ import logging
63
+ from pyquerytracker.config import configure
64
+
65
+ configure(
66
+ slow_log_threshold_ms=200, # Log queries slower than 200ms
67
+ slow_log_level=logging.DEBUG # Use DEBUG level for slow logs
68
+ )
69
+ ```
70
+
71
+ ---
72
+
73
+ ## ⚙️ Usage
74
+
50
75
  ### Basic Usage
76
+
51
77
  ```python
52
78
  import time
53
79
  from pyquerytracker import TrackQuery
@@ -59,26 +85,81 @@ def run_query():
59
85
 
60
86
  run_query()
61
87
  ```
62
- ### Output
63
- ```bash
88
+
89
+ **Output:**
90
+ ```
64
91
  2025-06-14 14:23:00,123 - pyquerytracker - INFO - Function run_query executed successfully in 305.12ms
65
92
  ```
66
93
 
67
- ### With Configure
94
+ ---
95
+
96
+ ### 🧩 Async Support
97
+
98
+ Use the same decorator with `async` functions or class methods:
99
+
100
+ ```python
101
+ import asyncio
102
+ from pyquerytracker import TrackQuery
103
+
104
+ @TrackQuery()
105
+ async def fetch_data():
106
+ await asyncio.sleep(0.2)
107
+ return "fetched"
108
+
109
+ class MyService:
110
+ @TrackQuery()
111
+ async def do_work(self, x, y):
112
+ await asyncio.sleep(0.1)
113
+ return x + y
114
+
115
+ asyncio.run(fetch_data())
68
116
  ```
69
- import logging
117
+
118
+ ---
119
+
120
+ ### 🌐 Run the FastAPI Server
121
+
122
+ To view tracked query logs via REST, WebSocket, or a Web-based dashboard, start the built-in FastAPI server:
123
+
124
+ ```bash
125
+ uvicorn pyquerytracker.api:app --reload
126
+ ```
127
+
128
+ - ⚠️ If your project or file structure is different, replace `pyquerytracker.api` with your own module path, like `<your_project_name>.<your_server(file)_name>`.
129
+
130
+ - Open docs at [http://localhost:8000/docs](http://localhost:8000/docs)
131
+ - **Query Dashboard UI:** [http://localhost:8000/dashboard](http://localhost:8000/dashboard)
132
+ - REST endpoint: `GET /queries`
133
+ - WebSocket stream: `ws://localhost:8000/ws`
134
+
135
+ Then run your tracked functions in another terminal or script:
136
+
137
+ ```python
138
+ @TrackQuery()
139
+ def insert_query():
140
+ time.sleep(0.4)
141
+ return "INSERT INTO users ..."
142
+ ```
143
+
144
+ You’ll see logs live on the server via API/WebSocket.
145
+
146
+ ---
147
+
148
+ ## 📤 Export Logs
149
+
150
+ Enable exporting to CSV or JSON by setting config:
151
+
152
+ ```python
70
153
  from pyquerytracker.config import configure
71
154
 
72
155
  configure(
73
- slow_log_threshold_ms=200, # Log queries slower than 200ms
74
- slow_log_level=logging.DEBUG # Use DEBUG level for slow logs
156
+ export_type="json",
157
+ export_path="./query_logs.json"
75
158
  )
76
159
  ```
77
160
 
78
- ### Output
79
- ```bash
80
- 2025-06-14 14:24:45,456 - pyquerytracker - WARNING - Slow execution: run_query took 501.87ms
81
- ```
161
+ ---
82
162
 
163
+ Let us know how you’re using `pyquerytracker` and feel free to contribute!
83
164
 
84
165
 
@@ -0,0 +1,39 @@
1
+ examples/async_example.py,sha256=c22yunbe7XW4Lqo9NQGJizNty69ieRtTrAo5179446Y,1476
2
+ examples/fastapi_app.py,sha256=bNYkkgzUCWFV_cJ7lfX-sQBhXXdUUeOd-bxum2KwggE,3334
3
+ examples/test_endpoints.py,sha256=wFea-BE82osBeUvY9zn7zD1PyQhAaaQ4es9AaefnhMQ,2402
4
+ examples/core/async_usage.py,sha256=-JDyXoaglwnipQLwSx_qrc5pE3KGWa7rf-bBurP8a_4,1432
5
+ examples/core/basic_usage.py,sha256=ux-FC8XJFS7KUK0Prci0a2BwGnS4v0_gcGP077e-aeE,393
6
+ examples/core/error_handling.py,sha256=V0ewoMJZOSPQR4NRzhSZv7OAEnjqLjef7QkNRaLvVMQ,527
7
+ examples/core/quick_test.py,sha256=a7lTkWj1ZJD75pH-Ae66nxvzUcKqOjKBi7mVRuJRzcU,850
8
+ examples/core/with_arguments.py,sha256=BWm4kuh7kpzKSb4pGOsx8bU2ZUPtR8UJLaDYQE_t5cM,759
9
+ examples/core/with_config.py,sha256=rqkhelnRnDs6h7KHekP62MSjnjU4lQJz6lKOx3ZIX_A,677
10
+ examples/exporter/csv_exporter_1.py,sha256=g0kB55sA0-8q66491ZD3K1QxB-doktqQAwDaKDVzbcA,648
11
+ examples/exporter/json_exporter.py,sha256=K-ZHdiLgODft6jZmE3nYTesEh_Z_OOOVQmc4WV_0KqY,674
12
+ pyquerytracker/__init__.py,sha256=8vDDWF9EBGmIyijtD4n9PPxVVhXkoUtIWKFAw5WbWXM,126
13
+ pyquerytracker/api.py,sha256=ILxHFRo9A-2rDzWKBK15_Z0wBQeq2JHvvJ8Ky0AJX-w,2079
14
+ pyquerytracker/config.py,sha256=o_MV1pGfb3HGQLNSfyISQ8YX0jDCymiCMQEHGkHVf2A,2268
15
+ pyquerytracker/core.py,sha256=8300ITVvMbjY8OsdqApc69zJaudeOp0YHWeq4f9iR_s,6169
16
+ pyquerytracker/main.py,sha256=EUbMAeokP1nsY-sXhG_kWtGzMLvUcJhLD7N8uJVRlhQ,137
17
+ pyquerytracker/tracker.py,sha256=2RIfq1CNMzJ7XiMBqcMcFgHIwgTXBfuXU7nfwDpNOgo,605
18
+ pyquerytracker/websocket.py,sha256=zPKjqrz1OQniMW9RQTnQHmg9yROE3vfWz23KCbBLd9g,908
19
+ pyquerytracker/db/models.py,sha256=R0o0uHQp0QZl886vi5wJix5I67jCuQq1mhzyKTj4ypQ,644
20
+ pyquerytracker/db/session.py,sha256=uuqrlNGdvTRkYnfEXNGB7bWKUsFDX8KnaI6LgCAW-0w,278
21
+ pyquerytracker/db/writer.py,sha256=h2peKrlEB3bTuzs6NdJe6fyMVZohMRussSzPXPPdlNI,2157
22
+ pyquerytracker/exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ pyquerytracker/exporter/base.py,sha256=jRrQT29ZLDAFuyv3YnpmLD_dO7HuFeRQZ53eVDVmtfk,412
24
+ pyquerytracker/exporter/csv_exporter.py,sha256=2XWZs9VKcWxYZ_NTd8iiVhWOFW3FfQwysZRHixZsMZE,1648
25
+ pyquerytracker/exporter/json_exporter.py,sha256=YJGqHJpwi-yA_Cxjj1GIMytm3WW34ngjoH0j3ZTBsOE,1422
26
+ pyquerytracker/exporter/manager.py,sha256=owg6mlvGa6qjR92VayOIO2lUVGBUukYm54Mu3gOzB18,1016
27
+ pyquerytracker/utils/logger.py,sha256=cgBQG8yvwGA1SUsbd2Ef7zBieVEVWxRx_7xIVWrhMsc,506
28
+ pyquerytracker-0.1.1.dist-info/licenses/LICENSE,sha256=lXcEFZRxovixBqp9SYJRLrN5OpP6AMggc_v7eaAMWn4,1065
29
+ tests/test_async_core.py,sha256=mwTvPdUHPVmdQMd_mB1BpIF-PTgcg3Hs3AAofY03sts,2716
30
+ tests/test_config.py,sha256=XKi8GM5P2lj0Xm10pPSwMYnHRnYhvpkmTyeKGFyaiOM,945
31
+ tests/test_core.py,sha256=SFooHkpvYUWipuon0IIwVUChVhRwUaXyBH78humupkM,1688
32
+ tests/test_dashboard.py,sha256=OsUP7_4mBF6u0HgAgzL5Vkf1qO4Vm7Np9z4uv8zSGzc,724
33
+ tests/test_persist.py,sha256=E7MFfP4LAXATRojpGoNgK8HbyszdILNWB47c-9SsBis,123
34
+ tests/test_websocket.py,sha256=Akfd9EH9XhBMMGb-B5Jg_aaC1OAHeVFiMIKlFjyLaS0,1470
35
+ tests/exporter/test_json_exporter.py,sha256=m6mDQTjdmiag9AulmP2mzDl78xwgqXZ1hMm6HQBkKg4,4553
36
+ pyquerytracker-0.1.1.dist-info/METADATA,sha256=F5uwJ3ZyysfGbOQzg8xmDyw-ejkrhjJNwzNc4M_iwmw,4519
37
+ pyquerytracker-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
38
+ pyquerytracker-0.1.1.dist-info/top_level.txt,sha256=gSZtdyA6IEnK-AIdMq8dMYY4AUNhnLm6jOmUY8pFZrI,30
39
+ pyquerytracker-0.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1 +1,3 @@
1
+ examples
1
2
  pyquerytracker
3
+ tests
@@ -0,0 +1,182 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import tempfile
5
+
6
+
7
+ def run_test_in_subprocess(script: str, export_path: str):
8
+ print("\n----- Running Script -----\n")
9
+ print(script)
10
+ print("\n--------------------------\n")
11
+
12
+ try:
13
+ subprocess.run(
14
+ ["python3", "-c", script], check=True, capture_output=True, text=True
15
+ )
16
+ except subprocess.CalledProcessError as e:
17
+ print("STDOUT:\n", e.stdout)
18
+ print("STDERR:\n", e.stderr)
19
+ raise
20
+
21
+ assert os.path.exists(export_path)
22
+ with open(export_path) as f:
23
+ return json.load(f)
24
+
25
+
26
+ def test_successful_function_logs():
27
+ with tempfile.TemporaryDirectory() as tmpdir:
28
+ export_path = os.path.join(tmpdir, "log.json")
29
+
30
+ script = f"""
31
+ import time
32
+ from pyquerytracker import TrackQuery
33
+ from pyquerytracker.config import configure, ExportType
34
+
35
+ configure(
36
+ slow_log_threshold_ms=0.0,
37
+ slow_log_level=10,
38
+ export_type=ExportType.JSON,
39
+ export_path=r"{export_path}",
40
+ )
41
+
42
+ @TrackQuery()
43
+ def foo(x, y):
44
+ return x + y
45
+
46
+ foo(1, 2)
47
+ from pyquerytracker.exporter.manager import ExporterManager
48
+ ExporterManager.get().flush()
49
+ """
50
+
51
+ logs = run_test_in_subprocess(script, export_path)
52
+
53
+ assert isinstance(logs, list)
54
+ functions_logged = {log.get("function_name") for log in logs}
55
+ assert "foo" in functions_logged
56
+ assert any(log.get("event") == "success" for log in logs) or any(
57
+ log.get("event") == "slow_execution" for log in logs
58
+ )
59
+
60
+
61
+ def test_error_function_logs():
62
+ with tempfile.TemporaryDirectory() as tmpdir:
63
+ export_path = os.path.join(tmpdir, "log.json")
64
+
65
+ script = f"""
66
+ import time
67
+ from pyquerytracker import TrackQuery
68
+ from pyquerytracker.config import configure, ExportType
69
+
70
+ configure(
71
+ slow_log_threshold_ms=0.0,
72
+ slow_log_level=10,
73
+ export_type=ExportType.JSON,
74
+ export_path=r"{export_path}",
75
+ )
76
+
77
+ @TrackQuery()
78
+ def bar():
79
+ raise RuntimeError("error")
80
+
81
+ try:
82
+ bar()
83
+ except RuntimeError:
84
+ pass
85
+
86
+ from pyquerytracker.exporter.manager import ExporterManager
87
+ ExporterManager.get().flush()
88
+
89
+ """
90
+
91
+ logs = run_test_in_subprocess(script, export_path)
92
+
93
+ assert isinstance(logs, list)
94
+ functions_logged = {log.get("function_name") for log in logs}
95
+ assert "bar" in functions_logged
96
+ assert any(log.get("event") == "error" for log in logs)
97
+
98
+
99
+ def test_slow_function_logs():
100
+ with tempfile.TemporaryDirectory() as tmpdir:
101
+ export_path = os.path.join(tmpdir, "log.json")
102
+
103
+ script = f"""
104
+ import time
105
+ from pyquerytracker import TrackQuery
106
+ from pyquerytracker.config import configure, ExportType
107
+
108
+ # Set threshold low so the sleep triggers slow log
109
+ configure(
110
+ slow_log_threshold_ms=5.0,
111
+ slow_log_level=10,
112
+ export_type=ExportType.JSON,
113
+ export_path=r"{export_path}",
114
+ )
115
+
116
+ @TrackQuery()
117
+ def slow_func():
118
+ time.sleep(0.01)
119
+ return "done"
120
+
121
+ slow_func()
122
+
123
+ from pyquerytracker.exporter.manager import ExporterManager
124
+ ExporterManager.get().flush()
125
+
126
+ """
127
+
128
+ logs = run_test_in_subprocess(script, export_path)
129
+
130
+ assert isinstance(logs, list)
131
+ functions_logged = {log.get("function_name") for log in logs}
132
+ assert "slow_func" in functions_logged
133
+ assert any(log.get("event") == "slow_execution" for log in logs)
134
+
135
+
136
+ def test_multiple_calls_logged():
137
+ with tempfile.TemporaryDirectory() as tmpdir:
138
+ export_path = os.path.join(tmpdir, "log.json")
139
+
140
+ script = f"""
141
+ import time
142
+ from pyquerytracker import TrackQuery
143
+ from pyquerytracker.config import configure, ExportType
144
+
145
+ configure(
146
+ slow_log_threshold_ms=0.0,
147
+ slow_log_level=10,
148
+ export_type=ExportType.JSON,
149
+ export_path=r"{export_path}",
150
+ )
151
+
152
+ @TrackQuery()
153
+ def a():
154
+ return 1
155
+
156
+ @TrackQuery()
157
+ def b():
158
+ raise ValueError("fail")
159
+
160
+ a()
161
+ try:
162
+ b()
163
+ except Exception:
164
+ pass
165
+ a()
166
+
167
+ from pyquerytracker.exporter.manager import ExporterManager
168
+ ExporterManager.get().flush()
169
+
170
+ """
171
+
172
+ logs = run_test_in_subprocess(script, export_path)
173
+
174
+ assert isinstance(logs, list)
175
+ functions_logged = [log.get("function_name") for log in logs]
176
+
177
+ # Because of overwrite behavior, logs might only contain last flush,
178
+ # but often logs include multiple entries. So test at least one a, b occurrence:
179
+ assert "a" in functions_logged or "b" in functions_logged
180
+ # Check for presence of success and error events in the batch
181
+ events = {log.get("event") for log in logs}
182
+ assert "error" in events or "success" in events or "slow_execution" in events
@@ -0,0 +1,93 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ import pytest
5
+
6
+ from pyquerytracker import TrackQuery, configure
7
+
8
+ # Mark all tests in this file as asyncio
9
+ pytestmark = pytest.mark.asyncio
10
+
11
+
12
+ async def test_async_tracking_output(caplog):
13
+ caplog.set_level("INFO")
14
+
15
+ @TrackQuery()
16
+ async def fake_async_db_query():
17
+ await asyncio.sleep(0.01)
18
+ return "done"
19
+
20
+ result = await fake_async_db_query()
21
+ assert result == "done"
22
+
23
+ # Check the log records
24
+ assert len(caplog.records) == 1
25
+ record = caplog.records[0]
26
+ assert record.levelname == "INFO"
27
+ assert "Function fake_async_db_query executed successfully" in record.message
28
+ assert "ms" in record.message
29
+
30
+
31
+ async def test_async_tracking_output_with_error(caplog):
32
+ configure(slow_log_threshold_ms=10, slow_log_level=40)
33
+ caplog.set_level("ERROR")
34
+
35
+ @TrackQuery()
36
+ async def failing_async_query():
37
+ await asyncio.sleep(0.01)
38
+ raise ValueError("Async Test error")
39
+
40
+ result = await failing_async_query()
41
+ assert result is None
42
+
43
+ # Check the log records
44
+ assert len(caplog.records) == 1
45
+ record = caplog.records[0]
46
+ assert record.levelname == "ERROR"
47
+ assert "Function failing_async_query failed" in record.message
48
+ assert "Async Test error" in record.message
49
+ assert "ms" in record.message
50
+
51
+
52
+ async def test_async_tracking_with_class(caplog):
53
+ configure(slow_log_threshold_ms=1000, slow_log_level=logging.INFO)
54
+ caplog.set_level("INFO")
55
+
56
+ class MyAsyncClass:
57
+ @TrackQuery()
58
+ async def do_work(self, a, b):
59
+ await asyncio.sleep(0.01)
60
+ return a + b
61
+
62
+ result = await MyAsyncClass().do_work(5, 10)
63
+ assert result == 15
64
+ assert len(caplog.records) == 1
65
+ record = caplog.records[0]
66
+ assert record.levelname == "INFO"
67
+ assert "MyAsyncClass" in record.message
68
+ assert "do_work" in record.message
69
+ assert "ms" in record.message
70
+
71
+
72
+ async def test_async_configure_slow_log(caplog):
73
+ configure(slow_log_threshold_ms=10, slow_log_level=40)
74
+ caplog.set_level("ERROR", logger="pyquerytracker")
75
+
76
+ @TrackQuery()
77
+ async def do_slow_async_work():
78
+ await asyncio.sleep(0.1)
79
+ return "slow"
80
+
81
+ try:
82
+ result = await do_slow_async_work()
83
+ assert result == "slow"
84
+ assert len(caplog.records) == 1
85
+ record = caplog.records[0]
86
+ assert record.levelname == "ERROR" # Configured to ERROR for slow logs
87
+ assert "do_slow_async_work" in record.message
88
+ assert "Slow execution" in record.message
89
+ assert "ms" in record.message
90
+
91
+ finally:
92
+ # Reset config to avoid affecting other tests
93
+ configure(slow_log_threshold_ms=100.0, slow_log_level=logging.WARNING)
tests/test_config.py ADDED
@@ -0,0 +1,40 @@
1
+ import logging
2
+ import time
3
+
4
+ from pyquerytracker.core import TrackQuery, logger
5
+
6
+
7
+ def configure(slow_log_threshold_ms=100, slow_log_level=logging.INFO):
8
+ logger.setLevel(slow_log_level)
9
+
10
+
11
+ def test_configure_basic(caplog):
12
+ configure(slow_log_threshold_ms=250)
13
+
14
+ class MyClass:
15
+ @TrackQuery()
16
+ def do_work(self, a, b):
17
+ time.sleep(0.5)
18
+ return a * b
19
+
20
+ MyClass().do_work(2, 3)
21
+ assert len(caplog.records) == 1
22
+
23
+
24
+ def test_configure_basic_with_loglevel(caplog):
25
+ caplog.set_level("ERROR", logger="pyquerytracker")
26
+
27
+ configure(slow_log_threshold_ms=100, slow_log_level=logging.ERROR)
28
+
29
+ class MyClass:
30
+ def do_slow_work(self, a, b):
31
+ import time
32
+
33
+ time.sleep(0.2)
34
+ return a * b
35
+
36
+ # Apply TrackQuery to the unbound method
37
+ MyClass.do_slow_work = TrackQuery()(MyClass.do_slow_work)
38
+
39
+ result = MyClass().do_slow_work(2, 3)
40
+ assert result == 6
tests/test_core.py ADDED
@@ -0,0 +1,72 @@
1
+ import logging
2
+ import time
3
+
4
+ from pyquerytracker import TrackQuery
5
+
6
+
7
+ def test_tracking_output():
8
+ @TrackQuery()
9
+ def fake_db_query():
10
+ return "done"
11
+
12
+ assert fake_db_query() == "done"
13
+
14
+
15
+ logging.basicConfig(
16
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
17
+ )
18
+
19
+
20
+ def test_tracking_output_with_logging(caplog):
21
+ caplog.set_level("INFO")
22
+
23
+ @TrackQuery()
24
+ def fake_db_query():
25
+ return "done"
26
+
27
+ result = fake_db_query()
28
+ assert result == "done"
29
+
30
+ # Check the log records
31
+ assert len(caplog.records) == 1
32
+ record = caplog.records[0]
33
+ assert record.levelname == "INFO"
34
+ assert "Function fake_db_query executed successfully" in record.message
35
+ assert "ms" in record.message
36
+
37
+
38
+ def test_tracking_output_with_error(caplog):
39
+ caplog.set_level("ERROR")
40
+
41
+ @TrackQuery()
42
+ def failing_query():
43
+ raise ValueError("Test error")
44
+
45
+ result = failing_query()
46
+ assert result is None
47
+
48
+ # Check the log records
49
+ assert len(caplog.records) == 1
50
+ record = caplog.records[0]
51
+ assert record.levelname == "ERROR"
52
+ assert "Function failing_query failed" in record.message
53
+ assert "Test error" in record.message
54
+ assert "ms" in record.message
55
+
56
+
57
+ def test_tracking_with_class(caplog):
58
+ caplog.set_level("INFO")
59
+
60
+ class MyClass:
61
+ @TrackQuery()
62
+ def do_work(self, a, b):
63
+ time.sleep(0.09)
64
+ return a * b
65
+
66
+ MyClass().do_work(2, 3)
67
+ assert len(caplog.records) == 1
68
+ record = caplog.records[0]
69
+ assert record.levelname == "INFO"
70
+ assert "MyClass" in record.message
71
+ assert "do_work" in record.message
72
+ assert "ms" in record.message
@@ -0,0 +1,31 @@
1
+ from fastapi.testclient import TestClient
2
+
3
+ from pyquerytracker.api import app
4
+ from pyquerytracker.core import TrackQuery
5
+
6
+ client = TestClient(app)
7
+
8
+
9
+ # Simulate query activity
10
+ @TrackQuery()
11
+ def sample_query():
12
+ return "ok"
13
+
14
+
15
+ def test_query_stats_endpoint():
16
+ # Trigger a few logs
17
+ for _ in range(3):
18
+ sample_query()
19
+
20
+ # Call the stats endpoint
21
+ response = client.get("/api/query-stats?minutes=5")
22
+ assert response.status_code == 200
23
+
24
+ json = response.json()
25
+ assert "labels" in json
26
+ assert "durations" in json
27
+ assert isinstance(json["labels"], list)
28
+ assert isinstance(json["durations"], list)
29
+
30
+ # Optional: Print results for debug
31
+ print("📊 Dashboard API response:", json)
tests/test_persist.py ADDED
@@ -0,0 +1,9 @@
1
+ from pyquerytracker import TrackQuery
2
+
3
+
4
+ @TrackQuery()
5
+ def sample_query():
6
+ return "DB test successful"
7
+
8
+
9
+ sample_query()
@@ -0,0 +1,58 @@
1
+ import asyncio
2
+
3
+ from starlette.testclient import TestClient
4
+
5
+ from pyquerytracker.api import app
6
+ from pyquerytracker.websocket import (broadcast, connected_clients,
7
+ websocket_endpoint)
8
+
9
+
10
+ def test_websocket_connection():
11
+ client = TestClient(app)
12
+ with client.websocket_connect("/ws") as websocket:
13
+ websocket.send_text("ping") # No response expected, just test it works
14
+
15
+
16
+ def test_broadcast_message_format():
17
+ class FakeWebSocket:
18
+ def __init__(self):
19
+ self.sent = []
20
+
21
+ async def send_text(self, msg):
22
+ self.sent.append(msg)
23
+
24
+ fake_ws = FakeWebSocket()
25
+ connected_clients.append(fake_ws)
26
+
27
+ asyncio.run(broadcast("hello"))
28
+ assert fake_ws.sent == ["hello"]
29
+
30
+ connected_clients.remove(fake_ws)
31
+
32
+
33
+ def test_connection_lifecycle():
34
+ class DummyWebSocket:
35
+ def __init__(self):
36
+ self.accepted = False
37
+
38
+ async def accept(self):
39
+ self.accepted = True
40
+
41
+ async def receive_text(self):
42
+ raise Exception("Simulated disconnect")
43
+
44
+ ws = DummyWebSocket()
45
+ connected_clients.clear()
46
+ try:
47
+ asyncio.run(websocket_endpoint(ws))
48
+ except Exception:
49
+ pass
50
+ assert ws not in connected_clients
51
+
52
+
53
+ def test_broadcast_no_clients():
54
+ connected_clients.clear()
55
+ try:
56
+ asyncio.run(broadcast("no one here"))
57
+ except Exception:
58
+ assert False, "Broadcast failed when no clients connected"
@@ -1,8 +0,0 @@
1
- pyquerytracker/__init__.py,sha256=pYyF2RPTTMhA0SakZTZ_dJ5dbPOIPYlacXJg7dSfsdc,98
2
- pyquerytracker/config.py,sha256=GuMaHx4RB77FFad4bzZTQ-EyQYkDYHRrELHD_bTjAZo,1907
3
- pyquerytracker/core.py,sha256=eLMfYs41ajLRjoxtxKjyi9G3NPN7tpwYkNerccL2oM4,3583
4
- pyquerytracker-0.1.0.dist-info/licenses/LICENSE,sha256=lXcEFZRxovixBqp9SYJRLrN5OpP6AMggc_v7eaAMWn4,1065
5
- pyquerytracker-0.1.0.dist-info/METADATA,sha256=RlPCS75bqssay-luqk6PuGlz_sb6NKAEKlcixWokI8E,2540
6
- pyquerytracker-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- pyquerytracker-0.1.0.dist-info/top_level.txt,sha256=E5kxHgMifJIw-S2IjZqA1XuAU06Vbhd7geNfRZgjZuQ,15
8
- pyquerytracker-0.1.0.dist-info/RECORD,,