lsp-devtools 0.2.2__tar.gz → 0.2.3__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 (40) hide show
  1. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/CHANGES.md +13 -0
  2. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/PKG-INFO +3 -3
  3. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/hatch.toml +7 -0
  4. lsp_devtools-0.2.3/lsp_devtools/__init__.py +1 -0
  5. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/agent/__init__.py +12 -41
  6. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/agent/agent.py +95 -17
  7. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/agent/client.py +25 -2
  8. lsp_devtools-0.2.3/lsp_devtools/agent/protocol.py +7 -0
  9. lsp_devtools-0.2.3/lsp_devtools/agent/server.py +85 -0
  10. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/__init__.py +4 -2
  11. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/lsp.py +17 -2
  12. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/database.py +13 -12
  13. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/handlers/__init__.py +17 -16
  14. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/handlers/sql.py +2 -2
  15. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/inspector/__init__.py +15 -56
  16. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/__init__.py +42 -26
  17. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/filters.py +2 -2
  18. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/formatters.py +2 -1
  19. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/record/visualize.py +1 -1
  20. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/pyproject.toml +1 -5
  21. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/record/test_filters.py +11 -11
  22. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/record/test_formatters.py +1 -1
  23. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/record/test_record.py +2 -2
  24. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/servers/simple.py +1 -0
  25. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/tests/test_agent.py +9 -4
  26. lsp_devtools-0.2.2/lsp_devtools/__init__.py +0 -1
  27. lsp_devtools-0.2.2/lsp_devtools/agent/protocol.py +0 -47
  28. lsp_devtools-0.2.2/lsp_devtools/agent/server.py +0 -108
  29. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/.gitignore +0 -0
  30. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/LICENSE +0 -0
  31. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/README.md +0 -0
  32. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/__main__.py +0 -0
  33. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/cli.py +0 -0
  34. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/app.css +0 -0
  35. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/editor/__init__.py +0 -0
  36. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/editor/completion.py +0 -0
  37. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/client/editor/text_editor.py +0 -0
  38. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/handlers/dbinit.sql +0 -0
  39. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/inspector/app.css +0 -0
  40. {lsp_devtools-0.2.2 → lsp_devtools-0.2.3}/lsp_devtools/py.typed +0 -0
@@ -1,3 +1,16 @@
1
+ ## v0.2.3 - 2024-05-22
2
+
3
+
4
+ ### Enhancements
5
+
6
+ - The `lsp-devtools agent` now forwards the server's `stderr` channel ([#165](https://github.com/swyddfa/lsp-devtools/issues/165))
7
+
8
+ ### Fixes
9
+
10
+ - All `lsp-devtools` commands should no longer crash when encountering messages containing unicode characters ([#157](https://github.com/swyddfa/lsp-devtools/issues/157))
11
+ - Commands like `lsp-devtools record` should now continue to function after encountering an error ([#158](https://github.com/swyddfa/lsp-devtools/issues/158))
12
+
13
+
1
14
  ## v0.2.2 - 2024-01-29
2
15
 
3
16
 
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: lsp-devtools
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Developer tooling for language servers
5
5
  Project-URL: Bug Tracker, https://github.com/swyddfa/lsp-devtools/issues
6
- Project-URL: Documentation, https://swyddfa.github.io/lsp-devtools/
6
+ Project-URL: Documentation, https://lsp-devtools.readthedocs.io/en/latest/
7
7
  Project-URL: Source Code, https://github.com/swyddfa/lsp-devtools
8
8
  Author-email: Alex Carney <alcarneyme@gmail.com>
9
9
  License: MIT
@@ -7,3 +7,10 @@ include = ["lsp_devtools", "tests", "CHANGES.md"]
7
7
 
8
8
  [build.targets.wheel]
9
9
  packages = ["lsp_devtools"]
10
+
11
+ [envs.hatch-test]
12
+ extra-dependencies = ["pytest-asyncio"]
13
+
14
+ [envs.hatch-static-analysis]
15
+ config-path = "ruff_defaults.toml"
16
+ dependencies = ["ruff==0.4.4"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -1,57 +1,34 @@
1
1
  import argparse
2
2
  import asyncio
3
- import logging
4
3
  import subprocess
5
4
  import sys
6
5
  from typing import List
7
- from uuid import uuid4
8
6
 
9
7
  from .agent import Agent
8
+ from .agent import RPCMessage
10
9
  from .agent import logger
10
+ from .agent import parse_rpc_message
11
11
  from .client import AgentClient
12
- from .protocol import MESSAGE_TEXT_NOTIFICATION
13
- from .protocol import MessageText
14
12
  from .server import AgentServer
15
- from .server import parse_rpc_message
16
13
 
17
14
  __all__ = [
18
15
  "Agent",
19
16
  "AgentClient",
20
17
  "AgentServer",
18
+ "RPCMessage",
21
19
  "logger",
22
- "MESSAGE_TEXT_NOTIFICATION",
23
- "MessageText",
24
20
  "parse_rpc_message",
25
21
  ]
26
22
 
27
23
 
28
- class MessageHandler(logging.Handler):
29
- """Logging handler that forwards captured JSON-RPC messages through to the
30
- ``AgentServer`` instance."""
24
+ async def forward_stderr(server: asyncio.subprocess.Process):
25
+ """Forward the server's stderr to the agent's stderr."""
26
+ if server.stderr is None:
27
+ return
31
28
 
32
- def __init__(self, client: AgentClient, *args, **kwargs):
33
- super().__init__(*args, **kwargs)
34
- self.client = client
35
- self.session = str(uuid4())
36
- self._buffer: List[MessageText] = []
37
-
38
- def emit(self, record: logging.LogRecord):
39
- message = MessageText(
40
- text=record.args[0], # type: ignore
41
- session=self.session,
42
- timestamp=record.created,
43
- source=record.__dict__["source"],
44
- )
45
-
46
- if not self.client.connected:
47
- self._buffer.append(message)
48
- return
49
-
50
- # Send any buffered messages
51
- while len(self._buffer) > 0:
52
- self.client.protocol.message_text_notification(self._buffer.pop(0))
53
-
54
- self.client.protocol.message_text_notification(message)
29
+ # EOF is signalled with an empty bytestring
30
+ while (line := await server.stderr.readline()) != b"":
31
+ sys.stderr.buffer.write(line)
55
32
 
56
33
 
57
34
  async def main(args, extra: List[str]):
@@ -60,7 +37,6 @@ async def main(args, extra: List[str]):
60
37
  return 1
61
38
 
62
39
  command, *arguments = extra
63
-
64
40
  server = await asyncio.create_subprocess_exec(
65
41
  command,
66
42
  *arguments,
@@ -68,18 +44,13 @@ async def main(args, extra: List[str]):
68
44
  stdout=subprocess.PIPE,
69
45
  stderr=subprocess.PIPE,
70
46
  )
71
- agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer)
72
-
73
47
  client = AgentClient()
74
- handler = MessageHandler(client)
75
- handler.setLevel(logging.INFO)
76
-
77
- logger.setLevel(logging.INFO)
78
- logger.addHandler(handler)
48
+ agent = Agent(server, sys.stdin.buffer, sys.stdout.buffer, client.forward_message)
79
49
 
80
50
  await asyncio.gather(
81
51
  client.start_tcp(args.host, args.port),
82
52
  agent.start(),
53
+ forward_stderr(server),
83
54
  )
84
55
 
85
56
 
@@ -2,35 +2,83 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import inspect
5
+ import json
5
6
  import logging
6
7
  import re
7
8
  import sys
8
9
  import typing
10
+ from datetime import datetime
11
+ from datetime import timezone
9
12
  from functools import partial
13
+ from uuid import uuid4
14
+
15
+ import attrs
10
16
 
11
17
  if typing.TYPE_CHECKING:
18
+ from typing import Any
12
19
  from typing import BinaryIO
20
+ from typing import Callable
21
+ from typing import Coroutine
22
+ from typing import Dict
13
23
  from typing import Optional
14
24
  from typing import Set
15
25
  from typing import Tuple
26
+ from typing import Union
27
+
28
+ MessageHandler = Callable[[bytes], Union[None, Coroutine[Any, Any, None]]]
16
29
 
30
+ UTC = timezone.utc
17
31
  logger = logging.getLogger("lsp_devtools.agent")
18
32
 
19
33
 
20
- async def forward_message(source: str, dest: asyncio.StreamWriter, message: bytes):
21
- """Forward the given message to the destination channel"""
22
- dest.write(message)
23
- await dest.drain()
34
+ @attrs.define
35
+ class RPCMessage:
36
+ """A Json-RPC message."""
24
37
 
25
- # Log the full message
26
- logger.info(
27
- "%s",
28
- message.decode("utf8"),
29
- extra={"source": source},
30
- )
38
+ headers: Dict[str, str]
39
+
40
+ body: Dict[str, Any]
41
+
42
+ def __getitem__(self, key: str):
43
+ return self.headers[key]
44
+
45
+
46
+ def parse_rpc_message(data: bytes) -> RPCMessage:
47
+ """Parse a JSON-RPC message from the given set of bytes."""
48
+
49
+ headers: Dict[str, str] = {}
50
+ body: Optional[Dict[str, Any]] = None
51
+ headers_complete = False
52
+
53
+ for line in data.split(b"\r\n"):
54
+ if line == b"":
55
+ if "Content-Length" not in headers:
56
+ raise ValueError("Missing 'Content-Length' header")
31
57
 
58
+ headers_complete = True
59
+ continue
32
60
 
33
- async def aio_readline(reader: asyncio.StreamReader, message_handler):
61
+ if headers_complete:
62
+ length = int(headers["Content-Length"])
63
+ if len(line) != length:
64
+ raise ValueError("Incorrect 'Content-Length'")
65
+
66
+ body = json.loads(line)
67
+ continue
68
+
69
+ if (idx := line.find(b":")) < 0:
70
+ raise ValueError(f"Invalid header: {line!r}")
71
+
72
+ name, value = line[:idx], line[idx + 1 :]
73
+ headers[name.decode("utf8").strip()] = value.decode("utf8").strip()
74
+
75
+ if body is None:
76
+ raise ValueError("Missing message body")
77
+
78
+ return RPCMessage(headers, body)
79
+
80
+
81
+ async def aio_readline(reader: asyncio.StreamReader, message_handler: MessageHandler):
34
82
  CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
35
83
 
36
84
  # Initialize message buffer
@@ -90,11 +138,17 @@ class Agent:
90
138
  enabling them to be recorded."""
91
139
 
92
140
  def __init__(
93
- self, server: asyncio.subprocess.Process, stdin: BinaryIO, stdout: BinaryIO
141
+ self,
142
+ server: asyncio.subprocess.Process,
143
+ stdin: BinaryIO,
144
+ stdout: BinaryIO,
145
+ handler: MessageHandler,
94
146
  ):
95
147
  self.stdin = stdin
96
148
  self.stdout = stdout
97
149
  self.server = server
150
+ self.handler = handler
151
+ self.session_id = str(uuid4())
98
152
 
99
153
  self._tasks: Set[asyncio.Task] = set()
100
154
  self.reader: Optional[asyncio.StreamReader] = None
@@ -105,14 +159,14 @@ class Agent:
105
159
  self.reader, self.writer = await get_streams(self.stdin, self.stdout)
106
160
 
107
161
  # Keep mypy happy
108
- assert self.server.stdin
109
- assert self.server.stdout
162
+ if self.server.stdin is None or self.server.stdout is None:
163
+ raise RuntimeError("Unable to find server I/O streams")
110
164
 
111
165
  # Connect stdin to the subprocess' stdin
112
166
  client_to_server = asyncio.create_task(
113
167
  aio_readline(
114
168
  self.reader,
115
- partial(forward_message, "client", self.server.stdin),
169
+ partial(self.forward_message, "client", self.server.stdin),
116
170
  ),
117
171
  )
118
172
  self._tasks.add(client_to_server)
@@ -121,7 +175,7 @@ class Agent:
121
175
  server_to_client = asyncio.create_task(
122
176
  aio_readline(
123
177
  self.server.stdout,
124
- partial(forward_message, "server", self.writer),
178
+ partial(self.forward_message, "server", self.writer),
125
179
  ),
126
180
  )
127
181
  self._tasks.add(server_to_client)
@@ -133,6 +187,30 @@ class Agent:
133
187
  self._watch_server_process(),
134
188
  )
135
189
 
190
+ async def forward_message(
191
+ self, source: str, dest: asyncio.StreamWriter, message: bytes
192
+ ):
193
+ """Forward the given message to the destination channel"""
194
+
195
+ # Forward the message as-is to the client/server
196
+ dest.write(message)
197
+ await dest.drain()
198
+
199
+ # Include some additional metadata before passing it onto the devtool.
200
+ # TODO: How do we make sure we choose the same encoding as `message`?
201
+ now = datetime.now(tz=UTC).isoformat()
202
+ fields = [
203
+ f"Message-Source: {source}\r\n".encode(),
204
+ f"Message-Session: {self.session_id}\r\n".encode(),
205
+ f"Message-Timestamp: {now}\r\n".encode(),
206
+ message,
207
+ ]
208
+
209
+ if inspect.iscoroutine(res := self.handler(b"".join(fields))):
210
+ task = asyncio.create_task(res)
211
+ self._tasks.add(task)
212
+ task.add_done_callback(self._tasks.discard)
213
+
136
214
  async def _watch_server_process(self):
137
215
  """Once the server process exits, ensure that the agent is also shutdown."""
138
216
  ret = await self.server.wait()
@@ -149,7 +227,7 @@ class Agent:
149
227
  self.server.kill()
150
228
 
151
229
  args = {}
152
- if sys.version_info.minor > 8:
230
+ if sys.version_info >= (3, 9):
153
231
  args["msg"] = "lsp-devtools agent is stopping."
154
232
 
155
233
  # Cancel the tasks connecting client to server
@@ -1,6 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
- from typing import Any
3
- from typing import Optional
4
+ import typing
4
5
 
5
6
  import stamina
6
7
  from pygls.client import JsonRPCClient
@@ -9,6 +10,11 @@ from pygls.protocol import default_converter
9
10
 
10
11
  from lsp_devtools.agent.protocol import AgentProtocol
11
12
 
13
+ if typing.TYPE_CHECKING:
14
+ from typing import Any
15
+ from typing import List
16
+ from typing import Optional
17
+
12
18
  # from websockets.client import WebSocketClientProtocol
13
19
 
14
20
 
@@ -39,6 +45,7 @@ class AgentClient(JsonRPCClient):
39
45
  protocol_cls=AgentProtocol, converter_factory=default_converter
40
46
  )
41
47
  self.connected = False
48
+ self._buffer: List[bytes] = []
42
49
 
43
50
  def _report_server_error(self, error, source):
44
51
  # Bail on error
@@ -71,6 +78,22 @@ class AgentClient(JsonRPCClient):
71
78
  self.connected = True
72
79
  self._async_tasks.append(connection)
73
80
 
81
+ def forward_message(self, message: bytes):
82
+ """Forward the given message to the server instance."""
83
+
84
+ if not self.connected:
85
+ self._buffer.append(message)
86
+ return
87
+
88
+ if self.protocol.transport is None:
89
+ return
90
+
91
+ # Send any buffered messages
92
+ while len(self._buffer) > 0:
93
+ self.protocol.transport.write(self._buffer.pop(0))
94
+
95
+ self.protocol.transport.write(message)
96
+
74
97
  # TODO: Upstream this... or at least something equivalent.
75
98
  # def start_ws(self, host: str, port: int):
76
99
  # self.protocol._send_only_body = True # Don't send headers within the payload
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from pygls.protocol import JsonRPCProtocol
4
+
5
+
6
+ class AgentProtocol(JsonRPCProtocol):
7
+ """The RPC protocol exposed by the agent."""
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import traceback
6
+ import typing
7
+
8
+ from pygls.protocol import default_converter
9
+ from pygls.server import Server
10
+
11
+ from lsp_devtools.agent.agent import aio_readline
12
+ from lsp_devtools.agent.protocol import AgentProtocol
13
+ from lsp_devtools.database import Database
14
+
15
+ if typing.TYPE_CHECKING:
16
+ from typing import Any
17
+ from typing import List
18
+ from typing import Optional
19
+
20
+ from lsp_devtools.agent.agent import MessageHandler
21
+
22
+
23
+ class AgentServer(Server):
24
+ """A pygls server that accepts connections from agents allowing them to send their
25
+ collected messages."""
26
+
27
+ lsp: AgentProtocol
28
+
29
+ def __init__(
30
+ self,
31
+ *args,
32
+ logger: Optional[logging.Logger] = None,
33
+ handler: Optional[MessageHandler] = None,
34
+ **kwargs,
35
+ ):
36
+ if "protocol_cls" not in kwargs:
37
+ kwargs["protocol_cls"] = AgentProtocol
38
+
39
+ if "converter_factory" not in kwargs:
40
+ kwargs["converter_factory"] = default_converter
41
+
42
+ super().__init__(*args, **kwargs)
43
+
44
+ self.logger = logger or logging.getLogger(__name__)
45
+ self.handler = handler or self.lsp.data_received
46
+ self.db: Optional[Database] = None
47
+
48
+ self._client_buffer: List[str] = []
49
+ self._server_buffer: List[str] = []
50
+ self._tcp_server: Optional[asyncio.Task] = None
51
+
52
+ def _report_server_error(self, exc: Exception, source):
53
+ """Report internal server errors."""
54
+ tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
55
+ self.logger.error("%s: %s", type(exc).__name__, exc)
56
+ self.logger.debug("%s", tb)
57
+
58
+ def feature(self, feature_name: str, options: Optional[Any] = None):
59
+ return self.lsp.fm.feature(feature_name, options)
60
+
61
+ async def start_tcp(self, host: str, port: int) -> None: # type: ignore[override]
62
+ async def handle_client(reader, writer):
63
+ self.lsp.connection_made(writer)
64
+
65
+ try:
66
+ await aio_readline(reader, self.handler)
67
+ except asyncio.CancelledError:
68
+ pass
69
+ finally:
70
+ writer.close()
71
+ await writer.wait_closed()
72
+
73
+ # Uncomment if we ever need to introduce a mode where the server stops
74
+ # automatically once a session ends.
75
+ #
76
+ # self.stop()
77
+
78
+ server = await asyncio.start_server(handle_client, host, port)
79
+ async with server:
80
+ self._tcp_server = asyncio.create_task(server.serve_forever())
81
+ await self._tcp_server
82
+
83
+ def stop(self):
84
+ if self._tcp_server is not None:
85
+ self._tcp_server.cancel()
@@ -69,7 +69,9 @@ class LSPClient(App):
69
69
 
70
70
  def compose(self) -> ComposeResult:
71
71
  message_viewer = MessageViewer("")
72
- messages_table = MessagesTable(self.db, message_viewer, session=self.session)
72
+ messages_table = MessagesTable(
73
+ self.db, message_viewer, session=self.lsp_client.session_id
74
+ )
73
75
 
74
76
  yield Header()
75
77
  yield Explorer(".")
@@ -145,7 +147,7 @@ def client(args, extra: List[str]):
145
147
  db = Database(args.dbpath)
146
148
 
147
149
  session = str(uuid4())
148
- dbhandler = DatabaseLogHandler(db, session=session)
150
+ dbhandler = DatabaseLogHandler(db)
149
151
  dbhandler.setLevel(logging.INFO)
150
152
 
151
153
  logger.setLevel(logging.INFO)
@@ -1,6 +1,9 @@
1
1
  import importlib.metadata
2
2
  import json
3
+ from datetime import datetime
4
+ from datetime import timezone
3
5
  from typing import Optional
6
+ from uuid import uuid4
4
7
 
5
8
  from lsprotocol import types
6
9
  from pygls.lsp.client import BaseLanguageClient
@@ -8,6 +11,7 @@ from pygls.protocol import LanguageServerProtocol
8
11
 
9
12
  from lsp_devtools.agent import logger
10
13
 
14
+ UTC = timezone.utc
11
15
  VERSION = importlib.metadata.version("lsp-devtools")
12
16
 
13
17
 
@@ -16,12 +20,17 @@ class RecordingLSProtocol(LanguageServerProtocol):
16
20
 
17
21
  def __init__(self, server, converter):
18
22
  super().__init__(server, converter)
23
+ self.session_id = ""
19
24
 
20
25
  def _procedure_handler(self, message):
21
26
  logger.info(
22
27
  "%s",
23
28
  json.dumps(message, default=self._serialize_message),
24
- extra={"source": "server"},
29
+ extra={
30
+ "Message-Source": "server",
31
+ "Message-Session": self.session_id,
32
+ "Message-Timestamp": datetime.now(tz=UTC).isoformat(),
33
+ },
25
34
  )
26
35
  return super()._procedure_handler(message)
27
36
 
@@ -29,7 +38,11 @@ class RecordingLSProtocol(LanguageServerProtocol):
29
38
  logger.info(
30
39
  "%s",
31
40
  json.dumps(data, default=self._serialize_message),
32
- extra={"source": "client"},
41
+ extra={
42
+ "Message-Source": "client",
43
+ "Message-Session": self.session_id,
44
+ "Message-Timestamp": datetime.now(tz=UTC).isoformat(),
45
+ },
33
46
  )
34
47
  return super()._send_data(data)
35
48
 
@@ -40,6 +53,8 @@ class LanguageClient(BaseLanguageClient):
40
53
  def __init__(self):
41
54
  super().__init__("lsp-devtools", VERSION, protocol_cls=RecordingLSProtocol)
42
55
 
56
+ self.session_id = str(uuid4())
57
+ self.protocol.session_id = self.session_id # type: ignore[attr-defined]
43
58
  self._server_capabilities: Optional[types.ServerCapabilities] = None
44
59
 
45
60
  @property
@@ -9,7 +9,6 @@ from typing import Dict
9
9
  from typing import List
10
10
  from typing import Optional
11
11
  from typing import Set
12
- from uuid import uuid4
13
12
 
14
13
  import aiosqlite
15
14
  from textual.app import App
@@ -17,10 +16,10 @@ from textual.message import Message
17
16
 
18
17
  from lsp_devtools.handlers import LspMessage
19
18
 
20
- if sys.version_info.minor < 9:
19
+ if sys.version_info < (3, 9):
21
20
  import importlib_resources as resources
22
21
  else:
23
- import importlib.resources as resources # type: ignore[no-redef]
22
+ from importlib import resources # type: ignore[no-redef]
24
23
 
25
24
 
26
25
  class Database:
@@ -62,14 +61,14 @@ class Database:
62
61
 
63
62
  await self.db.commit()
64
63
 
65
- async def add_message(self, session: str, timestamp: float, source: str, rpc: dict):
64
+ async def add_message(self, session: str, timestamp: str, source: str, rpc: dict):
66
65
  """Add a new rpc message to the database."""
67
66
 
68
- msg_id = rpc.get("id", None)
69
- method = rpc.get("method", None)
70
- params = rpc.get("params", None)
71
- result = rpc.get("result", None)
72
- error = rpc.get("error", None)
67
+ msg_id = rpc.get("id")
68
+ method = rpc.get("method")
69
+ params = rpc.get("params")
70
+ result = rpc.get("result")
71
+ error = rpc.get("error")
73
72
 
74
73
  async with self.cursor() as cursor:
75
74
  await cursor.execute(
@@ -149,17 +148,19 @@ class Database:
149
148
  class DatabaseLogHandler(logging.Handler):
150
149
  """A logging handler that records messages in the given database."""
151
150
 
152
- def __init__(self, db: Database, *args, session=None, **kwargs):
151
+ def __init__(self, db: Database, *args, **kwargs):
153
152
  super().__init__(*args, **kwargs)
154
153
  self.db = db
155
- self.session = session or str(uuid4())
156
154
  self._tasks: Set[asyncio.Task] = set()
157
155
 
158
156
  def emit(self, record: logging.LogRecord):
159
157
  body = json.loads(record.args[0]) # type: ignore
160
158
  task = asyncio.create_task(
161
159
  self.db.add_message(
162
- self.session, record.created, record.__dict__["source"], body
160
+ record.__dict__["Message-Session"],
161
+ record.__dict__["Message-Timestamp"],
162
+ record.__dict__["Message-Source"],
163
+ body,
163
164
  )
164
165
  )
165
166