diff --git a/pyproject.toml b/pyproject.toml index d160c4b..ca58e6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [ "fastapi-mcp>=0.4.0", "gunicorn>=23.0", "httpx>=0.28.1", + "httpx-kerberos>=0.1.2", "loguru>=0.7.3", "pydantic>=2.13.4", "pydantic-core>=2.46.4", diff --git a/spark_executor/core/yarn_client.py b/spark_executor/core/yarn_client.py index d574fc7..5a0b8e4 100644 --- a/spark_executor/core/yarn_client.py +++ b/spark_executor/core/yarn_client.py @@ -21,6 +21,7 @@ import json from dataclasses import dataclass import httpx +import httpx_kerberos from common.config import settings from common.logging import logger @@ -47,6 +48,11 @@ class YarnClientConfig: yarn_rm_url: str | None ssl_verify: bool | None = None ssl_ca_bundle: str | None = None + auth_type: str = "none" + auth_user: str | None = None + auth_password: str | None = None + auth_principal: str | None = None # display/audit only for kerberos + auth_keytab: str | None = None # display/audit only for kerberos @classmethod def from_connection(cls, conn: Connection) -> "YarnClientConfig": @@ -54,6 +60,11 @@ class YarnClientConfig: yarn_rm_url=conn.yarn_rm_url, ssl_verify=conn.ssl_verify, ssl_ca_bundle=conn.ssl_ca_bundle, + auth_type=conn.auth_type, + auth_user=conn.auth_user, + auth_password=conn.auth_password, + auth_principal=conn.auth_principal, + auth_keytab=conn.auth_keytab, ) def verify_for_httpx(self) -> bool | str: @@ -70,6 +81,18 @@ class YarnClientConfig: return self.ssl_verify return settings.ssl_verify_default + def auth_for_httpx(self) -> httpx.Auth | None: + """Return the httpx.Auth object implied by this config, if any.""" + if self.auth_type in ("none", "simple"): + return None + if self.auth_type == "basic": + if not self.auth_user: + raise YarnConfigError("auth_type='basic' requires auth_user") + return httpx.BasicAuth(self.auth_user, self.auth_password or "") + if self.auth_type == "kerberos": + return httpx_kerberos.HTTPKerberosAuth() + raise YarnConfigError(f"Unknown auth_type: {self.auth_type!r}") + def _base_url(yarn_rm_url: str | None) -> str: """Resolve and validate the RM URL. Raises YarnConfigError if unusable.""" @@ -89,10 +112,11 @@ def _base_url(yarn_rm_url: str | None) -> str: def _request(method: str, url: str, *, json_body: dict | None = None, - timeout: float = 30.0, verify: bool | str = True) -> httpx.Response: + timeout: float = 30.0, verify: bool | str = True, + auth: httpx.Auth | None = None) -> httpx.Response: logger.debug(f"YARN {method} {url}" + (f" body={json_body}" if json_body else "")) try: - resp = httpx.request(method, url, json=json_body, timeout=timeout, verify=verify) + resp = httpx.request(method, url, json=json_body, timeout=timeout, verify=verify, auth=auth) except httpx.HTTPError as exc: logger.error(f"YARN {method} {url} failed: {exc}") raise YarnError(f"YARN connection failed: {exc}") from exc @@ -106,7 +130,7 @@ def _request(method: str, url: str, *, json_body: dict | None = None, def get_application_status(application_id: str, config: YarnClientConfig) -> tuple[str, str]: """Return (state, raw_json_text) for an application, or raise YarnError.""" url = f"{_base_url(config.yarn_rm_url)}/ws/v1/cluster/apps/{application_id}" - resp = _request("GET", url, verify=config.verify_for_httpx()) + resp = _request("GET", url, verify=config.verify_for_httpx(), auth=config.auth_for_httpx()) if resp.status_code == 404: raise YarnError(f"YARN application {application_id!r} not found") if resp.status_code >= 400: @@ -140,7 +164,7 @@ def _fetch_logs_via_nodemanager(application_id: str, config: YarnClientConfig) - # Latest (or all) app attempts. attempts_url = f"{app_url}/appattempts" - resp = _request("GET", attempts_url, timeout=30.0, verify=config.verify_for_httpx()) + resp = _request("GET", attempts_url, timeout=30.0, verify=config.verify_for_httpx(), auth=config.auth_for_httpx()) if resp.status_code >= 400: raise _logs_unavailable_error(application_id) attempts = resp.json().get("appAttempts", {}).get("appAttempt", []) @@ -154,7 +178,7 @@ def _fetch_logs_via_nodemanager(application_id: str, config: YarnClientConfig) - if not attempt_id: continue containers_url = f"{app_url}/appattempts/{attempt_id}/containers" - resp = _request("GET", containers_url, timeout=30.0, verify=config.verify_for_httpx()) + resp = _request("GET", containers_url, timeout=30.0, verify=config.verify_for_httpx(), auth=config.auth_for_httpx()) if resp.status_code >= 400: raise _logs_unavailable_error(application_id) containers.extend(resp.json().get("containers", {}).get("container", [])) @@ -171,7 +195,7 @@ def _fetch_logs_via_nodemanager(application_id: str, config: YarnClientConfig) - continue scheme = "https" if config.yarn_rm_url and config.yarn_rm_url.startswith("https://") else "http" nm_url = f"{scheme}://{node_http_address}/node/containerlogs/{container_id}/{user}/" - resp = _request("GET", nm_url, timeout=60.0, verify=config.verify_for_httpx()) + resp = _request("GET", nm_url, timeout=60.0, verify=config.verify_for_httpx(), auth=config.auth_for_httpx()) if resp.status_code >= 400: raise _logs_unavailable_error(application_id) parts.append(f"=== container: {container_id} ===\n{resp.text}") @@ -184,7 +208,7 @@ def _fetch_logs_via_nodemanager(application_id: str, config: YarnClientConfig) - def get_application_logs(application_id: str, config: YarnClientConfig) -> str: """Return aggregated container logs for an application as text.""" url = f"{_base_url(config.yarn_rm_url)}/ws/v1/cluster/apps/{application_id}/aggregated-logs" - resp = _request("GET", url, timeout=60.0, verify=config.verify_for_httpx()) + resp = _request("GET", url, timeout=60.0, verify=config.verify_for_httpx(), auth=config.auth_for_httpx()) if resp.status_code in (404, 501): return _fetch_logs_via_nodemanager(application_id, config) if resp.status_code >= 400: @@ -197,7 +221,7 @@ def get_application_logs(application_id: str, config: YarnClientConfig) -> str: def kill_application(application_id: str, config: YarnClientConfig) -> None: """PUT state=KILLED to /ws/v1/cluster/apps/{appid}/state.""" url = f"{_base_url(config.yarn_rm_url)}/ws/v1/cluster/apps/{application_id}/state" - resp = _request("PUT", url, json_body={"state": "KILLED"}, verify=config.verify_for_httpx()) + resp = _request("PUT", url, json_body={"state": "KILLED"}, verify=config.verify_for_httpx(), auth=config.auth_for_httpx()) if resp.status_code >= 400: logger.error(f"YARN PUT {url} -> {resp.status_code}: {resp.text[:500]}") raise YarnError(f"YARN kill returned HTTP {resp.status_code}: {resp.text}") diff --git a/spark_executor/models.py b/spark_executor/models.py index b22f5bb..50b05eb 100644 --- a/spark_executor/models.py +++ b/spark_executor/models.py @@ -53,6 +53,13 @@ class Connection(BaseModel): # overrides the global default for this connection. ssl_verify: bool | None = None ssl_ca_bundle: str | None = None + # Authentication for YARN REST calls. + auth_type: str = "none" # "none" | "simple" | "basic" | "kerberos" + auth_user: str | None = None + auth_password: str | None = None + # Display/audit only for kerberos; actual SPNEGO uses the system cache. + auth_principal: str | None = None + auth_keytab: str | None = None @field_validator("master") @classmethod @@ -67,6 +74,15 @@ class Connection(BaseModel): f"or 'local[/N]'; got {v!r}" ) + @field_validator("auth_type") + @classmethod + def _check_auth_type(cls, v: str) -> str: + if v not in {"none", "simple", "basic", "kerberos"}: + raise ValueError( + f"auth_type must be one of none/simple/basic/kerberos; got {v!r}" + ) + return v + class PendingSubmission(BaseModel): pending_id: str diff --git a/spark_executor/tools/connections.py b/spark_executor/tools/connections.py index c29f91a..ae381f8 100644 --- a/spark_executor/tools/connections.py +++ b/spark_executor/tools/connections.py @@ -17,6 +17,11 @@ def save_connection( spark_conf: dict[str, str] | None = None, ssl_verify: bool | None = None, ssl_ca_bundle: str | None = None, + auth_type: str = "none", + auth_user: str | None = None, + auth_password: str | None = None, + auth_principal: str | None = None, + auth_keytab: str | None = None, ) -> dict[str, str]: logger.debug( f"save_connection enter name={name} master={master} deploy_mode={deploy_mode} " @@ -30,6 +35,11 @@ def save_connection( spark_conf=spark_conf or {}, ssl_verify=ssl_verify, ssl_ca_bundle=ssl_ca_bundle, + auth_type=auth_type, + auth_user=auth_user, + auth_password=auth_password, + auth_principal=auth_principal, + auth_keytab=auth_keytab, ) store.save(conn) return {"name": name, "status": "SAVED"} diff --git a/spark_executor/tools/requests.py b/spark_executor/tools/requests.py index d58cb4c..104db0a 100644 --- a/spark_executor/tools/requests.py +++ b/spark_executor/tools/requests.py @@ -24,6 +24,11 @@ class SaveConnectionRequest(BaseModel): spark_conf: dict[str, str] | None = None ssl_verify: bool | None = None ssl_ca_bundle: str | None = None + auth_type: str = "none" + auth_user: str | None = None + auth_password: str | None = None + auth_principal: str | None = None + auth_keytab: str | None = None class PrepareSubmitJobRequest(BaseModel): diff --git a/tests/unit/test_connection_tools.py b/tests/unit/test_connection_tools.py index 890820d..3b73af2 100644 --- a/tests/unit/test_connection_tools.py +++ b/tests/unit/test_connection_tools.py @@ -81,6 +81,29 @@ def test_save_connection_preserves_ssl_fields(_fresh_store): assert out["ssl_ca_bundle"] == "/tmp/ca.crt" +def test_save_connection_with_basic_auth(_fresh_store): + connections.save_connection( + name="secure", + master="yarn", + auth_type="basic", + auth_user="hdfs", + auth_password="secret", + ) + out = connections.get_connection("secure") + assert out["auth_type"] == "basic" + assert out["auth_user"] == "hdfs" + assert out["auth_password"] == "secret" + + +def test_save_connection_invalid_auth_type_raises(_fresh_store): + with pytest.raises(ValueError, match="auth_type must be one of none/simple/basic/kerberos"): + connections.save_connection( + name="bad", + master="yarn", + auth_type="oauth", + ) + + def test_get_connection_unknown_raises(_fresh_store): with pytest.raises(KeyError): connections.get_connection("missing") diff --git a/tests/unit/test_status_tool.py b/tests/unit/test_status_tool.py index e3bc30b..dd0840f 100644 --- a/tests/unit/test_status_tool.py +++ b/tests/unit/test_status_tool.py @@ -8,6 +8,7 @@ from spark_executor.core.job_store import JobStore from spark_executor.core import connection_store from spark_executor.models import Connection, Job from spark_executor.tools import connections, status +from spark_executor.core.yarn_client import YarnClientConfig def _fresh_stores(): @@ -67,3 +68,38 @@ def test_get_job_status_raises_when_connection_missing(): ) with pytest.raises(KeyError, match="Connection not found"): status.get_job_status("abc") + + +def test_get_job_status_passes_auth_config(): + _fresh_stores() + status.conn_store.save( + Connection( + name="secure", + master="yarn", + yarn_rm_url="http://rm:8088", + auth_type="basic", + auth_user="hdfs", + auth_password="secret", + ) + ) + status.store.put( + Job( + job_id="abc", + application_id="application_1", + script_path="/tmp/j.py", + queue="default", + submit_time=datetime(2026, 6, 24), + connection="secure", + yarn_rm_url="http://rm:8088", + ) + ) + with patch( + "spark_executor.tools.status.get_application_status", + return_value=("RUNNING", "State : RUNNING\n"), + ) as m: + status.get_job_status("abc") + args = m.call_args.args + assert args[0] == "application_1" + assert isinstance(args[1], YarnClientConfig) + assert args[1].auth_type == "basic" + assert args[1].auth_user == "hdfs" diff --git a/tests/unit/test_yarn_client.py b/tests/unit/test_yarn_client.py index 2ed8d2f..8e8ea22 100644 --- a/tests/unit/test_yarn_client.py +++ b/tests/unit/test_yarn_client.py @@ -1,4 +1,6 @@ # coding=utf-8 +import base64 + from unittest.mock import patch import httpx @@ -323,3 +325,48 @@ def test_ssl_verify_false_without_ca_bundle_uses_global_default(): with patch("spark_executor.core.yarn_client.httpx.request", return_value=fake) as m: get_application_status("application_1", cfg) assert m.call_args.kwargs["verify"] is False + + +# --- authentication configuration --- + +def test_status_no_auth_for_none(): + fake = _resp(200, json_data={"app": {"state": "RUNNING"}}) + with patch("spark_executor.core.yarn_client.httpx.request", return_value=fake) as m: + get_application_status("application_1", YarnClientConfig(yarn_rm_url=RM, auth_type="none")) + assert m.call_args.kwargs["auth"] is None + +def test_status_basic_auth_header(): + fake = _resp(200, json_data={"app": {"state": "RUNNING"}}) + cfg = YarnClientConfig( + yarn_rm_url=RM, + auth_type="basic", + auth_user="alice", + auth_password="pw", + ) + with patch("spark_executor.core.yarn_client.httpx.request", return_value=fake) as m: + get_application_status("application_1", cfg) + auth = m.call_args.kwargs["auth"] + assert isinstance(auth, httpx.BasicAuth) + encoded = auth._auth_header.split(" ", 1)[1] + assert base64.b64decode(encoded).decode() == "alice:pw" + + +def test_status_no_auth_for_simple(): + fake = _resp(200, json_data={"app": {"state": "RUNNING"}}) + with patch("spark_executor.core.yarn_client.httpx.request", return_value=fake) as m: + get_application_status("application_1", YarnClientConfig(yarn_rm_url=RM, auth_type="simple")) + assert m.call_args.kwargs["auth"] is None + +def test_status_kerberos_auth(): + fake = _resp(200, json_data={"app": {"state": "RUNNING"}}) + cfg = YarnClientConfig(yarn_rm_url=RM, auth_type="kerberos") + with patch("spark_executor.core.yarn_client.httpx.request", return_value=fake) as m: + get_application_status("application_1", cfg) + auth = m.call_args.kwargs["auth"] + assert auth is not None + assert "Kerberos" in type(auth).__name__ + +def test_basic_auth_missing_user_raises(): + cfg = YarnClientConfig(yarn_rm_url=RM, auth_type="basic") + with pytest.raises(YarnConfigError, match="auth_type='basic' requires auth_user"): + get_application_status("application_1", cfg) diff --git a/uv.lock b/uv.lock index 7a857a1..6eedbbc 100644 --- a/uv.lock +++ b/uv.lock @@ -258,6 +258,15 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + [[package]] name = "fastapi" version = "0.138.0" @@ -295,6 +304,21 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/83/6bf02ff9e3ca1d24765050e3b51dceae9bb69909cc5385623cf6f3fd7c23/fastapi_mcp-0.4.0-py3-none-any.whl", hash = "sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec", size = 25085, upload-time = "2025-07-28T12:11:04.472Z" }, ] +[[package]] +name = "gssapi" +version = "1.11.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +dependencies = [ + { name = "decorator", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/52/c1e90623c259a42ab0587078bb04f959867b970add46ff66750ead8fc7c5/gssapi-1.11.1.tar.gz", hash = "sha256:2049ee4b1d0c363163a1344b7282a363f9f4094e51d2c36de0cf01d4735e0ae2", size = 95233, upload-time = "2026-01-26T21:01:39.463Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/75/3cc18f2d084d19fbba38dc684588cf5f674c647e754f9cf1625bd78c39f8/gssapi-1.11.1-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2298e5909a8f2d27c29352885a24e4026cfd3fa24fc38d4a0a3743fa5a3e7667", size = 611712, upload-time = "2026-01-26T21:01:19.626Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/f9/ac0f8c43c209d56c89655f80cd4ae43379f88370d01a7e11f264f081eef5/gssapi-1.11.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:5b60b1f8d8d3e36c025bd3494105de1dfccee578e8de001f423cc094468e3022", size = 642782, upload-time = "2026-01-26T21:01:21.462Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/79/9148636b75ca5741ddc9c57c4b256adec422e3a89bca14306d53b48caac9/gssapi-1.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:82fba401e9514ad21749b8d8954e2de1c617b0a73204c8598ee84630e23c5810", size = 714379, upload-time = "2026-01-26T21:01:25.961Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/77/f34fd81bbccf2e682073964a1b1b0a383e70d02946e472f78881d50cec6f/gssapi-1.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb0250f27d288d4217d7f606d3b68ecb9a10fce9391106129cada96434a685b0", size = 734103, upload-time = "2026-01-26T21:01:27.3Z" }, +] + [[package]] name = "gunicorn" version = "26.0.0" @@ -344,6 +368,21 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-kerberos" +version = "0.1.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "httpx" }, + { name = "pyspnego" }, + { name = "pyspnego", extra = ["kerberos"], marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/e6/67838510aa9b6f38b139a13e60b001dab578c2bc1cc390b48a94c6542a72/httpx_kerberos-0.1.2.tar.gz", hash = "sha256:ae47ddbe9468dfee49e79477baeb5237143ecf483ecef15ea4b4a9c86a961264", size = 8372, upload-time = "2025-03-23T15:41:26.324Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/28/ed689c79425d9676418b1c011cd002b12c560e81da652667da93fe98dc36/httpx_kerberos-0.1.2-py3-none-any.whl", hash = "sha256:efc87c5dbb0b37f380727f68285fe900842a712c2ab38b80e8b8d01b13bab6ba", size = 9924, upload-time = "2025-03-23T15:41:25.521Z" }, +] + [[package]] name = "httpx-sse" version = "0.4.3" @@ -398,6 +437,18 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "krb5" +version = "0.9.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/15/55a01be5f1816fe6d7d36fec4c6b2cb6f5264d289a015322562c582a81b7/krb5-0.9.0.tar.gz", hash = "sha256:4cdd2c85ff4770108edaf48fedf19888cf956ff374e2e97e40f8412b048caee6", size = 236761, upload-time = "2025-11-25T18:53:46.997Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/4e/0a35ae4821ca5f0491844d9343c816883b3218a265a4a95ecec66e76f239/krb5-0.9.0-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7a021e869833bfed44ed4c9dfefd25813fab40a381ad4482b79ea36744545f62", size = 913483, upload-time = "2025-11-25T18:53:38.966Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/32/d2a9d977d23425777c0c5722947e6f0bc80882c7de15038bd02f9269c01a/krb5-0.9.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:2bca06e7ce1551f3eb7f674508bc34d9f44fa1c9056f24120878ec9ab8e430cb", size = 939459, upload-time = "2025-11-25T18:53:40.597Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/b9/fd0079f9208738bc09cf99208fc92cdf7d8b6a2a363af9a73256efa76399/krb5-0.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ea2ee73bb5aa7818ce50895fb29c276c7fe478c6eda121a5b66b0cc45fe2d42e", size = 1090101, upload-time = "2025-11-25T18:53:41.937Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/9f/df4e24995e0fea7792e8ab152124c65ac845e00ddfb4dd8f7d22907d122b/krb5-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9da4558a47ec105a1a2c185bb4b2d1dd90b7c021e3116895171f913ef048d03", size = 1101333, upload-time = "2025-11-25T18:53:43.177Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -611,6 +662,25 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyspnego" +version = "0.12.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "sspilib", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/84/58577bd1b14293650879de0579ec263a1d8350f1d6d227226cf776b5a6a6/pyspnego-0.12.1.tar.gz", hash = "sha256:ff4fb6df38202a012ea2a0f43091ae9680878443f0ea61c9ea0e2e8152a4b810", size = 226027, upload-time = "2026-03-02T20:16:09.74Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/9f/4da6a1b9611af2397289b77d6fd08f5fe8f1f34dabc3bf85b0638d655e64/pyspnego-0.12.1-py3-none-any.whl", hash = "sha256:7237cb47985ccf5da512106ddb2731e4f9cefec00991f76c054488eb95fb1a2d", size = 130247, upload-time = "2026-03-02T20:16:08.331Z" }, +] + +[package.optional-dependencies] +kerberos = [ + { name = "gssapi", marker = "sys_platform != 'win32'" }, + { name = "krb5", marker = "sys_platform != 'win32'" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -846,6 +916,7 @@ dependencies = [ { name = "fastapi-mcp" }, { name = "gunicorn" }, { name = "httpx" }, + { name = "httpx-kerberos" }, { name = "loguru" }, { name = "pydantic" }, { name = "pydantic-core" }, @@ -865,6 +936,7 @@ requires-dist = [ { name = "fastapi-mcp", specifier = ">=0.4.0" }, { name = "gunicorn", specifier = ">=23.0" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "httpx-kerberos", specifier = ">=0.1.2" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "pydantic", specifier = ">=2.13.4" }, { name = "pydantic-core", specifier = ">=2.46.4" }, @@ -900,6 +972,20 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] +[[package]] +name = "sspilib" +version = "0.5.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/e6/d0d74b18bed8c16949fddc0401005c072947ae7bf1bab982ed28f9ebc2d8/sspilib-0.5.0.tar.gz", hash = "sha256:b62f7f2602aa1add0505eee2417e2df24421224cb411e53bf3ae42a71b62fe98", size = 59920, upload-time = "2025-12-03T00:31:05.564Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/1b/dd9066491168933b0f7ab6e396ac58cc024c8954e95264c38e3dc9363d7c/sspilib-0.5.0-cp311-abi3-win32.whl", hash = "sha256:fcb57b41b3200ef2e6e8846e2a13799d20b35b796267f2f75cc65e3883e8eeb6", size = 451246, upload-time = "2025-12-03T00:30:36.685Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/6a/a11abf90172ff580ac2f9ade3496d868e05e851c4ecf487dd5baeb966b1d/sspilib-0.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ca2a21a4e90db563c2cec639c66b3a29ea53129a0c55ff1e4154a02937f6bd45", size = 540777, upload-time = "2025-12-03T00:30:38.44Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/05/983876d281b9e7926f1c9126e72de8bd5928b1de45433163f54d4e217502/sspilib-0.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:6893bad16f122fc3c4bd908461b9728694465c05ca97c22f7e2094791c4ee3cb", size = 470353, upload-time = "2025-12-03T00:30:39.63Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/f8/34e8e86883054b961c2eb88a5b42b89b2bf975723b1acca090966c2d03ff/sspilib-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:9dad272abf3f4cf0bf95d495075d2987f6ba1fb300f8d603661ccac07d11272f", size = 567408, upload-time = "2025-12-03T00:30:48.723Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/d8/8c4ba75f925fd9651cb855c47e0e67931a175d6fd41e569193a8d58133ac/sspilib-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7d7724d5dbb31f68e62465863dfb862fe2793281ce40d0c8f2dc60c8f07998f2", size = 690291, upload-time = "2025-12-03T00:30:49.929Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/c3/07af17b6fcc2b02af294a8817e30441a502880a04c8d60be2d71e0a1eacc/sspilib-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8ce23ec740dee025136370ed4ae64b7d1535368321049ef960012a57c93ebe15", size = 534304, upload-time = "2025-12-03T00:30:51.348Z" }, +] + [[package]] name = "starlette" version = "1.3.1"