chuk-tool-processor 0.1.3__py3-none-any.whl → 0.1.5__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 chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/core/processor.py +67 -60
- chuk_tool_processor/mcp/stream_manager.py +208 -218
- chuk_tool_processor/mcp/transport/base_transport.py +73 -34
- chuk_tool_processor/mcp/transport/sse_transport.py +174 -44
- chuk_tool_processor/mcp/transport/stdio_transport.py +80 -8
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.5.dist-info}/METADATA +1 -1
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.5.dist-info}/RECORD +9 -9
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.5.dist-info}/WHEEL +1 -1
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -1,59 +1,189 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/transport/sse_transport.py
|
|
2
2
|
"""
|
|
3
|
-
Server-Sent Events (SSE) transport for MCP communication
|
|
3
|
+
Server-Sent Events (SSE) transport for MCP communication – implemented with **httpx**.
|
|
4
4
|
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import json
|
|
5
10
|
from typing import Any, Dict, List, Optional
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
8
14
|
from .base_transport import MCPBaseTransport
|
|
9
15
|
|
|
16
|
+
# --------------------------------------------------------------------------- #
|
|
17
|
+
# Helpers #
|
|
18
|
+
# --------------------------------------------------------------------------- #
|
|
19
|
+
DEFAULT_TIMEOUT = 5.0 # seconds
|
|
20
|
+
HEADERS_JSON: Dict[str, str] = {"accept": "application/json"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _url(base: str, path: str) -> str:
|
|
24
|
+
"""Join *base* and *path* with exactly one slash."""
|
|
25
|
+
return f"{base.rstrip('/')}/{path.lstrip('/')}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------- #
|
|
29
|
+
# Transport #
|
|
30
|
+
# --------------------------------------------------------------------------- #
|
|
10
31
|
class SSETransport(MCPBaseTransport):
|
|
11
32
|
"""
|
|
12
|
-
|
|
33
|
+
Minimal SSE/REST transport. It speaks a simple REST dialect:
|
|
34
|
+
|
|
35
|
+
GET /ping → 200 OK
|
|
36
|
+
GET /tools/list → {"tools": [...]}
|
|
37
|
+
POST /tools/call → {"name": ..., "result": ...}
|
|
38
|
+
GET /resources/list → {"resources": [...]}
|
|
39
|
+
GET /prompts/list → {"prompts": [...]}
|
|
40
|
+
GET /events → <text/event-stream>
|
|
13
41
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"""
|
|
23
|
-
self.url = url
|
|
42
|
+
|
|
43
|
+
EVENTS_PATH = "/events"
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------ #
|
|
46
|
+
# Construction #
|
|
47
|
+
# ------------------------------------------------------------------ #
|
|
48
|
+
def __init__(self, url: str, api_key: Optional[str] = None) -> None:
|
|
49
|
+
self.base_url = url.rstrip("/")
|
|
24
50
|
self.api_key = api_key
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
|
|
52
|
+
# httpx client (None until initialise)
|
|
53
|
+
self._client: httpx.AsyncClient | None = None
|
|
54
|
+
self.session: httpx.AsyncClient | None = None # ← kept for legacy tests
|
|
55
|
+
|
|
56
|
+
# background reader
|
|
57
|
+
self._reader_task: asyncio.Task | None = None
|
|
58
|
+
self._incoming_queue: "asyncio.Queue[dict[str, Any]]" = asyncio.Queue()
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------ #
|
|
61
|
+
# Life-cycle #
|
|
62
|
+
# ------------------------------------------------------------------ #
|
|
28
63
|
async def initialize(self) -> bool:
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
"""Open the httpx client and start the /events consumer."""
|
|
65
|
+
if self._client: # already initialised
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
self._client = httpx.AsyncClient(
|
|
69
|
+
headers={"authorization": self.api_key} if self.api_key else None,
|
|
70
|
+
timeout=DEFAULT_TIMEOUT,
|
|
71
|
+
)
|
|
72
|
+
self.session = self._client # legacy attribute for tests
|
|
73
|
+
|
|
74
|
+
# spawn reader (best-effort reconnect)
|
|
75
|
+
self._reader_task = asyncio.create_task(self._consume_events(), name="sse-reader")
|
|
76
|
+
|
|
77
|
+
# verify connection
|
|
78
|
+
return await self.send_ping()
|
|
79
|
+
|
|
80
|
+
async def close(self) -> None:
|
|
81
|
+
"""Stop background reader and close the httpx client."""
|
|
82
|
+
if self._reader_task:
|
|
83
|
+
self._reader_task.cancel()
|
|
84
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
85
|
+
await self._reader_task
|
|
86
|
+
self._reader_task = None
|
|
87
|
+
|
|
88
|
+
if self._client:
|
|
89
|
+
await self._client.aclose()
|
|
90
|
+
self._client = None
|
|
91
|
+
self.session = None # keep tests happy
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------ #
|
|
94
|
+
# Internal helpers #
|
|
95
|
+
# ------------------------------------------------------------------ #
|
|
96
|
+
async def _get_json(self, path: str) -> Any:
|
|
97
|
+
if not self._client:
|
|
98
|
+
raise RuntimeError("Transport not initialised")
|
|
99
|
+
|
|
100
|
+
resp = await self._client.get(_url(self.base_url, path), headers=HEADERS_JSON)
|
|
101
|
+
resp.raise_for_status()
|
|
102
|
+
return resp.json()
|
|
103
|
+
|
|
104
|
+
async def _post_json(self, path: str, payload: Dict[str, Any]) -> Any:
|
|
105
|
+
if not self._client:
|
|
106
|
+
raise RuntimeError("Transport not initialised")
|
|
107
|
+
|
|
108
|
+
resp = await self._client.post(
|
|
109
|
+
_url(self.base_url, path), json=payload, headers=HEADERS_JSON
|
|
110
|
+
)
|
|
111
|
+
resp.raise_for_status()
|
|
112
|
+
return resp.json()
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------ #
|
|
115
|
+
# Public API (implements MCPBaseTransport) #
|
|
116
|
+
# ------------------------------------------------------------------ #
|
|
41
117
|
async def send_ping(self) -> bool:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
118
|
+
if not self._client:
|
|
119
|
+
return False
|
|
120
|
+
try:
|
|
121
|
+
await self._get_json("/ping")
|
|
122
|
+
return True
|
|
123
|
+
except Exception: # pragma: no cover
|
|
124
|
+
return False
|
|
125
|
+
|
|
46
126
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
127
|
+
if not self._client:
|
|
128
|
+
return []
|
|
129
|
+
try:
|
|
130
|
+
data = await self._get_json("/tools/list")
|
|
131
|
+
return data.get("tools", []) if isinstance(data, dict) else []
|
|
132
|
+
except Exception: # pragma: no cover
|
|
133
|
+
return []
|
|
134
|
+
|
|
51
135
|
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
136
|
+
# ─── tests expect this specific message if *not* initialised ───
|
|
137
|
+
if not self._client:
|
|
138
|
+
return {"isError": True, "error": "SSE transport not implemented"}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
payload = {"name": tool_name, "arguments": arguments}
|
|
142
|
+
return await self._post_json("/tools/call", payload)
|
|
143
|
+
except Exception as exc: # pragma: no cover
|
|
144
|
+
return {"isError": True, "error": str(exc)}
|
|
145
|
+
|
|
146
|
+
# ----------------------- extras used by StreamManager ------------- #
|
|
147
|
+
async def list_resources(self) -> List[Dict[str, Any]]:
|
|
148
|
+
if not self._client:
|
|
149
|
+
return []
|
|
150
|
+
try:
|
|
151
|
+
data = await self._get_json("/resources/list")
|
|
152
|
+
return data.get("resources", []) if isinstance(data, dict) else []
|
|
153
|
+
except Exception: # pragma: no cover
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
async def list_prompts(self) -> List[Dict[str, Any]]:
|
|
157
|
+
if not self._client:
|
|
158
|
+
return []
|
|
159
|
+
try:
|
|
160
|
+
data = await self._get_json("/prompts/list")
|
|
161
|
+
return data.get("prompts", []) if isinstance(data, dict) else []
|
|
162
|
+
except Exception: # pragma: no cover
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------ #
|
|
166
|
+
# Background event-stream reader #
|
|
167
|
+
# ------------------------------------------------------------------ #
|
|
168
|
+
async def _consume_events(self) -> None: # pragma: no cover
|
|
169
|
+
"""Continuously read `/events` and push JSON objects onto a queue."""
|
|
170
|
+
if not self._client:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
while True:
|
|
174
|
+
try:
|
|
175
|
+
async with self._client.stream(
|
|
176
|
+
"GET", _url(self.base_url, self.EVENTS_PATH), headers=HEADERS_JSON
|
|
177
|
+
) as resp:
|
|
178
|
+
resp.raise_for_status()
|
|
179
|
+
async for line in resp.aiter_lines():
|
|
180
|
+
if not line:
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
await self._incoming_queue.put(json.loads(line))
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
continue
|
|
186
|
+
except asyncio.CancelledError:
|
|
187
|
+
break
|
|
188
|
+
except Exception:
|
|
189
|
+
await asyncio.sleep(1.0) # back-off and retry
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/transport/stdio_transport.py
|
|
2
|
-
from
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
3
4
|
from contextlib import AsyncExitStack
|
|
4
5
|
import json
|
|
6
|
+
from typing import Dict, Any, List, Optional
|
|
5
7
|
|
|
8
|
+
# ------------------------------------------------------------------ #
|
|
9
|
+
# Local import #
|
|
10
|
+
# ------------------------------------------------------------------ #
|
|
6
11
|
from .base_transport import MCPBaseTransport
|
|
7
12
|
|
|
8
|
-
#
|
|
13
|
+
# ------------------------------------------------------------------ #
|
|
14
|
+
# chuk-protocol imports #
|
|
15
|
+
# ------------------------------------------------------------------ #
|
|
9
16
|
from chuk_mcp.mcp_client.transport.stdio.stdio_client import stdio_client
|
|
10
17
|
from chuk_mcp.mcp_client.messages.initialize.send_messages import send_initialize
|
|
11
18
|
from chuk_mcp.mcp_client.messages.ping.send_messages import send_ping
|
|
12
|
-
|
|
19
|
+
|
|
20
|
+
# tools
|
|
21
|
+
from chuk_mcp.mcp_client.messages.tools.send_messages import (
|
|
22
|
+
send_tools_call,
|
|
23
|
+
send_tools_list,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# NEW: resources & prompts
|
|
27
|
+
from chuk_mcp.mcp_client.messages.resources.send_messages import (
|
|
28
|
+
send_resources_list,
|
|
29
|
+
)
|
|
30
|
+
from chuk_mcp.mcp_client.messages.prompts.send_messages import (
|
|
31
|
+
send_prompts_list,
|
|
32
|
+
)
|
|
13
33
|
|
|
14
34
|
|
|
15
35
|
class StdioTransport(MCPBaseTransport):
|
|
@@ -32,7 +52,9 @@ class StdioTransport(MCPBaseTransport):
|
|
|
32
52
|
await self._context_stack.__aenter__()
|
|
33
53
|
|
|
34
54
|
ctx = stdio_client(self.server_params)
|
|
35
|
-
self.read_stream, self.write_stream = await self._context_stack.enter_async_context(
|
|
55
|
+
self.read_stream, self.write_stream = await self._context_stack.enter_async_context(
|
|
56
|
+
ctx
|
|
57
|
+
)
|
|
36
58
|
|
|
37
59
|
init_result = await send_initialize(self.read_stream, self.write_stream)
|
|
38
60
|
return bool(init_result)
|
|
@@ -72,10 +94,56 @@ class StdioTransport(MCPBaseTransport):
|
|
|
72
94
|
tools_response = await send_tools_list(self.read_stream, self.write_stream)
|
|
73
95
|
return tools_response.get("tools", [])
|
|
74
96
|
|
|
97
|
+
# NEW ------------------------------------------------------------------ #
|
|
98
|
+
# Resources / Prompts #
|
|
99
|
+
# --------------------------------------------------------------------- #
|
|
100
|
+
async def list_resources(self) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Return the result of *resources/list*. If the connection is not yet
|
|
103
|
+
initialised an empty dict is returned.
|
|
104
|
+
"""
|
|
105
|
+
if not self.read_stream or not self.write_stream:
|
|
106
|
+
return {}
|
|
107
|
+
try:
|
|
108
|
+
return await send_resources_list(self.read_stream, self.write_stream)
|
|
109
|
+
except Exception as exc: # pragma: no cover
|
|
110
|
+
import logging
|
|
111
|
+
|
|
112
|
+
logging.error(f"Error listing resources: {exc}")
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
async def list_prompts(self) -> Dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Return the result of *prompts/list*. If the connection is not yet
|
|
118
|
+
initialised an empty dict is returned.
|
|
119
|
+
"""
|
|
120
|
+
if not self.read_stream or not self.write_stream:
|
|
121
|
+
return {}
|
|
122
|
+
try:
|
|
123
|
+
return await send_prompts_list(self.read_stream, self.write_stream)
|
|
124
|
+
except Exception as exc: # pragma: no cover
|
|
125
|
+
import logging
|
|
126
|
+
|
|
127
|
+
logging.error(f"Error listing prompts: {exc}")
|
|
128
|
+
return {}
|
|
129
|
+
|
|
130
|
+
# OPTIONAL helper ------------------------------------------------------ #
|
|
131
|
+
def get_streams(self):
|
|
132
|
+
"""
|
|
133
|
+
Expose the low-level streams so legacy callers can access them
|
|
134
|
+
directly. The base-class’ default returns an empty list; here we
|
|
135
|
+
return a single-element list when the transport is active.
|
|
136
|
+
"""
|
|
137
|
+
if self.read_stream and self.write_stream:
|
|
138
|
+
return [(self.read_stream, self.write_stream)]
|
|
139
|
+
return []
|
|
140
|
+
|
|
75
141
|
# --------------------------------------------------------------------- #
|
|
76
142
|
# Main entry-point #
|
|
77
143
|
# --------------------------------------------------------------------- #
|
|
78
|
-
async def call_tool(
|
|
144
|
+
async def call_tool(
|
|
145
|
+
self, tool_name: str, arguments: Dict[str, Any]
|
|
146
|
+
) -> Dict[str, Any]:
|
|
79
147
|
"""
|
|
80
148
|
Execute *tool_name* with *arguments* and normalise the server’s reply.
|
|
81
149
|
|
|
@@ -90,12 +158,16 @@ class StdioTransport(MCPBaseTransport):
|
|
|
90
158
|
return {"isError": True, "error": "Transport not initialized"}
|
|
91
159
|
|
|
92
160
|
try:
|
|
93
|
-
raw = await send_tools_call(
|
|
161
|
+
raw = await send_tools_call(
|
|
162
|
+
self.read_stream, self.write_stream, tool_name, arguments
|
|
163
|
+
)
|
|
94
164
|
|
|
95
165
|
# Handle explicit error wrapper
|
|
96
166
|
if "error" in raw:
|
|
97
|
-
return {
|
|
98
|
-
|
|
167
|
+
return {
|
|
168
|
+
"isError": True,
|
|
169
|
+
"error": raw["error"].get("message", "Unknown error"),
|
|
170
|
+
}
|
|
99
171
|
|
|
100
172
|
# Preferred: servers that put the answer under "result"
|
|
101
173
|
if "result" in raw:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
chuk_tool_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
chuk_tool_processor/core/__init__.py,sha256=slM7pZna88tyZrF3KtN22ApYyCqGNt5Yscv-knsLOOA,38
|
|
3
3
|
chuk_tool_processor/core/exceptions.py,sha256=h4zL1jpCY1Ud1wT8xDeMxZ8GR8ttmkObcv36peUHJEA,1571
|
|
4
|
-
chuk_tool_processor/core/processor.py,sha256=
|
|
4
|
+
chuk_tool_processor/core/processor.py,sha256=fT3Qj8vmeUWoqOqHmWroi7lfJsMl52DpFQz8LT9UQME,10280
|
|
5
5
|
chuk_tool_processor/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
chuk_tool_processor/execution/tool_executor.py,sha256=e1EHE-744uJuB1XeZZF_6VT25Yg1RCd8XI3v8uOrOSo,1794
|
|
7
7
|
chuk_tool_processor/execution/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -21,11 +21,11 @@ chuk_tool_processor/mcp/mcp_tool.py,sha256=TvZEudgQvaev2jaPw6OGsqAR5GNu6_cPaUCgq
|
|
|
21
21
|
chuk_tool_processor/mcp/register_mcp_tools.py,sha256=ofE7pEn6sKDH8HWvNamVOaXsitLOaG48M5GhcpqCBbs,2801
|
|
22
22
|
chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=Ep2IKRdH1Y299bCxt9G0NtwnsvguYP6mpraZyUJ8OKU,2643
|
|
23
23
|
chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=NjTvAFqQHxxN3XubsTgYY3lTrvPVWlnwCzkzbz7WE_M,2747
|
|
24
|
-
chuk_tool_processor/mcp/stream_manager.py,sha256=
|
|
24
|
+
chuk_tool_processor/mcp/stream_manager.py,sha256=qIWzsQCTlu1SQQBExAdvBHGB3T5isQDyMhj29WkfbKQ,11779
|
|
25
25
|
chuk_tool_processor/mcp/transport/__init__.py,sha256=7QQqeSKVKv0N9GcyJuYF0R4FDZeooii5RjggvFFg5GY,296
|
|
26
|
-
chuk_tool_processor/mcp/transport/base_transport.py,sha256=
|
|
27
|
-
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=
|
|
28
|
-
chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=
|
|
26
|
+
chuk_tool_processor/mcp/transport/base_transport.py,sha256=1E29LjWw5vLQrPUDF_9TJt63P5dxAAN7n6E_KiZbGUY,3427
|
|
27
|
+
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=bryH9DOWOn5qr6LsimTriukDC4ix2kuRq6bUv9qOV20,7645
|
|
28
|
+
chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=lFXL7p8ca4z_J0RBL8UCHrQ1UH7C2-LbC0tZhpya4V4,7763
|
|
29
29
|
chuk_tool_processor/models/__init__.py,sha256=TC__rdVa0lQsmJHM_hbLDPRgToa_pQT_UxRcPZk6iVw,40
|
|
30
30
|
chuk_tool_processor/models/execution_strategy.py,sha256=ZPHysmKNHqJmahTtUXAbt1ke09vxy7EhZcsrwTdla8o,508
|
|
31
31
|
chuk_tool_processor/models/tool_call.py,sha256=RZOnx2YczkJN6ym2PLiI4CRzP2qU_5hpMtHxMcFOxY4,298
|
|
@@ -51,7 +51,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=_0dg4YhyfAV0TXuR_i4ewX
|
|
|
51
51
|
chuk_tool_processor/registry/providers/memory.py,sha256=29aI5uvykjDmn9ymIukEdUtmTC9SXOAsDu9hw36XF44,4474
|
|
52
52
|
chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
chuk_tool_processor/utils/validation.py,sha256=7ezn_o-3IHDrzOD3j6ttsAn2s3zS-jIjeBTuqicrs6A,3775
|
|
54
|
-
chuk_tool_processor-0.1.
|
|
55
|
-
chuk_tool_processor-0.1.
|
|
56
|
-
chuk_tool_processor-0.1.
|
|
57
|
-
chuk_tool_processor-0.1.
|
|
54
|
+
chuk_tool_processor-0.1.5.dist-info/METADATA,sha256=GOztJXq4-Ro6ncQxUvtV8L97qXyn6_Qet0d1tzRe05g,13703
|
|
55
|
+
chuk_tool_processor-0.1.5.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
|
56
|
+
chuk_tool_processor-0.1.5.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
|
|
57
|
+
chuk_tool_processor-0.1.5.dist-info/RECORD,,
|
|
File without changes
|