mirror of
https://github.com/dnlbauer/cordra-mcp.git
synced 2026-09-10 13:45:30 +00:00
feat: replace CordraPy with http requests
This commit is contained in:
@@ -10,11 +10,9 @@ license = "MIT"
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mcp>=1.2.0",
|
"mcp>=1.2.0",
|
||||||
"httpx>=0.25.0",
|
|
||||||
"pydantic>=2.0.0",
|
"pydantic>=2.0.0",
|
||||||
"pydantic-settings>=2.0.0",
|
"pydantic-settings>=2.0.0",
|
||||||
"cordrapy",
|
"requests>=2.25.0",
|
||||||
"setuptools>=80.9.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -56,8 +54,6 @@ asyncio_mode = "auto"
|
|||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
addopts = "-v"
|
addopts = "-v"
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
cordrapy = { git = "https://github.com/usnistgov/CordraPy.git" }
|
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
"""Cordra client wrapper using CordraPy."""
|
"""Cordra client wrapper using HTTP requests."""
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import cordra
|
import requests
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from .config import CordraConfig
|
from .config import CordraConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DigitalObject(BaseModel):
|
class DigitalObject(BaseModel):
|
||||||
"""Model for a Cordra digital object."""
|
"""Model for a Cordra digital object."""
|
||||||
@@ -30,7 +33,7 @@ class CordraNotFoundError(CordraClientError):
|
|||||||
|
|
||||||
|
|
||||||
class CordraClient:
|
class CordraClient:
|
||||||
"""Client for interacting with Cordra repository."""
|
"""Client for interacting with Cordra repository using HTTP requests."""
|
||||||
|
|
||||||
def __init__(self, config: CordraConfig) -> None:
|
def __init__(self, config: CordraConfig) -> None:
|
||||||
"""Initialize the Cordra client.
|
"""Initialize the Cordra client.
|
||||||
@@ -39,6 +42,14 @@ class CordraClient:
|
|||||||
config: Configuration settings for the Cordra connection
|
config: Configuration settings for the Cordra connection
|
||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.verify = config.verify_ssl
|
||||||
|
|
||||||
|
# Set up authentication
|
||||||
|
if config.username and config.password:
|
||||||
|
self.session.auth = (config.username, config.password)
|
||||||
|
elif config.username or config.password:
|
||||||
|
logger.warning("Only username or password provided, not both. Authentication may fail.")
|
||||||
|
|
||||||
async def get_object(self, object_id: str) -> DigitalObject:
|
async def get_object(self, object_id: str) -> DigitalObject:
|
||||||
"""Retrieve a digital object by its ID.
|
"""Retrieve a digital object by its ID.
|
||||||
@@ -54,28 +65,34 @@ class CordraClient:
|
|||||||
CordraClientError: For other API errors
|
CordraClientError: For other API errors
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cordra_obj: dict[str, Any] = cordra.CordraObject.read(
|
# Build URL: cordra_base_url/objects/prefix/postfix
|
||||||
host=self.config.cordra_url, #type: ignore
|
url = f"{self.config.cordra_url}/objects/{object_id}"
|
||||||
obj_id=object_id,
|
|
||||||
username=self.config.username,
|
# Add full=true parameter to get complete object details
|
||||||
password=self.config.password,
|
params = {"full": "true"}
|
||||||
verify=self.config.verify_ssl,
|
|
||||||
full=True # Get full object details including metadata, paylods, etc.
|
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
||||||
)
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
raise CordraNotFoundError(f"Object not found: {object_id}")
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
cordra_obj = response.json()
|
||||||
|
|
||||||
return DigitalObject(
|
return DigitalObject(
|
||||||
id=object_id,
|
id=object_id,
|
||||||
type=cordra_obj['type'],
|
type=cordra_obj.get('type', ''),
|
||||||
content=cordra_obj['content'],
|
content=cordra_obj.get('content', cordra_obj),
|
||||||
metadata=cordra_obj.get('metadata'),
|
metadata=cordra_obj.get('metadata'),
|
||||||
acl=cordra_obj.get('acl'),
|
acl=cordra_obj.get('acl'),
|
||||||
payloads=cordra_obj.get('payloads'),
|
payloads=cordra_obj.get('payloads'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except CordraNotFoundError:
|
||||||
|
raise
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e).lower()
|
|
||||||
if 'not found' in error_msg or '404' in error_msg:
|
|
||||||
raise CordraNotFoundError(f"Object not found: {object_id}") from e
|
|
||||||
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
|
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
|
||||||
|
|
||||||
async def find(self, query: str) -> list[dict[str, Any]]:
|
async def find(self, query: str) -> list[dict[str, Any]]:
|
||||||
@@ -91,19 +108,22 @@ class CordraClient:
|
|||||||
CordraClientError: If there's an API error
|
CordraClientError: If there's an API error
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Use CordraPy to find objects
|
# Use HTTP GET request to search endpoint
|
||||||
# TODO - need to handle pagination, but the CordraPy API does not support it.
|
url = f"{self.config.cordra_url}/search"
|
||||||
response: dict[str, Any] = cordra.CordraObject.find(
|
params = {"query": query}
|
||||||
self.config.cordra_url, # type: ignore
|
|
||||||
query
|
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
||||||
)
|
response.raise_for_status()
|
||||||
|
|
||||||
|
search_result = response.json()
|
||||||
|
|
||||||
# Extract the results array from the response
|
# Extract the results array from the response
|
||||||
if isinstance(response, dict) and 'results' in response:
|
if isinstance(search_result, dict) and 'results' in search_result:
|
||||||
return response['results']
|
return search_result['results']
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except requests.RequestException as e:
|
||||||
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e
|
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e
|
||||||
|
except Exception as e:
|
||||||
|
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e
|
||||||
@@ -59,8 +59,8 @@ async def list_cordra_schemas() -> str:
|
|||||||
# Extract the names from the schema objects
|
# Extract the names from the schema objects
|
||||||
schema_names = []
|
schema_names = []
|
||||||
for schema in schemas:
|
for schema in schemas:
|
||||||
if isinstance(schema, dict) and 'name' in schema:
|
if isinstance(schema, dict) and 'content' in schema and 'name' in schema['content']:
|
||||||
schema_names.append(schema['name'])
|
schema_names.append(schema['content']['name'])
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"schemas": schema_names,
|
"schemas": schema_names,
|
||||||
|
|||||||
@@ -112,10 +112,13 @@ class TestCordraClient:
|
|||||||
client = CordraClient(config)
|
client = CordraClient(config)
|
||||||
assert client.config == config
|
assert client.config == config
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.read')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_get_object_success(self, mock_read, client, mock_cordra_object):
|
async def test_get_object_success(self, mock_get, client, mock_cordra_object):
|
||||||
"""Test successful object retrieval."""
|
"""Test successful object retrieval."""
|
||||||
mock_read.return_value = mock_cordra_object
|
mock_response = mock_get.return_value
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_cordra_object
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
|
||||||
result = await client.get_object("test/123")
|
result = await client.get_object("test/123")
|
||||||
|
|
||||||
@@ -127,39 +130,38 @@ class TestCordraClient:
|
|||||||
assert result.acl == {"read": ["public"], "write": ["admin"]}
|
assert result.acl == {"read": ["public"], "write": ["admin"]}
|
||||||
assert result.payloads and len(result.payloads) == 2
|
assert result.payloads and len(result.payloads) == 2
|
||||||
|
|
||||||
mock_read.assert_called_once_with(
|
mock_get.assert_called_once_with(
|
||||||
host="https://test.example.com",
|
"https://test.example.com/objects/test/123",
|
||||||
obj_id="test/123",
|
params={"full": "true"},
|
||||||
username="testuser",
|
timeout=30
|
||||||
password="testpass",
|
|
||||||
verify=False,
|
|
||||||
full=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.read')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_get_object_not_found(self, mock_read, client):
|
async def test_get_object_not_found(self, mock_get, client):
|
||||||
"""Test object not found exception."""
|
"""Test object not found exception."""
|
||||||
mock_read.side_effect = Exception("Object not found")
|
mock_response = mock_get.return_value
|
||||||
|
mock_response.status_code = 404
|
||||||
|
|
||||||
with pytest.raises(CordraNotFoundError) as exc_info:
|
with pytest.raises(CordraNotFoundError) as exc_info:
|
||||||
await client.get_object("test/nonexistent")
|
await client.get_object("test/nonexistent")
|
||||||
|
|
||||||
assert "Object not found: test/nonexistent" in str(exc_info.value)
|
assert "Object not found: test/nonexistent" in str(exc_info.value)
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.read')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_get_object_general_error(self, mock_read, client):
|
async def test_get_object_general_error(self, mock_get, client):
|
||||||
"""Test general error handling."""
|
"""Test general error handling."""
|
||||||
mock_read.side_effect = Exception("Connection failed")
|
from requests import RequestException
|
||||||
|
mock_get.side_effect = RequestException("Connection failed")
|
||||||
|
|
||||||
with pytest.raises(CordraClientError) as exc_info:
|
with pytest.raises(CordraClientError) as exc_info:
|
||||||
await client.get_object("test/123")
|
await client.get_object("test/123")
|
||||||
|
|
||||||
assert "Failed to retrieve object test/123" in str(exc_info.value)
|
assert "Failed to retrieve object test/123" in str(exc_info.value)
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.find')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_find_success(self, mock_find, client):
|
async def test_find_success(self, mock_get, client):
|
||||||
"""Test successful find operation."""
|
"""Test successful find operation."""
|
||||||
mock_response = {
|
mock_response_data = {
|
||||||
"results": [
|
"results": [
|
||||||
{"name": "User", "identifier": "test/user-schema"},
|
{"name": "User", "identifier": "test/user-schema"},
|
||||||
{"name": "Project", "identifier": "test/project-schema"},
|
{"name": "Project", "identifier": "test/project-schema"},
|
||||||
@@ -167,7 +169,10 @@ class TestCordraClient:
|
|||||||
],
|
],
|
||||||
"size": 3
|
"size": 3
|
||||||
}
|
}
|
||||||
mock_find.return_value = mock_response
|
mock_response = mock_get.return_value
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_response_data
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
|
||||||
result = await client.find("type:Schema")
|
result = await client.find("type:Schema")
|
||||||
|
|
||||||
@@ -176,39 +181,48 @@ class TestCordraClient:
|
|||||||
assert result[1]["name"] == "Project"
|
assert result[1]["name"] == "Project"
|
||||||
assert result[2]["name"] == "Document"
|
assert result[2]["name"] == "Document"
|
||||||
|
|
||||||
mock_find.assert_called_once_with(
|
mock_get.assert_called_once_with(
|
||||||
client.config.cordra_url,
|
"https://test.example.com/search",
|
||||||
"type:Schema"
|
params={"query": "type:Schema"},
|
||||||
|
timeout=30
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.find')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_find_empty_results(self, mock_find, client):
|
async def test_find_empty_results(self, mock_get, client):
|
||||||
"""Test find with empty results."""
|
"""Test find with empty results."""
|
||||||
mock_response = {"results": [], "size": 0}
|
mock_response_data = {"results": [], "size": 0}
|
||||||
mock_find.return_value = mock_response
|
mock_response = mock_get.return_value
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_response_data
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
|
||||||
result = await client.find("type:NonExistent")
|
result = await client.find("type:NonExistent")
|
||||||
|
|
||||||
assert result == []
|
assert result == []
|
||||||
mock_find.assert_called_once_with(
|
mock_get.assert_called_once_with(
|
||||||
client.config.cordra_url,
|
"https://test.example.com/search",
|
||||||
"type:NonExistent"
|
params={"query": "type:NonExistent"},
|
||||||
|
timeout=30
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.find')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_find_no_results_key(self, mock_find, client):
|
async def test_find_no_results_key(self, mock_get, client):
|
||||||
"""Test find with response missing results key."""
|
"""Test find with response missing results key."""
|
||||||
mock_response = {"size": 0} # No results key
|
mock_response_data = {"size": 0} # No results key
|
||||||
mock_find.return_value = mock_response
|
mock_response = mock_get.return_value
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = mock_response_data
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
|
||||||
result = await client.find("type:Schema")
|
result = await client.find("type:Schema")
|
||||||
|
|
||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
@patch('mcp_cordra.client.cordra.CordraObject.find')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_find_error(self, mock_find, client):
|
async def test_find_error(self, mock_get, client):
|
||||||
"""Test find error handling."""
|
"""Test find error handling."""
|
||||||
mock_find.side_effect = Exception("Search failed")
|
from requests import RequestException
|
||||||
|
mock_get.side_effect = RequestException("Search failed")
|
||||||
|
|
||||||
with pytest.raises(CordraClientError) as exc_info:
|
with pytest.raises(CordraClientError) as exc_info:
|
||||||
await client.find("invalid:query")
|
await client.find("invalid:query")
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ class TestListCordraSchemas:
|
|||||||
async def test_list_schemas_success(self, mock_client):
|
async def test_list_schemas_success(self, mock_client):
|
||||||
"""Test successful schema listing."""
|
"""Test successful schema listing."""
|
||||||
mock_schemas = [
|
mock_schemas = [
|
||||||
{"name": "User", "identifier": "test/user-schema"},
|
{"content": {"name": "User"}, "identifier": "test/user-schema"},
|
||||||
{"name": "Project", "identifier": "test/project-schema"},
|
{"content": {"name": "Project"}, "identifier": "test/project-schema"},
|
||||||
{"name": "Document", "identifier": "test/doc-schema"},
|
{"content": {"name": "Document"}, "identifier": "test/doc-schema"},
|
||||||
{"name": "CaptureEvent", "identifier": "test/capture-schema"}
|
{"content": {"name": "CaptureEvent"}, "identifier": "test/capture-schema"}
|
||||||
]
|
]
|
||||||
mock_client.find = AsyncMock(return_value=mock_schemas)
|
mock_client.find = AsyncMock(return_value=mock_schemas)
|
||||||
|
|
||||||
@@ -193,10 +193,10 @@ class TestListCordraSchemas:
|
|||||||
async def test_list_schemas_missing_name_field(self, mock_client):
|
async def test_list_schemas_missing_name_field(self, mock_client):
|
||||||
"""Test schema listing with objects missing name field."""
|
"""Test schema listing with objects missing name field."""
|
||||||
mock_schemas = [
|
mock_schemas = [
|
||||||
{"name": "User", "identifier": "test/user-schema"},
|
{"content": {"name": "User"}, "identifier": "test/user-schema"},
|
||||||
{"identifier": "test/no-name-schema"}, # Missing name field
|
{"content": {}, "identifier": "test/no-name-schema"}, # Missing name field
|
||||||
{"name": "Project", "identifier": "test/project-schema"},
|
{"content": {"name": "Project"}, "identifier": "test/project-schema"},
|
||||||
{"other": "field"} # No name or identifier
|
{"content": {"other": "field"}} # No name or identifier
|
||||||
]
|
]
|
||||||
mock_client.find = AsyncMock(return_value=mock_schemas)
|
mock_client.find = AsyncMock(return_value=mock_schemas)
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ class TestListCordraSchemas:
|
|||||||
async def test_list_schemas_json_format(self, mock_client):
|
async def test_list_schemas_json_format(self, mock_client):
|
||||||
"""Test that the returned JSON is properly formatted."""
|
"""Test that the returned JSON is properly formatted."""
|
||||||
mock_schemas = [
|
mock_schemas = [
|
||||||
{"name": "TestSchema", "identifier": "test/schema"}
|
{"content": {"name": "TestSchema"}, "identifier": "test/schema"}
|
||||||
]
|
]
|
||||||
mock_client.find = AsyncMock(return_value=mock_schemas)
|
mock_client.find = AsyncMock(return_value=mock_schemas)
|
||||||
|
|
||||||
|
|||||||
25
uv.lock
generated
25
uv.lock
generated
@@ -112,14 +112,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cordrapy"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = { git = "https://github.com/usnistgov/CordraPy.git#7d73f3f58461a2dc7540970fe7defaa361b61740" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "requests" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.9.1"
|
version = "7.9.1"
|
||||||
@@ -309,12 +301,10 @@ name = "mcp-cordra"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cordrapy" },
|
|
||||||
{ name = "httpx" },
|
|
||||||
{ name = "mcp" },
|
{ name = "mcp" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "setuptools" },
|
{ name = "requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -334,8 +324,6 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "cordrapy", git = "https://github.com/usnistgov/CordraPy.git" },
|
|
||||||
{ name = "httpx", specifier = ">=0.25.0" },
|
|
||||||
{ name = "loguru", marker = "extra == 'dev'", specifier = ">=0.7.0" },
|
{ name = "loguru", marker = "extra == 'dev'", specifier = ">=0.7.0" },
|
||||||
{ name = "mcp", specifier = ">=1.2.0" },
|
{ name = "mcp", specifier = ">=1.2.0" },
|
||||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.6.0" },
|
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.6.0" },
|
||||||
@@ -344,8 +332,8 @@ requires-dist = [
|
|||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
||||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
|
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
|
||||||
|
{ name = "requests", specifier = ">=2.25.0" },
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
||||||
{ name = "setuptools", specifier = ">=80.9.0" },
|
|
||||||
]
|
]
|
||||||
provides-extras = ["dev"]
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
@@ -711,15 +699,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209 },
|
{ url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "setuptools"
|
|
||||||
version = "80.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user