Spaces:
Sleeping
Sleeping
Add MLX-LM support (CPU only) (#4)
Browse files* Add MLX-LM support (CPU only)
* Fix 401 Client Error: Unauthorized for url: https://huggingface.co/api/whoami-v2
* Add FLOWER_APPS settings (comma-separated values)
* Add CA cert download for approved participants
- blossomtune_gradio/config.py +8 -0
- blossomtune_gradio/federation.py +10 -8
- blossomtune_gradio/gradio_app.py +1 -0
- blossomtune_gradio/ui/auth.py +5 -1
- blossomtune_gradio/ui/callbacks.py +7 -2
- blossomtune_gradio/ui/components.py +5 -2
- pyproject.toml +2 -0
- tests/test_federation.py +14 -6
- uv.lock +88 -22
blossomtune_gradio/config.py
CHANGED
|
@@ -37,3 +37,11 @@ BLOSSOMTUNE_TLS_CERT_PATH = os.getenv(
|
|
| 37 |
if os.path.isdir("/data/certs")
|
| 38 |
else "./data/certs/server.crt",
|
| 39 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
if os.path.isdir("/data/certs")
|
| 38 |
else "./data/certs/server.crt",
|
| 39 |
)
|
| 40 |
+
|
| 41 |
+
# Flower Apps
|
| 42 |
+
FLOWER_APPS = os.getenv("FLOWER_APPS", ["flower_apps.quickstart_huggingface"])
|
| 43 |
+
FLOWER_APPS = (
|
| 44 |
+
FLOWER_APPS
|
| 45 |
+
if isinstance(FLOWER_APPS, list)
|
| 46 |
+
else [app.strip() for app in FLOWER_APPS.split(",")]
|
| 47 |
+
)
|
blossomtune_gradio/federation.py
CHANGED
|
@@ -45,16 +45,16 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
|
|
| 45 |
# Case 1: New user registration
|
| 46 |
if result is None:
|
| 47 |
if activation_code:
|
| 48 |
-
return (False, settings.get_text("activation_invalid_md"))
|
| 49 |
if not util.validate_email(email):
|
| 50 |
-
return (False, settings.get_text("invalid_email_md"))
|
| 51 |
|
| 52 |
with sqlite3.connect(cfg.DB_PATH) as conn:
|
| 53 |
cursor = conn.cursor()
|
| 54 |
cursor.execute("SELECT COUNT(*) FROM requests WHERE status = 'approved'")
|
| 55 |
approved_count = cursor.fetchone()[0]
|
| 56 |
if approved_count >= cfg.MAX_NUM_NODES:
|
| 57 |
-
return (False, settings.get_text("federation_full_md"))
|
| 58 |
|
| 59 |
participant_id = generate_participant_id()
|
| 60 |
new_activation_code = generate_activation_code()
|
|
@@ -73,9 +73,9 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
|
|
| 73 |
0,
|
| 74 |
),
|
| 75 |
)
|
| 76 |
-
return (True, settings.get_text("registration_submitted_md"))
|
| 77 |
else:
|
| 78 |
-
return (False, message)
|
| 79 |
|
| 80 |
# Existing user
|
| 81 |
participant_id, status, partition_id, is_activated, stored_code = result
|
|
@@ -88,9 +88,9 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
|
|
| 88 |
"UPDATE requests SET is_activated = 1 WHERE hf_handle = ?",
|
| 89 |
(pid_to_check,),
|
| 90 |
)
|
| 91 |
-
return (True, settings.get_text("activation_successful_md"))
|
| 92 |
else:
|
| 93 |
-
return (False, settings.get_text("activation_invalid_md"))
|
| 94 |
else:
|
| 95 |
if not activation_code:
|
| 96 |
return (False, settings.get_text("missing_activation_code_md"))
|
|
@@ -111,13 +111,15 @@ def check_participant_status(pid_to_check: str, email: str, activation_code: str
|
|
| 111 |
superlink_hostname=superlink_hostname,
|
| 112 |
num_partitions=num_partitions,
|
| 113 |
)
|
| 114 |
-
|
|
|
|
| 115 |
elif status == "pending":
|
| 116 |
return (False, settings.get_text("status_pending_md"))
|
| 117 |
else: # Denied
|
| 118 |
return (
|
| 119 |
False,
|
| 120 |
settings.get_text("status_denied_md", participant_id=participant_id),
|
|
|
|
| 121 |
)
|
| 122 |
|
| 123 |
|
|
|
|
| 45 |
# Case 1: New user registration
|
| 46 |
if result is None:
|
| 47 |
if activation_code:
|
| 48 |
+
return (False, settings.get_text("activation_invalid_md"), None)
|
| 49 |
if not util.validate_email(email):
|
| 50 |
+
return (False, settings.get_text("invalid_email_md"), None)
|
| 51 |
|
| 52 |
with sqlite3.connect(cfg.DB_PATH) as conn:
|
| 53 |
cursor = conn.cursor()
|
| 54 |
cursor.execute("SELECT COUNT(*) FROM requests WHERE status = 'approved'")
|
| 55 |
approved_count = cursor.fetchone()[0]
|
| 56 |
if approved_count >= cfg.MAX_NUM_NODES:
|
| 57 |
+
return (False, settings.get_text("federation_full_md"), None)
|
| 58 |
|
| 59 |
participant_id = generate_participant_id()
|
| 60 |
new_activation_code = generate_activation_code()
|
|
|
|
| 73 |
0,
|
| 74 |
),
|
| 75 |
)
|
| 76 |
+
return (True, settings.get_text("registration_submitted_md"), None)
|
| 77 |
else:
|
| 78 |
+
return (False, message, None)
|
| 79 |
|
| 80 |
# Existing user
|
| 81 |
participant_id, status, partition_id, is_activated, stored_code = result
|
|
|
|
| 88 |
"UPDATE requests SET is_activated = 1 WHERE hf_handle = ?",
|
| 89 |
(pid_to_check,),
|
| 90 |
)
|
| 91 |
+
return (True, settings.get_text("activation_successful_md"), None)
|
| 92 |
else:
|
| 93 |
+
return (False, settings.get_text("activation_invalid_md"), None)
|
| 94 |
else:
|
| 95 |
if not activation_code:
|
| 96 |
return (False, settings.get_text("missing_activation_code_md"))
|
|
|
|
| 111 |
superlink_hostname=superlink_hostname,
|
| 112 |
num_partitions=num_partitions,
|
| 113 |
)
|
| 114 |
+
# TODO: build and provide .blossomfile for download
|
| 115 |
+
return (True, connection_string, cfg.BLOSSOMTUNE_TLS_CERT_PATH)
|
| 116 |
elif status == "pending":
|
| 117 |
return (False, settings.get_text("status_pending_md"))
|
| 118 |
else: # Denied
|
| 119 |
return (
|
| 120 |
False,
|
| 121 |
settings.get_text("status_denied_md", participant_id=participant_id),
|
| 122 |
+
None,
|
| 123 |
)
|
| 124 |
|
| 125 |
|
blossomtune_gradio/gradio_app.py
CHANGED
|
@@ -32,6 +32,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Flower Superlink & Runner") as dem
|
|
| 32 |
)
|
| 33 |
check_status_btn = gr.Button("Submit Request / Activate", variant="primary")
|
| 34 |
request_status_md = components.request_status_md.render()
|
|
|
|
| 35 |
check_status_btn.click(
|
| 36 |
fn=callbacks.on_check_participant_status,
|
| 37 |
inputs=[hf_handle_tb, email_tb, activation_code_tb],
|
|
|
|
| 32 |
)
|
| 33 |
check_status_btn = gr.Button("Submit Request / Activate", variant="primary")
|
| 34 |
request_status_md = components.request_status_md.render()
|
| 35 |
+
ca_cert_download = components.ca_cert_download.render()
|
| 36 |
check_status_btn.click(
|
| 37 |
fn=callbacks.on_check_participant_status,
|
| 38 |
inputs=[hf_handle_tb, email_tb, activation_code_tb],
|
blossomtune_gradio/ui/auth.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
from huggingface_hub import whoami
|
|
|
|
| 3 |
|
| 4 |
|
| 5 |
from blossomtune_gradio import config as cfg
|
|
@@ -10,7 +11,10 @@ def is_space_owner(profile: gr.OAuthProfile | None, oauth_token: gr.OAuthToken |
|
|
| 10 |
if cfg.SPACE_OWNER is None:
|
| 11 |
return True
|
| 12 |
if oauth_token:
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
| 14 |
else:
|
| 15 |
org_names = []
|
| 16 |
return profile is not None and (
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from huggingface_hub import whoami
|
| 3 |
+
from requests.exceptions import HTTPError
|
| 4 |
|
| 5 |
|
| 6 |
from blossomtune_gradio import config as cfg
|
|
|
|
| 11 |
if cfg.SPACE_OWNER is None:
|
| 12 |
return True
|
| 13 |
if oauth_token:
|
| 14 |
+
try:
|
| 15 |
+
org_names = [org["name"] for org in whoami(oauth_token.token)["orgs"]]
|
| 16 |
+
except HTTPError:
|
| 17 |
+
return False
|
| 18 |
else:
|
| 19 |
org_names = []
|
| 20 |
return profile is not None and (
|
blossomtune_gradio/ui/callbacks.py
CHANGED
|
@@ -203,10 +203,15 @@ def on_check_participant_status(
|
|
| 203 |
email_to_add = email.strip()
|
| 204 |
activation_code_to_check = activation_code.strip()
|
| 205 |
# The federation module is responsible for getting the correct text from settings
|
| 206 |
-
_, message = fed.check_participant_status(
|
| 207 |
pid_to_check, email_to_add, activation_code_to_check
|
| 208 |
)
|
| 209 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
|
| 212 |
def on_manage_fed_request(participant_id: str, partition_id: str, action: str):
|
|
|
|
| 203 |
email_to_add = email.strip()
|
| 204 |
activation_code_to_check = activation_code.strip()
|
| 205 |
# The federation module is responsible for getting the correct text from settings
|
| 206 |
+
_, message, download = fed.check_participant_status(
|
| 207 |
pid_to_check, email_to_add, activation_code_to_check
|
| 208 |
)
|
| 209 |
+
return {
|
| 210 |
+
components.request_status_md: gr.update(value=message),
|
| 211 |
+
components.ca_cert_download: gr.update(
|
| 212 |
+
value=download or "", visible=True if download else False
|
| 213 |
+
),
|
| 214 |
+
}
|
| 215 |
|
| 216 |
|
| 217 |
def on_manage_fed_request(participant_id: str, partition_id: str, action: str):
|
blossomtune_gradio/ui/components.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
|
| 3 |
|
| 4 |
# This component's value is updated dynamically by callbacks.
|
|
@@ -47,9 +48,8 @@ runner_status_txt = gr.Textbox(
|
|
| 47 |
)
|
| 48 |
runner_toggle_btn = gr.Button("▶️ Start Federated Run", variant="primary")
|
| 49 |
runner_app_dd = gr.Dropdown(
|
| 50 |
-
|
| 51 |
label="Select Runner App",
|
| 52 |
-
value="flower_apps.quickstart_huggingface",
|
| 53 |
render=False,
|
| 54 |
)
|
| 55 |
run_id_tb = gr.Textbox(label="Run ID", placeholder="e.g., run_123")
|
|
@@ -88,3 +88,6 @@ partition_id_tb = gr.Textbox(
|
|
| 88 |
placeholder="Auto-filled on selection...",
|
| 89 |
render=False,
|
| 90 |
)
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from blossomtune_gradio import config as cfg
|
| 3 |
|
| 4 |
|
| 5 |
# This component's value is updated dynamically by callbacks.
|
|
|
|
| 48 |
)
|
| 49 |
runner_toggle_btn = gr.Button("▶️ Start Federated Run", variant="primary")
|
| 50 |
runner_app_dd = gr.Dropdown(
|
| 51 |
+
cfg.FLOWER_APPS,
|
| 52 |
label="Select Runner App",
|
|
|
|
| 53 |
render=False,
|
| 54 |
)
|
| 55 |
run_id_tb = gr.Textbox(label="Run ID", placeholder="e.g., run_123")
|
|
|
|
| 88 |
placeholder="Auto-filled on selection...",
|
| 89 |
render=False,
|
| 90 |
)
|
| 91 |
+
ca_cert_download = gr.File(
|
| 92 |
+
label="Download CA Certificate (ca.crt)", visible=False, render=False
|
| 93 |
+
)
|
pyproject.toml
CHANGED
|
@@ -70,6 +70,8 @@ convention = "google" # Accepts: "google", "numpy", or "pep257".
|
|
| 70 |
dev = [
|
| 71 |
"cryptography>=44.0.3",
|
| 72 |
"dnspython>=2.8.0",
|
|
|
|
|
|
|
| 73 |
"pytest>=8.4.1",
|
| 74 |
"pytest-mock>=3.15.1",
|
| 75 |
]
|
|
|
|
| 70 |
dev = [
|
| 71 |
"cryptography>=44.0.3",
|
| 72 |
"dnspython>=2.8.0",
|
| 73 |
+
"mlx-lm>=0.28.2",
|
| 74 |
+
"mlx[cpu]>=0.29.2",
|
| 75 |
"pytest>=8.4.1",
|
| 76 |
"pytest-mock>=3.15.1",
|
| 77 |
]
|
tests/test_federation.py
CHANGED
|
@@ -45,10 +45,11 @@ class TestCheckParticipantStatus:
|
|
| 45 |
):
|
| 46 |
"""Verify successful registration for a new user."""
|
| 47 |
mock_mail.return_value = (True, "")
|
| 48 |
-
success, message = fed.check_participant_status(
|
| 49 |
"new_user", "new@example.com", ""
|
| 50 |
)
|
| 51 |
assert success is True
|
|
|
|
| 52 |
assert message == "mock_registration_submitted_md"
|
| 53 |
|
| 54 |
# Verify the user was added to the database
|
|
@@ -58,17 +59,21 @@ class TestCheckParticipantStatus:
|
|
| 58 |
|
| 59 |
def test_new_user_invalid_email(self, in_memory_db, mock_settings):
|
| 60 |
"""Verify registration fails with an invalid email."""
|
| 61 |
-
success, message = fed.check_participant_status(
|
|
|
|
|
|
|
| 62 |
assert success is False
|
|
|
|
| 63 |
assert message == "mock_invalid_email_md"
|
| 64 |
|
| 65 |
def test_new_user_federation_full(self, in_memory_db, mock_settings, mocker):
|
| 66 |
"""Verify registration fails when the federation is full."""
|
| 67 |
mocker.patch("blossomtune_gradio.federation.cfg.MAX_NUM_NODES", 0)
|
| 68 |
-
success, message = fed.check_participant_status(
|
| 69 |
"another_user", "another@example.com", ""
|
| 70 |
)
|
| 71 |
assert success is False
|
|
|
|
| 72 |
assert message == "mock_federation_full_md"
|
| 73 |
|
| 74 |
def test_user_activation_success(self, in_memory_db, mock_settings):
|
|
@@ -81,10 +86,11 @@ class TestCheckParticipantStatus:
|
|
| 81 |
)
|
| 82 |
in_memory_db.commit()
|
| 83 |
|
| 84 |
-
success, message = fed.check_participant_status(
|
| 85 |
"test_user", "test@example.com", "ABCDEF"
|
| 86 |
)
|
| 87 |
assert success is True
|
|
|
|
| 88 |
assert message == "mock_activation_successful_md"
|
| 89 |
# Verify the user is now activated
|
| 90 |
cursor.execute(
|
|
@@ -102,10 +108,11 @@ class TestCheckParticipantStatus:
|
|
| 102 |
)
|
| 103 |
in_memory_db.commit()
|
| 104 |
|
| 105 |
-
success, message = fed.check_participant_status(
|
| 106 |
"test_user", "test@example.com", "WRONGCODE"
|
| 107 |
)
|
| 108 |
assert success is False
|
|
|
|
| 109 |
assert message == "mock_activation_invalid_md"
|
| 110 |
|
| 111 |
def test_status_check_approved(self, in_memory_db, mock_settings):
|
|
@@ -125,10 +132,11 @@ class TestCheckParticipantStatus:
|
|
| 125 |
),
|
| 126 |
)
|
| 127 |
in_memory_db.commit()
|
| 128 |
-
success, message = fed.check_participant_status(
|
| 129 |
"approved_user", "approved@example.com", "GHIJKL"
|
| 130 |
)
|
| 131 |
assert success is True
|
|
|
|
| 132 |
assert "mock_status_approved_md" in message
|
| 133 |
|
| 134 |
|
|
|
|
| 45 |
):
|
| 46 |
"""Verify successful registration for a new user."""
|
| 47 |
mock_mail.return_value = (True, "")
|
| 48 |
+
success, message, download = fed.check_participant_status(
|
| 49 |
"new_user", "new@example.com", ""
|
| 50 |
)
|
| 51 |
assert success is True
|
| 52 |
+
assert download is None
|
| 53 |
assert message == "mock_registration_submitted_md"
|
| 54 |
|
| 55 |
# Verify the user was added to the database
|
|
|
|
| 59 |
|
| 60 |
def test_new_user_invalid_email(self, in_memory_db, mock_settings):
|
| 61 |
"""Verify registration fails with an invalid email."""
|
| 62 |
+
success, message, download = fed.check_participant_status(
|
| 63 |
+
"user", "invalid-email", ""
|
| 64 |
+
)
|
| 65 |
assert success is False
|
| 66 |
+
assert download is None
|
| 67 |
assert message == "mock_invalid_email_md"
|
| 68 |
|
| 69 |
def test_new_user_federation_full(self, in_memory_db, mock_settings, mocker):
|
| 70 |
"""Verify registration fails when the federation is full."""
|
| 71 |
mocker.patch("blossomtune_gradio.federation.cfg.MAX_NUM_NODES", 0)
|
| 72 |
+
success, message, download = fed.check_participant_status(
|
| 73 |
"another_user", "another@example.com", ""
|
| 74 |
)
|
| 75 |
assert success is False
|
| 76 |
+
assert download is None
|
| 77 |
assert message == "mock_federation_full_md"
|
| 78 |
|
| 79 |
def test_user_activation_success(self, in_memory_db, mock_settings):
|
|
|
|
| 86 |
)
|
| 87 |
in_memory_db.commit()
|
| 88 |
|
| 89 |
+
success, message, download = fed.check_participant_status(
|
| 90 |
"test_user", "test@example.com", "ABCDEF"
|
| 91 |
)
|
| 92 |
assert success is True
|
| 93 |
+
assert download is None
|
| 94 |
assert message == "mock_activation_successful_md"
|
| 95 |
# Verify the user is now activated
|
| 96 |
cursor.execute(
|
|
|
|
| 108 |
)
|
| 109 |
in_memory_db.commit()
|
| 110 |
|
| 111 |
+
success, message, download = fed.check_participant_status(
|
| 112 |
"test_user", "test@example.com", "WRONGCODE"
|
| 113 |
)
|
| 114 |
assert success is False
|
| 115 |
+
assert download is None
|
| 116 |
assert message == "mock_activation_invalid_md"
|
| 117 |
|
| 118 |
def test_status_check_approved(self, in_memory_db, mock_settings):
|
|
|
|
| 132 |
),
|
| 133 |
)
|
| 134 |
in_memory_db.commit()
|
| 135 |
+
success, message, download = fed.check_participant_status(
|
| 136 |
"approved_user", "approved@example.com", "GHIJKL"
|
| 137 |
)
|
| 138 |
assert success is True
|
| 139 |
+
assert download is not None
|
| 140 |
assert "mock_status_approved_md" in message
|
| 141 |
|
| 142 |
|
uv.lock
CHANGED
|
@@ -243,6 +243,8 @@ dependencies = [
|
|
| 243 |
dev = [
|
| 244 |
{ name = "cryptography" },
|
| 245 |
{ name = "dnspython" },
|
|
|
|
|
|
|
| 246 |
{ name = "pytest" },
|
| 247 |
{ name = "pytest-mock" },
|
| 248 |
]
|
|
@@ -265,6 +267,8 @@ requires-dist = [
|
|
| 265 |
dev = [
|
| 266 |
{ name = "cryptography", specifier = ">=44.0.3" },
|
| 267 |
{ name = "dnspython", specifier = ">=2.8.0" },
|
|
|
|
|
|
|
| 268 |
{ name = "pytest", specifier = ">=8.4.1" },
|
| 269 |
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
| 270 |
]
|
|
@@ -1364,6 +1368,68 @@ wheels = [
|
|
| 1364 |
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
| 1365 |
]
|
| 1366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1367 |
[[package]]
|
| 1368 |
name = "mpmath"
|
| 1369 |
version = "1.3.0"
|
|
@@ -2782,10 +2848,10 @@ dependencies = [
|
|
| 2782 |
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" },
|
| 2783 |
]
|
| 2784 |
wheels = [
|
| 2785 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl" },
|
| 2786 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl" },
|
| 2787 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl" },
|
| 2788 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl" },
|
| 2789 |
]
|
| 2790 |
|
| 2791 |
[[package]]
|
|
@@ -2807,24 +2873,24 @@ dependencies = [
|
|
| 2807 |
{ name = "typing-extensions", marker = "sys_platform != 'darwin'" },
|
| 2808 |
]
|
| 2809 |
wheels = [
|
| 2810 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl" },
|
| 2811 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl" },
|
| 2812 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl" },
|
| 2813 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl" },
|
| 2814 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl" },
|
| 2815 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl" },
|
| 2816 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" },
|
| 2817 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" },
|
| 2818 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl" },
|
| 2819 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl" },
|
| 2820 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl" },
|
| 2821 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" },
|
| 2822 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" },
|
| 2823 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl" },
|
| 2824 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl" },
|
| 2825 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" },
|
| 2826 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" },
|
| 2827 |
-
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl" },
|
| 2828 |
]
|
| 2829 |
|
| 2830 |
[[package]]
|
|
|
|
| 243 |
dev = [
|
| 244 |
{ name = "cryptography" },
|
| 245 |
{ name = "dnspython" },
|
| 246 |
+
{ name = "mlx", extra = ["cpu"] },
|
| 247 |
+
{ name = "mlx-lm" },
|
| 248 |
{ name = "pytest" },
|
| 249 |
{ name = "pytest-mock" },
|
| 250 |
]
|
|
|
|
| 267 |
dev = [
|
| 268 |
{ name = "cryptography", specifier = ">=44.0.3" },
|
| 269 |
{ name = "dnspython", specifier = ">=2.8.0" },
|
| 270 |
+
{ name = "mlx", extras = ["cpu"], specifier = ">=0.29.2" },
|
| 271 |
+
{ name = "mlx-lm", specifier = ">=0.28.2" },
|
| 272 |
{ name = "pytest", specifier = ">=8.4.1" },
|
| 273 |
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
| 274 |
]
|
|
|
|
| 1368 |
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
| 1369 |
]
|
| 1370 |
|
| 1371 |
+
[[package]]
|
| 1372 |
+
name = "mlx"
|
| 1373 |
+
version = "0.29.2"
|
| 1374 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1375 |
+
dependencies = [
|
| 1376 |
+
{ name = "mlx-metal", marker = "sys_platform == 'darwin'" },
|
| 1377 |
+
]
|
| 1378 |
+
wheels = [
|
| 1379 |
+
{ url = "https://files.pythonhosted.org/packages/cb/f0/f57349f37cf5dd53f95127e141fc59fc435e4b6bfabba5a84c65de4d3597/mlx-0.29.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:e74965369227230374b3e8e8c8d46e209e5221a9b76bbb0fa788617e2c68f73c", size = 547581, upload-time = "2025-09-26T22:21:39.24Z" },
|
| 1380 |
+
{ url = "https://files.pythonhosted.org/packages/66/04/e016ca28dc9e0738a2541581420125cfe6bba24466a64420600bdd6fd52c/mlx-0.29.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0f79194eeac78e85b96439d3bbc17aae5aba045a2af083c000b4fbbc501f253e", size = 547581, upload-time = "2025-09-26T22:21:47.706Z" },
|
| 1381 |
+
{ url = "https://files.pythonhosted.org/packages/1f/b3/e2595e70ef8d4438dff694857745b0e108911e5b5fb83259dde6e5dc5bd1/mlx-0.29.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:33bbbb0fd24895d5ff080bb4d10e3e77017bba675d9a12466c8866eaf9b47854", size = 547578, upload-time = "2025-09-26T22:21:22.041Z" },
|
| 1382 |
+
{ url = "https://files.pythonhosted.org/packages/11/8c/5d51543ab128c2dff5e4b44ca799db8db5aa4f4ffc34af6531fd73627b54/mlx-0.29.2-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:32e159f2772be893bec580d2d50c0e6b32ad71a19ded7307bf6c871c8aaa9cf2", size = 651900, upload-time = "2025-09-26T22:26:11.133Z" },
|
| 1383 |
+
{ url = "https://files.pythonhosted.org/packages/f3/84/7250237039e91d8e44ca0cf3522f189164844c196f262509afd29ef54710/mlx-0.29.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:eec950bf7118ad0865d0fc4686bd85d99bf8463fc717d836a5132e1a08b4f129", size = 548336, upload-time = "2025-09-26T22:21:44.914Z" },
|
| 1384 |
+
{ url = "https://files.pythonhosted.org/packages/13/47/428ac8d9b0cb5c136e5ce6c726cfdd55caa5b9497dafb6221acfee18f145/mlx-0.29.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bef7333268d6d02e50a9ac6b10f661b711cd02da4a5e2d7619cf198a7e530308", size = 548334, upload-time = "2025-09-26T22:21:21.41Z" },
|
| 1385 |
+
{ url = "https://files.pythonhosted.org/packages/14/f0/7d5d3527ca3fdc664c900b4b822028691739e58c8e8f7975b33df4d3536e/mlx-0.29.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:f622fc6a84542a08ad2136e9251822d2c08106e5a1a0bd5d249a2d72bccd6577", size = 548330, upload-time = "2025-09-26T22:21:41.182Z" },
|
| 1386 |
+
{ url = "https://files.pythonhosted.org/packages/09/18/e202e0f6232822f6768995cdbf50eda202137bb6547368f6e3993dbee00b/mlx-0.29.2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:a1aa1aee8e1b6bd1e51361e6b692c70d281b8187b2e859e70ecc11daab306dac", size = 648728, upload-time = "2025-09-26T22:25:49.159Z" },
|
| 1387 |
+
{ url = "https://files.pythonhosted.org/packages/a0/9a/91f6f5d031f109fa8c00ba9dd4f7a3fc42e1097a57c26783ce000069c264/mlx-0.29.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:05ea54173f4bde11b2c93e673d65d72523f5d850f5112d3874156a6fc74ca591", size = 548297, upload-time = "2025-09-26T22:21:41.991Z" },
|
| 1388 |
+
{ url = "https://files.pythonhosted.org/packages/2b/2d/dae7ca0b7fa68c6c1f2b896dfe1b8060647f144d5c5da2d53388e38809b1/mlx-0.29.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:199dd029b5e55b6d94f1ce366d0137824e46e4333891424dd00413c739f50ae9", size = 548305, upload-time = "2025-09-26T22:21:41.083Z" },
|
| 1389 |
+
{ url = "https://files.pythonhosted.org/packages/b1/56/f02f5c9e1fc11c020982501a763fa92b497ea50671a587760543987ba8c8/mlx-0.29.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:b6dd4e5f227414882b1676d99250d99389228d1bdc14e4e4e88c95d4903810b7", size = 548302, upload-time = "2025-09-26T22:21:30.546Z" },
|
| 1390 |
+
{ url = "https://files.pythonhosted.org/packages/4b/b4/b61eeb92c424947675492dec3a411bdbeae307dfd78162d65ab47e8c3b4f/mlx-0.29.2-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:c3b9a9aee13f346d060966472954eebe99d9f1b295c9a237c9a000f1ef9adf2c", size = 648709, upload-time = "2025-09-26T22:26:03.452Z" },
|
| 1391 |
+
]
|
| 1392 |
+
|
| 1393 |
+
[package.optional-dependencies]
|
| 1394 |
+
cpu = [
|
| 1395 |
+
{ name = "mlx-cpu", marker = "sys_platform == 'linux'" },
|
| 1396 |
+
]
|
| 1397 |
+
|
| 1398 |
+
[[package]]
|
| 1399 |
+
name = "mlx-cpu"
|
| 1400 |
+
version = "0.29.2"
|
| 1401 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1402 |
+
wheels = [
|
| 1403 |
+
{ url = "https://files.pythonhosted.org/packages/3d/3a/5f2246e0f68477c141b76da3afaabf0b7b8565eba12451482b3b164df762/mlx_cpu-0.29.2-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:209f6f587ee4bbff3fd18e5201e150880812cef39e1af2c102d7e8e681f06749", size = 10170075, upload-time = "2025-09-26T22:28:40.523Z" },
|
| 1404 |
+
]
|
| 1405 |
+
|
| 1406 |
+
[[package]]
|
| 1407 |
+
name = "mlx-lm"
|
| 1408 |
+
version = "0.28.2"
|
| 1409 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1410 |
+
dependencies = [
|
| 1411 |
+
{ name = "jinja2" },
|
| 1412 |
+
{ name = "mlx" },
|
| 1413 |
+
{ name = "numpy" },
|
| 1414 |
+
{ name = "protobuf" },
|
| 1415 |
+
{ name = "pyyaml" },
|
| 1416 |
+
{ name = "transformers" },
|
| 1417 |
+
]
|
| 1418 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1c/d7/fdde445c7bd443a2ed23badda6064f1477c4051543922106f365e94082cd/mlx_lm-0.28.2.tar.gz", hash = "sha256:d28752635ed5c89ff2b41361916c928e6b16f765c07b2908044e1dcaf921ed9b", size = 209374, upload-time = "2025-10-02T14:23:57.497Z" }
|
| 1419 |
+
wheels = [
|
| 1420 |
+
{ url = "https://files.pythonhosted.org/packages/f2/1c/89e0f60d45e364de8507065f73aeb8d2fd810d6cb95a9a512880b09399d5/mlx_lm-0.28.2-py3-none-any.whl", hash = "sha256:1501529e625d0d648216f7bb543b8b449d5fd17bd598f635536dbc1fbde6d1d6", size = 284600, upload-time = "2025-10-02T14:23:56.395Z" },
|
| 1421 |
+
]
|
| 1422 |
+
|
| 1423 |
+
[[package]]
|
| 1424 |
+
name = "mlx-metal"
|
| 1425 |
+
version = "0.29.2"
|
| 1426 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1427 |
+
wheels = [
|
| 1428 |
+
{ url = "https://files.pythonhosted.org/packages/31/a5/a045006546fed791f6e9a74ed4451dac871d3c35f9e54a3a25d820668a85/mlx_metal-0.29.2-py3-none-macosx_13_0_arm64.whl", hash = "sha256:cf8f83a521e620357185c57945142718d526b9312ee112e5a89eb5600480f4d6", size = 35056194, upload-time = "2025-09-26T22:23:47.201Z" },
|
| 1429 |
+
{ url = "https://files.pythonhosted.org/packages/4c/8c/4bdd3a7d04ed477b32aec30d30236dfca9f9ac27706cb309511278ddd281/mlx_metal-0.29.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:fa944001970813b296e8aff5616f2fa9daeda6bc1d190c17fbe8a7ca838ecef0", size = 34791708, upload-time = "2025-09-26T22:23:30.599Z" },
|
| 1430 |
+
{ url = "https://files.pythonhosted.org/packages/b1/11/12e158848fe4d3316c999ffb6c2d88f554bde98d69022b3385e25ece997e/mlx_metal-0.29.2-py3-none-macosx_15_0_arm64.whl", hash = "sha256:08d8b7fe305425a14b74ebf36cee176575bfd4cd8d34a2aaae8f05b9983d2d71", size = 34784506, upload-time = "2025-09-26T22:23:29.207Z" },
|
| 1431 |
+
]
|
| 1432 |
+
|
| 1433 |
[[package]]
|
| 1434 |
name = "mpmath"
|
| 1435 |
version = "1.3.0"
|
|
|
|
| 2848 |
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" },
|
| 2849 |
]
|
| 2850 |
wheels = [
|
| 2851 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122" },
|
| 2852 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
|
| 2853 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
|
| 2854 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
|
| 2855 |
]
|
| 2856 |
|
| 2857 |
[[package]]
|
|
|
|
| 2873 |
{ name = "typing-extensions", marker = "sys_platform != 'darwin'" },
|
| 2874 |
]
|
| 2875 |
wheels = [
|
| 2876 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6" },
|
| 2877 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8" },
|
| 2878 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb" },
|
| 2879 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:7631ef49fbd38d382909525b83696dc12a55d68492ade4ace3883c62b9fc140f" },
|
| 2880 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:41e6fc5ec0914fcdce44ccf338b1d19a441b55cafdd741fd0bf1af3f9e4cfd14" },
|
| 2881 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
|
| 2882 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
|
| 2883 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
|
| 2884 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
|
| 2885 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
|
| 2886 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
|
| 2887 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
|
| 2888 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
|
| 2889 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
|
| 2890 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
|
| 2891 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
|
| 2892 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
|
| 2893 |
+
{ url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
|
| 2894 |
]
|
| 2895 |
|
| 2896 |
[[package]]
|