nvidia-nat-mcp 1.3.0a20251012__py3-none-any.whl → 1.4.0a20251105__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 nvidia-nat-mcp might be problematic. Click here for more details.
- nat/plugins/mcp/auth/auth_flow_handler.py +64 -0
- nat/plugins/mcp/auth/auth_provider_config.py +3 -1
- nat/plugins/mcp/utils.py +152 -35
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/METADATA +2 -2
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/RECORD +10 -10
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/WHEEL +0 -0
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/entry_points.txt +0 -0
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/licenses/LICENSE.md +0 -0
- {nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/top_level.txt +0 -0
|
@@ -54,6 +54,9 @@ class MCPAuthenticationFlowHandler(ConsoleAuthenticationFlowHandler):
|
|
|
54
54
|
self._redirect_app: FastAPI | None = None
|
|
55
55
|
self._server_lock = asyncio.Lock()
|
|
56
56
|
self._oauth_client: AsyncOAuth2Client | None = None
|
|
57
|
+
self._redirect_host: str = "localhost" # Default host, will be overridden from config
|
|
58
|
+
self._redirect_port: int = 8000 # Default port, will be overridden from config
|
|
59
|
+
self._server_task: asyncio.Task | None = None
|
|
57
60
|
|
|
58
61
|
async def authenticate(self, config: AuthProviderBaseConfig, method: AuthFlowType) -> AuthenticatedContext:
|
|
59
62
|
"""
|
|
@@ -88,6 +91,34 @@ class MCPAuthenticationFlowHandler(ConsoleAuthenticationFlowHandler):
|
|
|
88
91
|
async def _handle_oauth2_auth_code_flow(self, cfg: OAuth2AuthCodeFlowProviderConfig) -> AuthenticatedContext:
|
|
89
92
|
logger.info("Starting MCP OAuth2 authorization code flow")
|
|
90
93
|
|
|
94
|
+
# Extract and validate host and port from redirect_uri for callback server
|
|
95
|
+
from urllib.parse import urlparse
|
|
96
|
+
parsed_uri = urlparse(str(cfg.redirect_uri))
|
|
97
|
+
|
|
98
|
+
# Validate scheme/host and choose a safe non-privileged bind port
|
|
99
|
+
scheme = (parsed_uri.scheme or "http").lower()
|
|
100
|
+
if scheme not in ("http", "https"):
|
|
101
|
+
raise ValueError(f"redirect_uri must use http or https scheme, got '{scheme}'")
|
|
102
|
+
|
|
103
|
+
host = parsed_uri.hostname
|
|
104
|
+
if not host:
|
|
105
|
+
raise ValueError("redirect_uri must include a hostname, for example http://localhost:8000/auth/redirect")
|
|
106
|
+
|
|
107
|
+
# Never auto-bind to 80/443; default to 8000 when port is not specified
|
|
108
|
+
port = parsed_uri.port or 8000
|
|
109
|
+
if not (1 <= port <= 65535):
|
|
110
|
+
raise ValueError(f"Invalid redirect port: {port}. Expected 1-65535.")
|
|
111
|
+
|
|
112
|
+
if scheme == "https" and parsed_uri.port is None:
|
|
113
|
+
logger.warning(
|
|
114
|
+
"redirect_uri uses https without an explicit port; binding to %d (plain HTTP). "
|
|
115
|
+
"Terminate TLS at a reverse proxy and forward to this port.",
|
|
116
|
+
port)
|
|
117
|
+
|
|
118
|
+
self._redirect_host = host
|
|
119
|
+
self._redirect_port = port
|
|
120
|
+
logger.info("MCP redirect server will use %s:%d", self._redirect_host, self._redirect_port)
|
|
121
|
+
|
|
91
122
|
state = secrets.token_urlsafe(16)
|
|
92
123
|
flow_state = _FlowState()
|
|
93
124
|
client = self.construct_oauth_client(cfg)
|
|
@@ -142,3 +173,36 @@ class MCPAuthenticationFlowHandler(ConsoleAuthenticationFlowHandler):
|
|
|
142
173
|
"raw_token": token,
|
|
143
174
|
},
|
|
144
175
|
)
|
|
176
|
+
|
|
177
|
+
async def _start_redirect_server(self) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Override to use the host and port from redirect_uri config instead of hardcoded localhost:8000.
|
|
180
|
+
|
|
181
|
+
This allows MCP authentication to work with custom redirect hosts and ports
|
|
182
|
+
specified in the configuration.
|
|
183
|
+
"""
|
|
184
|
+
# If the server is already running, do nothing
|
|
185
|
+
if self._server_controller:
|
|
186
|
+
return
|
|
187
|
+
try:
|
|
188
|
+
if not self._redirect_app:
|
|
189
|
+
raise RuntimeError("Redirect app not built.")
|
|
190
|
+
|
|
191
|
+
self._server_controller = _FastApiFrontEndController(self._redirect_app)
|
|
192
|
+
|
|
193
|
+
self._server_task = asyncio.create_task(
|
|
194
|
+
self._server_controller.start_server(host=self._redirect_host, port=self._redirect_port))
|
|
195
|
+
logger.debug("MCP redirect server starting on %s:%d", self._redirect_host, self._redirect_port)
|
|
196
|
+
|
|
197
|
+
# Wait for the server to bind (max ~10s)
|
|
198
|
+
start = asyncio.get_running_loop().time()
|
|
199
|
+
while True:
|
|
200
|
+
server = getattr(self._server_controller, "_server", None)
|
|
201
|
+
if server and getattr(server, "started", False):
|
|
202
|
+
break
|
|
203
|
+
if asyncio.get_running_loop().time() - start > 10:
|
|
204
|
+
raise RuntimeError("Redirect server did not report ready within 10s")
|
|
205
|
+
await asyncio.sleep(0.1)
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Failed to start MCP redirect server on {self._redirect_host}:{self._redirect_port}: {exc}") from exc
|
|
@@ -18,6 +18,7 @@ from pydantic import HttpUrl
|
|
|
18
18
|
from pydantic import model_validator
|
|
19
19
|
|
|
20
20
|
from nat.authentication.interfaces import AuthProviderBaseConfig
|
|
21
|
+
from nat.data_models.common import OptionalSecretStr
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class MCPOAuth2ProviderConfig(AuthProviderBaseConfig, name="mcp_oauth2"):
|
|
@@ -36,7 +37,8 @@ class MCPOAuth2ProviderConfig(AuthProviderBaseConfig, name="mcp_oauth2"):
|
|
|
36
37
|
|
|
37
38
|
# Client registration (manual registration vs DCR)
|
|
38
39
|
client_id: str | None = Field(default=None, description="OAuth2 client ID for pre-registered clients")
|
|
39
|
-
client_secret:
|
|
40
|
+
client_secret: OptionalSecretStr = Field(default=None,
|
|
41
|
+
description="OAuth2 client secret for pre-registered clients")
|
|
40
42
|
enable_dynamic_registration: bool = Field(default=True,
|
|
41
43
|
description="Enable OAuth2 Dynamic Client Registration (RFC 7591)")
|
|
42
44
|
client_name: str = Field(default="NAT MCP Client", description="OAuth2 client name for dynamic registration")
|
nat/plugins/mcp/utils.py
CHANGED
|
@@ -47,7 +47,7 @@ def model_from_mcp_schema(name: str, mcp_input_schema: dict) -> type[BaseModel]:
|
|
|
47
47
|
"integer": int,
|
|
48
48
|
"boolean": bool,
|
|
49
49
|
"array": list,
|
|
50
|
-
"null": None,
|
|
50
|
+
"null": type(None),
|
|
51
51
|
"object": dict,
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -58,51 +58,168 @@ def model_from_mcp_schema(name: str, mcp_input_schema: dict) -> type[BaseModel]:
|
|
|
58
58
|
def _generate_valid_classname(class_name: str):
|
|
59
59
|
return class_name.replace('_', ' ').replace('-', ' ').title().replace(' ', '')
|
|
60
60
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
def _resolve_schema_type(schema: dict[str, Any], name: str) -> Any:
|
|
62
|
+
"""
|
|
63
|
+
Recursively resolve a JSON schema to a Python type.
|
|
64
|
+
Handles nested anyOf/oneOf, arrays, objects, enums, and primitive types.
|
|
65
|
+
"""
|
|
66
|
+
# Check for anyOf/oneOf first
|
|
67
|
+
any_of = schema.get("anyOf")
|
|
68
|
+
one_of = schema.get("oneOf")
|
|
69
|
+
|
|
70
|
+
if any_of or one_of:
|
|
71
|
+
union_schemas = any_of if any_of else one_of
|
|
72
|
+
resolved_type: Any = None
|
|
73
|
+
|
|
74
|
+
if union_schemas:
|
|
75
|
+
for sub_schema in union_schemas:
|
|
76
|
+
mapped = _resolve_schema_type(sub_schema, name)
|
|
77
|
+
if resolved_type is None:
|
|
78
|
+
resolved_type = mapped
|
|
79
|
+
elif mapped is not type(None):
|
|
80
|
+
# Don't add None here, handle separately
|
|
81
|
+
resolved_type = resolved_type | mapped
|
|
82
|
+
else:
|
|
83
|
+
# If we encounter null, combine with None at the end
|
|
84
|
+
resolved_type = resolved_type | None if resolved_type else type(None)
|
|
85
|
+
|
|
86
|
+
return resolved_type if resolved_type is not None else Any
|
|
87
|
+
|
|
88
|
+
# Handle enum values
|
|
89
|
+
enum_vals = schema.get("enum")
|
|
65
90
|
if enum_vals:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
# Check if enum contains null
|
|
92
|
+
has_null = any(val is None or val == "null" for val in enum_vals)
|
|
93
|
+
# Filter out None/null values from enum
|
|
94
|
+
non_null_vals = [v for v in enum_vals if v is not None and v != "null"]
|
|
95
|
+
|
|
96
|
+
if non_null_vals:
|
|
97
|
+
enum_name = f"{name.capitalize()}Enum"
|
|
98
|
+
enum_type: Any = Enum(enum_name, {item: item for item in non_null_vals})
|
|
99
|
+
# If enum had null, make it a union with None
|
|
100
|
+
return enum_type | None if has_null else enum_type
|
|
101
|
+
elif has_null:
|
|
102
|
+
# Enum only contains null
|
|
103
|
+
return type(None)
|
|
75
104
|
else:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
# Empty enum (shouldn't happen but handle gracefully)
|
|
106
|
+
return Any
|
|
107
|
+
|
|
108
|
+
schema_type = schema.get("type")
|
|
109
|
+
|
|
110
|
+
# Handle type as list (e.g., ["string", "integer", "null"])
|
|
111
|
+
if isinstance(schema_type, list):
|
|
112
|
+
list_type: Any = None
|
|
113
|
+
for t in schema_type:
|
|
114
|
+
if t == "array":
|
|
115
|
+
# Incorporate the mapped type of items
|
|
116
|
+
item_schema = schema.get("items", {})
|
|
117
|
+
if item_schema:
|
|
118
|
+
item_type = _resolve_schema_type(item_schema, name)
|
|
119
|
+
mapped = list[item_type]
|
|
120
|
+
else:
|
|
121
|
+
mapped = _type_map.get(t, Any)
|
|
122
|
+
elif t == "object":
|
|
123
|
+
# Incorporate the mapped type from properties
|
|
124
|
+
if "properties" in schema:
|
|
125
|
+
mapped = model_from_mcp_schema(name=name, mcp_input_schema=schema)
|
|
126
|
+
else:
|
|
127
|
+
mapped = _type_map.get(t, Any)
|
|
128
|
+
else:
|
|
129
|
+
mapped = _type_map.get(t, Any)
|
|
130
|
+
|
|
131
|
+
list_type = mapped if list_type is None else list_type | mapped
|
|
132
|
+
return list_type if list_type is not None else Any
|
|
133
|
+
|
|
134
|
+
# Handle null type
|
|
135
|
+
if schema_type == "null":
|
|
136
|
+
return type(None)
|
|
137
|
+
|
|
138
|
+
# Handle object type
|
|
139
|
+
if schema_type == "object" and "properties" in schema:
|
|
140
|
+
return model_from_mcp_schema(name=name, mcp_input_schema=schema)
|
|
141
|
+
|
|
142
|
+
# Handle array type
|
|
143
|
+
if schema_type == "array" and "items" in schema:
|
|
144
|
+
item_schema = schema.get("items", {})
|
|
145
|
+
# Recursively resolve item type (handles nested anyOf/oneOf)
|
|
146
|
+
item_type = _resolve_schema_type(item_schema, name)
|
|
147
|
+
return list[item_type]
|
|
148
|
+
|
|
149
|
+
# Handle primitive types
|
|
150
|
+
if schema_type is not None:
|
|
151
|
+
return _type_map.get(schema_type, Any)
|
|
152
|
+
|
|
153
|
+
return Any
|
|
154
|
+
|
|
155
|
+
def _has_null_in_type(field_properties: dict[str, Any]) -> bool:
|
|
156
|
+
"""Check if a schema contains null as a valid type."""
|
|
157
|
+
# Check anyOf/oneOf for null
|
|
158
|
+
any_of = field_properties.get("anyOf")
|
|
159
|
+
one_of = field_properties.get("oneOf")
|
|
160
|
+
if any_of or one_of:
|
|
161
|
+
union_schemas = any_of if any_of else one_of
|
|
162
|
+
if union_schemas:
|
|
163
|
+
for schema in union_schemas:
|
|
164
|
+
if schema.get("type") == "null":
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
# Check type list for null
|
|
168
|
+
json_type = field_properties.get("type")
|
|
169
|
+
if isinstance(json_type, list) and "null" in json_type:
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
# Check enum for null (Python None or string "null")
|
|
173
|
+
enum_vals = field_properties.get("enum")
|
|
174
|
+
if enum_vals:
|
|
175
|
+
for val in enum_vals:
|
|
176
|
+
if val is None or val == "null":
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# Check const for null (Python None or string "null")
|
|
180
|
+
if "const" in field_properties:
|
|
181
|
+
const_val = field_properties.get("const")
|
|
182
|
+
if const_val is None or const_val == "null":
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _generate_field(field_name: str, field_properties: dict[str, Any]) -> tuple:
|
|
188
|
+
"""
|
|
189
|
+
Generate a Pydantic field from JSON schema properties.
|
|
190
|
+
Uses _resolve_schema_type for type resolution and handles field-specific logic.
|
|
191
|
+
"""
|
|
192
|
+
# Resolve the field type using the unified resolver
|
|
193
|
+
field_type = _resolve_schema_type(field_properties, field_name)
|
|
194
|
+
|
|
195
|
+
# Check if the type includes null
|
|
196
|
+
has_null = _has_null_in_type(field_properties)
|
|
90
197
|
|
|
91
198
|
# Determine the default value based on whether the field is required
|
|
199
|
+
default_value = field_properties.get("default")
|
|
200
|
+
|
|
92
201
|
if field_name in required_fields:
|
|
93
|
-
# Field is required - use explicit default if provided, otherwise
|
|
94
|
-
default_value
|
|
202
|
+
# Field is required - use explicit default if provided, otherwise use ... to enforce presence
|
|
203
|
+
if default_value is None and "default" not in field_properties:
|
|
204
|
+
# Required field without explicit default: always use ... even if nullable
|
|
205
|
+
default_value = ...
|
|
206
|
+
# Make the field type nullable if it allows null
|
|
207
|
+
if has_null:
|
|
208
|
+
field_type = field_type | None
|
|
95
209
|
else:
|
|
96
210
|
# Field is optional - use explicit default if provided, otherwise None
|
|
97
|
-
default_value
|
|
98
|
-
|
|
99
|
-
if
|
|
211
|
+
if default_value is None:
|
|
212
|
+
default_value = None
|
|
213
|
+
# Make the type optional if no default was provided and not already nullable
|
|
214
|
+
if "default" not in field_properties and not has_null:
|
|
100
215
|
field_type = field_type | None
|
|
101
216
|
|
|
217
|
+
# Handle nullable property (less common, but still supported)
|
|
102
218
|
nullable = field_properties.get("nullable", False)
|
|
103
|
-
|
|
219
|
+
if nullable and not has_null:
|
|
220
|
+
field_type = field_type | None
|
|
104
221
|
|
|
105
|
-
|
|
222
|
+
description = field_properties.get("description", "")
|
|
106
223
|
|
|
107
224
|
return field_type, Field(default=default_value, description=description)
|
|
108
225
|
|
{nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nvidia-nat-mcp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0a20251105
|
|
4
4
|
Summary: Subpackage for MCP client integration in NeMo Agent toolkit
|
|
5
5
|
Author: NVIDIA Corporation
|
|
6
6
|
Maintainer: NVIDIA Corporation
|
|
@@ -16,7 +16,7 @@ Requires-Python: <3.14,>=3.11
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE-3rd-party.txt
|
|
18
18
|
License-File: LICENSE.md
|
|
19
|
-
Requires-Dist: nvidia-nat==v1.
|
|
19
|
+
Requires-Dist: nvidia-nat==v1.4.0a20251105
|
|
20
20
|
Requires-Dist: aiorwlock~=1.5
|
|
21
21
|
Requires-Dist: mcp~=1.14
|
|
22
22
|
Dynamic: license-file
|
|
@@ -7,17 +7,17 @@ nat/plugins/mcp/exception_handler.py,sha256=4JVdZDJL4LyumZEcMIEBK2LYC6djuSMzqUhQ
|
|
|
7
7
|
nat/plugins/mcp/exceptions.py,sha256=EGVOnYlui8xufm8dhJyPL1SUqBLnCGOTvRoeyNcmcWE,5980
|
|
8
8
|
nat/plugins/mcp/register.py,sha256=HOT2Wl2isGuyFc7BUTi58-BbjI5-EtZMZo7stsv5pN4,831
|
|
9
9
|
nat/plugins/mcp/tool.py,sha256=xNfBIF__ugJKFEjkYEM417wWM1PpuTaCMGtSFmxHSuA,6089
|
|
10
|
-
nat/plugins/mcp/utils.py,sha256=
|
|
10
|
+
nat/plugins/mcp/utils.py,sha256=dUIig7jeKz0ctb4o38jFGbe2uvM3DMR3PSJjfN_Lr5M,9111
|
|
11
11
|
nat/plugins/mcp/auth/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
|
|
12
|
-
nat/plugins/mcp/auth/auth_flow_handler.py,sha256=
|
|
12
|
+
nat/plugins/mcp/auth/auth_flow_handler.py,sha256=v21IK3IKZ2TLEP6wO9r-sJQiilWPq7Ry40M96SAxQFA,9125
|
|
13
13
|
nat/plugins/mcp/auth/auth_provider.py,sha256=BgH66DlZgzhLDLO4cBERpHvNAmli5fMo_SCy11W9aBU,21251
|
|
14
|
-
nat/plugins/mcp/auth/auth_provider_config.py,sha256=
|
|
14
|
+
nat/plugins/mcp/auth/auth_provider_config.py,sha256=ZdiUObYU_Oj8KDDZ-JqkS6kJgup5EBy2ZREUOBw_kkI,4143
|
|
15
15
|
nat/plugins/mcp/auth/register.py,sha256=L2x69NjJPS4s6CCE5myzWVrWn3e_ttHyojmGXvBipMg,1228
|
|
16
16
|
nat/plugins/mcp/auth/token_storage.py,sha256=aS13ZvEJXcYzkZ0GSbrSor4i5bpjD5BkXHQw1iywC9k,9240
|
|
17
|
-
nvidia_nat_mcp-1.
|
|
18
|
-
nvidia_nat_mcp-1.
|
|
19
|
-
nvidia_nat_mcp-1.
|
|
20
|
-
nvidia_nat_mcp-1.
|
|
21
|
-
nvidia_nat_mcp-1.
|
|
22
|
-
nvidia_nat_mcp-1.
|
|
23
|
-
nvidia_nat_mcp-1.
|
|
17
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/licenses/LICENSE-3rd-party.txt,sha256=fOk5jMmCX9YoKWyYzTtfgl-SUy477audFC5hNY4oP7Q,284609
|
|
18
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/licenses/LICENSE.md,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
19
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/METADATA,sha256=cTjlqRzR05jFifL18XNTxYMjMLbI6rZNCQCWqB5FV54,2319
|
|
20
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/entry_points.txt,sha256=rYvUp4i-klBr3bVNh7zYOPXret704vTjvCk1qd7FooI,97
|
|
22
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
|
|
23
|
+
nvidia_nat_mcp-1.4.0a20251105.dist-info/RECORD,,
|
|
File without changes
|
{nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nvidia_nat_mcp-1.3.0a20251012.dist-info → nvidia_nat_mcp-1.4.0a20251105.dist-info}/top_level.txt
RENAMED
|
File without changes
|