Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-langsmith for 
openSUSE:Factory checked in at 2026-10-15 12:20:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langsmith (Old)
 and      /work/SRC/openSUSE:Factory/.python-langsmith.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langsmith"

Thu Oct 15 12:20:00 2026 rev:36 rq:1384905 version:0.14.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-langsmith/python-langsmith.changes        
2026-10-10 15:00:42.660323586 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-langsmith.new.3698736/python-langsmith.changes
   2026-10-15 12:21:02.207318780 +0200
@@ -1,0 +2,9 @@
+Thu Oct 15 04:58:48 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.14.8:
+  * Keep session_id when copying a run tree; fall back to
+    /runs/batch when a run is not found
+  * Harden sandbox token/callback JWT verification with
+    algorithm allowlists; S3 mounts gain KMS encryption option
+
+-------------------------------------------------------------------

Old:
----
  langsmith-0.14.7.tar.gz

New:
----
  langsmith-0.14.8.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-langsmith.spec ++++++
--- /var/tmp/diff_new_pack.mnemQH/_old  2026-10-15 12:21:02.850345676 +0200
+++ /var/tmp/diff_new_pack.mnemQH/_new  2026-10-15 12:21:02.852345759 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-langsmith
-Version:        0.14.7
+Version:        0.14.8
 Release:        0
 Summary:        Client library for the LangSmith LLM tracing and evaluation 
platform
 License:        MIT

++++++ langsmith-0.14.7.tar.gz -> langsmith-0.14.8.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/.bumpversion.cfg 
new/langsmith-0.14.8/.bumpversion.cfg
--- old/langsmith-0.14.7/.bumpversion.cfg       2020-02-02 01:00:00.000000000 
+0100
+++ new/langsmith-0.14.8/.bumpversion.cfg       2020-02-02 01:00:00.000000000 
+0100
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 0.14.7
+current_version = 0.14.8
 parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
 serialize = {major}.{minor}.{patch}
 search = {current_version}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/PKG-INFO 
new/langsmith-0.14.8/PKG-INFO
--- old/langsmith-0.14.7/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.5
 Name: langsmith
-Version: 0.14.7
+Version: 0.14.8
 Summary: Client library to connect to the LangSmith Observability and 
Evaluation Platform.
 Project-URL: Homepage, https://smith.langchain.com/
 Project-URL: Documentation, https://docs.smith.langchain.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/__init__.py 
new/langsmith-0.14.8/langsmith/__init__.py
--- old/langsmith-0.14.7/langsmith/__init__.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/langsmith-0.14.8/langsmith/__init__.py  2020-02-02 01:00:00.000000000 
+0100
@@ -57,7 +57,7 @@
 
 # Avoid calling into importlib on every call to __version__
 
-__version__ = "0.14.7"
+__version__ = "0.14.8"
 version = __version__  # for backwards compatibility
 
 # Metadata key to hide a traced run from LangSmith's Messages View.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/client.py 
new/langsmith-0.14.8/langsmith/client.py
--- old/langsmith-0.14.7/langsmith/client.py    2020-02-02 01:00:00.000000000 
+0100
+++ new/langsmith-0.14.8/langsmith/client.py    2020-02-02 01:00:00.000000000 
+0100
@@ -3697,6 +3697,9 @@
                     break
                 except ls_utils.LangSmithConflictError:
                     break
+                except ls_utils.LangSmithNotFoundError:
+                    # Caller falls back to /runs/batch
+                    raise
                 except (
                     ls_utils.LangSmithConnectionError,
                     ls_utils.LangSmithRequestTimeout,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/run_trees.py 
new/langsmith-0.14.8/langsmith/run_trees.py
--- old/langsmith-0.14.7/langsmith/run_trees.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/langsmith-0.14.8/langsmith/run_trees.py 2020-02-02 01:00:00.000000000 
+0100
@@ -1126,6 +1126,7 @@
                 outputs=self.outputs.copy() if self.outputs else None,
                 error=self.error,
                 parent_run_id=self.parent_run_id,
+                session_id=self.session_id,
                 session_name=self.session_name,
                 address=self.address,
                 reference_example_id=self.reference_example_id,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/sandbox/README.md 
new/langsmith-0.14.8/langsmith/sandbox/README.md
--- old/langsmith-0.14.7/langsmith/sandbox/README.md    2020-02-02 
01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/langsmith/sandbox/README.md    2020-02-02 
01:00:00.000000000 +0100
@@ -881,9 +881,10 @@
 ## Verifying Requests from LangSmith
 
 Install the extra: `pip install "langsmith[sandbox-auth]"`. 
`SandboxTokenVerifier`
-checks EdDSA signatures against LangSmith's JWKS (derived from
-`LANGSMITH_ENDPOINT`, cached). The JWKS must be served over HTTPS (localhost is
-exempt); for a plain-HTTP self-hosted deployment on a trusted network, pass
+checks signatures against LangSmith's JWKS (derived from `LANGSMITH_ENDPOINT`,
+cached); user tokens may use any JOSE asymmetric algorithm (RS*, PS*, ES*,
+EdDSA), chosen by the JWKS key. The JWKS must be served over HTTPS (localhost
+is exempt); for a plain-HTTP self-hosted deployment on a trusted network, pass
 `allow_insecure_jwks=True`.
 
 ### Service URL Users
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/sandbox/__init__.py 
new/langsmith-0.14.8/langsmith/sandbox/__init__.py
--- old/langsmith-0.14.7/langsmith/sandbox/__init__.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/langsmith/sandbox/__init__.py  2020-02-02 
01:00:00.000000000 +0100
@@ -94,6 +94,7 @@
     GitMountRefSpec,
     GitMountSpec,
     MountCacheConfig,
+    MountEncryptionConfig,
     S3MountConfig,
     S3MountSpec,
     SandboxMount,
@@ -184,6 +185,7 @@
     "ContextHubMountSpec",
     "GCPMountAuthConfig",
     "MountCacheConfig",
+    "MountEncryptionConfig",
     "GCSMountConfig",
     "GCSMountSpec",
     "GitMountConfig",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/sandbox/_mounts.py 
new/langsmith-0.14.8/langsmith/sandbox/_mounts.py
--- old/langsmith-0.14.7/langsmith/sandbox/_mounts.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/langsmith/sandbox/_mounts.py   2020-02-02 
01:00:00.000000000 +0100
@@ -21,6 +21,16 @@
     writeback_seconds: int
 
 
+class MountEncryptionConfig(TypedDict):
+    """Customer-managed key that uploads through a mount are encrypted with.
+
+    For S3 mounts, ``kms_key`` is a KMS key ARN in the mount's region and 
uploads
+    use SSE-KMS with it.
+    """
+
+    kms_key: str
+
+
 class BucketMountSpecBase(TypedDict, total=False):
     """Optional fields applied per bucket-backed sandbox mount."""
 
@@ -43,7 +53,13 @@
     path_style: bool
 
 
-class S3MountSpec(BucketMountSpecBase):
+class S3MountSpecBase(BucketMountSpecBase, total=False):
+    """Optional fields applied per S3 sandbox mount."""
+
+    encryption: MountEncryptionConfig
+
+
+class S3MountSpec(S3MountSpecBase):
     """S3-backed sandbox mount specification."""
 
     id: str
@@ -232,6 +248,7 @@
     path_style: bool = False,
     read_only: bool | None = None,
     cache: MountCacheConfig | None = None,
+    encryption: MountEncryptionConfig | None = None,
 ) -> S3MountSpec:
     """Build an S3-backed sandbox mount specification."""
     s3: S3MountConfig = {
@@ -253,6 +270,12 @@
         mount["read_only"] = read_only
     if cache is not None:
         mount["cache"] = _copy_cache_config(cache)
+    if encryption is not None:
+        mount["encryption"] = {
+            "kms_key": _require_non_empty_string(
+                encryption.get("kms_key", ""), "encryption.kms_key"
+            )
+        }
     return mount
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/langsmith/sandbox/_verify.py 
new/langsmith-0.14.8/langsmith/sandbox/_verify.py
--- old/langsmith-0.14.7/langsmith/sandbox/_verify.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/langsmith/sandbox/_verify.py   2020-02-02 
01:00:00.000000000 +0100
@@ -33,6 +33,20 @@
 CALLBACK_SIGNATURE_HEADER = "X-LangSmith-Signature-JWT"
 
 _CALLBACK_SUBJECT = "langsmith-sandbox-callback"
+# Any JOSE asymmetric algorithm; PyJWK binds the algorithm to the key the kid 
selects.
+_USER_TOKEN_ALGORITHMS = [
+    "RS256",
+    "RS384",
+    "RS512",
+    "PS256",
+    "PS384",
+    "PS512",
+    "ES256",
+    "ES384",
+    "ES512",
+    "EdDSA",
+]
+_CALLBACK_ALGORITHMS = ["EdDSA"]
 _JWKS_PATH = "/.well-known/jwks.json"
 _JWKS_TTL_SECONDS = 300.0
 # Bounds refetches when a token names a kid the cached set lacks.
@@ -211,14 +225,14 @@
                 ``request.headers["Host"]``. A full service URL is also 
accepted.
             issuer: If set, the LangSmith app URL the token must be issued by.
         """
-        kid = self._kid(token)
+        kid = self._kid(token, _USER_TOKEN_ALGORITHMS)
         return self._decode_user_token(token, self._key_sync(kid), audience, 
issuer)
 
     async def averify_user_token(
         self, token: str, *, audience: str, issuer: Optional[str] = None
     ) -> SandboxUser:
         """Async version of :meth:`verify_user_token`."""
-        kid = self._kid(token)
+        kid = self._kid(token, _USER_TOKEN_ALGORITHMS)
         key = await self._key_async(kid)
         return self._decode_user_token(token, key, audience, issuer)
 
@@ -242,7 +256,7 @@
             issuer: If set, the LangSmith OAuth issuer the signature must be
                 issued by.
         """
-        kid = self._kid(signature)
+        kid = self._kid(signature, _CALLBACK_ALGORITHMS)
         return self._decode_callback(signature, self._key_sync(kid), body, 
aud, issuer)
 
     async def averify_callback(
@@ -254,18 +268,18 @@
         issuer: Optional[str] = None,
     ) -> SandboxCallback:
         """Async version of :meth:`verify_callback`."""
-        kid = self._kid(signature)
+        kid = self._kid(signature, _CALLBACK_ALGORITHMS)
         key = await self._key_async(kid)
         return self._decode_callback(signature, key, body, aud, issuer)
 
-    def _kid(self, token: str) -> str:
+    def _kid(self, token: str, algorithms: list[str]) -> str:
         if not token:
             raise SandboxTokenVerificationError("token is empty")
         try:
             header = self._jwt.get_unverified_header(token)
         except self._jwt.PyJWTError as e:
             raise SandboxTokenVerificationError(f"malformed token: {e}") from e
-        if header.get("alg") != "EdDSA":
+        if header.get("alg") not in algorithms:
             raise SandboxTokenVerificationError(
                 f"unexpected signing algorithm: {header.get('alg')!r}"
             )
@@ -281,12 +295,13 @@
         audience: Optional[str],
         issuer: Optional[str],
         required: list[str],
+        algorithms: list[str],
     ) -> dict[str, Any]:
         try:
             return self._jwt.decode(
                 token,
                 key,
-                algorithms=["EdDSA"],
+                algorithms=algorithms,
                 audience=audience,
                 issuer=issuer.rstrip("/") if issuer else None,
                 leeway=_LEEWAY_SECONDS,
@@ -304,6 +319,7 @@
             _service_host(audience),
             issuer,
             ["exp", "iat", "iss", "aud", "sub"],
+            _USER_TOKEN_ALGORITHMS,
         )
         subject = claims["sub"]
         if not isinstance(subject, str) or not subject or subject == 
_CALLBACK_SUBJECT:
@@ -329,6 +345,7 @@
             aud if isinstance(aud, str) else None,
             issuer,
             ["exp", "iat", "iss", "aud", "sub", "body_sha256"],
+            _CALLBACK_ALGORITHMS,
         )
         if callable(aud):
             audiences = claims["aud"]
@@ -420,7 +437,7 @@
 def _find_key(keys: Any, kid: str) -> Any:
     for k in keys.keys:
         if k.key_id == kid:
-            return k.key
+            return k
     return None
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langsmith-0.14.7/tests/unit_tests/sandbox/test_proxy_config.py 
new/langsmith-0.14.8/tests/unit_tests/sandbox/test_proxy_config.py
--- old/langsmith-0.14.7/tests/unit_tests/sandbox/test_proxy_config.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/tests/unit_tests/sandbox/test_proxy_config.py  
2020-02-02 01:00:00.000000000 +0100
@@ -215,6 +215,25 @@
     }
 
 
+def test_s3_mount_sets_kms_key_only_when_encryption_given() -> None:
+    kms_key = (
+        
"arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
+    )
+
+    assert "encryption" not in s3_mount(id="s3_data", mount_path="/mnt/s3", 
bucket="b")
+    mount = s3_mount(
+        id="s3_data",
+        mount_path="/mnt/s3",
+        bucket="b",
+        encryption={"kms_key": kms_key},
+    )
+    assert mount["encryption"] == {"kms_key": kms_key}
+    with pytest.raises(ValueError, match="encryption.kms_key"):
+        s3_mount(
+            id="s3_data", mount_path="/mnt/s3", bucket="b", 
encryption={"kms_key": " "}
+        )
+
+
 def test_git_mount_serializes_backend_shape() -> None:
     assert git_mount(
         id="repo",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langsmith-0.14.7/tests/unit_tests/sandbox/test_verify.py 
new/langsmith-0.14.8/tests/unit_tests/sandbox/test_verify.py
--- old/langsmith-0.14.7/tests/unit_tests/sandbox/test_verify.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/tests/unit_tests/sandbox/test_verify.py        
2020-02-02 01:00:00.000000000 +0100
@@ -10,8 +10,9 @@
 
 import jwt
 import pytest
+from cryptography.hazmat.primitives.asymmetric import ec, rsa
 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from jwt.algorithms import OKPAlgorithm
+from jwt.algorithms import ECAlgorithm, OKPAlgorithm, RSAAlgorithm
 from pytest_httpx import HTTPXMock
 
 from langsmith import utils as ls_utils
@@ -27,6 +28,7 @@
 SERVICE_HOST = "0190aaaa-0000-7000-8000-000000000001--8080.svc.example.com"
 CALLBACK_URL = "https://integrator.example.com/sandbox-callback";
 KID = "test-kid"
+RSA_KID = "rsa-kid"
 
 
 @pytest.fixture(autouse=True)
@@ -54,15 +56,57 @@
 
 
 @pytest.fixture
+def rsa_key():
+    return rsa.generate_private_key(public_exponent=65537, key_size=2048)
+
+
[email protected]
+def rsa_jwks(httpx_mock: HTTPXMock, key, rsa_key):
+    rsa_jwk = {
+        **json.loads(RSAAlgorithm.to_jwk(rsa_key.public_key())),
+        "kid": RSA_KID,
+        "alg": "RS256",
+        "use": "sig",
+    }
+    httpx_mock.add_response(
+        url=JWKS_URL, json={"keys": [_jwk(key, KID), rsa_jwk]}, 
is_reusable=True
+    )
+
+
[email protected]
+def ec_key():
+    return ec.generate_private_key(ec.SECP256R1())
+
+
[email protected]
+def mixed_jwks(httpx_mock: HTTPXMock, key, rsa_key, ec_key):
+    rsa_jwk = {
+        **json.loads(RSAAlgorithm.to_jwk(rsa_key.public_key())),
+        "kid": RSA_KID,
+        "alg": "PS256",
+    }
+    ec_jwk = {**json.loads(ECAlgorithm.to_jwk(ec_key.public_key())), "kid": 
"ec-kid"}
+    httpx_mock.add_response(
+        url=JWKS_URL, json={"keys": [_jwk(key, KID), rsa_jwk, ec_jwk]}, 
is_reusable=True
+    )
+
+
[email protected]
 def jwks(httpx_mock: HTTPXMock, key):
     httpx_mock.add_response(
         url=JWKS_URL, json={"keys": [_jwk(key, KID)]}, is_reusable=True
     )
 
 
-def _sign(key, claims: dict, kid: str = KID, headers: dict | None = None) -> 
str:
+def _sign(
+    key,
+    claims: dict,
+    kid: str = KID,
+    headers: dict | None = None,
+    algorithm: str = "EdDSA",
+) -> str:
     return jwt.encode(
-        claims, key, algorithm="EdDSA", headers={"kid": kid, **(headers or {})}
+        claims, key, algorithm=algorithm, headers={"kid": kid, **(headers or 
{})}
     )
 
 
@@ -165,6 +209,39 @@
         with pytest.raises(SandboxTokenVerificationError, match="algorithm"):
             verifier.verify_user_token(token, audience=SERVICE_HOST)
 
+    def test_accepts_rs256(self, rsa_jwks, rsa_key, verifier):
+        token = _sign(rsa_key, _user_claims(), RSA_KID, algorithm="RS256")
+        user = verifier.verify_user_token(token, audience=SERVICE_HOST)
+        assert user.subject == "user-123"
+
+    def test_rejects_algorithm_key_type_mismatch(
+        self, rsa_jwks, key, rsa_key, verifier
+    ):
+        eddsa_under_rsa_kid = _sign(key, _user_claims(), RSA_KID)
+        rs256_under_ed_kid = _sign(rsa_key, _user_claims(), KID, 
algorithm="RS256")
+        for token in (eddsa_under_rsa_kid, rs256_under_ed_kid):
+            with pytest.raises(SandboxTokenVerificationError):
+                verifier.verify_user_token(token, audience=SERVICE_HOST)
+
+    @pytest.mark.parametrize("algorithm", ["ES256", "PS256"])
+    def test_accepts_other_jose_algorithms(
+        self, mixed_jwks, rsa_key, ec_key, verifier, algorithm
+    ):
+        signer, kid = (ec_key, "ec-kid") if algorithm == "ES256" else 
(rsa_key, RSA_KID)
+        token = _sign(signer, _user_claims(), kid, algorithm=algorithm)
+        user = verifier.verify_user_token(token, audience=SERVICE_HOST)
+        assert user.subject == "user-123"
+
+    def test_rejects_es256_under_rsa_key(self, mixed_jwks, ec_key, verifier):
+        token = _sign(ec_key, _user_claims(), RSA_KID, algorithm="ES256")
+        with pytest.raises(SandboxTokenVerificationError):
+            verifier.verify_user_token(token, audience=SERVICE_HOST)
+
+    def test_rejects_alg_none(self, verifier):
+        token = jwt.encode(_user_claims(), None, algorithm="none", 
headers={"kid": KID})
+        with pytest.raises(SandboxTokenVerificationError, match="algorithm"):
+            verifier.verify_user_token(token, audience=SERVICE_HOST)
+
     def test_rejects_callback_signature(self, jwks, key, verifier):
         body = _callback_body()
         token = _sign(key, _callback_claims(body, aud=[SERVICE_HOST]))
@@ -262,6 +339,12 @@
         assert cb.identity.sandbox_id == 
json.loads(body)["identity"]["sandbox_id"]
         assert cb.request is None
 
+    def test_rejects_rs256(self, rsa_key, verifier):
+        body = _callback_body()
+        signature = _sign(rsa_key, _callback_claims(body), RSA_KID, 
algorithm="RS256")
+        with pytest.raises(SandboxTokenVerificationError, match="algorithm"):
+            verifier.verify_callback(body=body, signature=signature)
+
     def test_full_request(self, jwks, key, verifier):
         body = _callback_body(
             request={
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langsmith-0.14.7/tests/unit_tests/test_multipart_retry_matrix.py 
new/langsmith-0.14.8/tests/unit_tests/test_multipart_retry_matrix.py
--- old/langsmith-0.14.7/tests/unit_tests/test_multipart_retry_matrix.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/tests/unit_tests/test_multipart_retry_matrix.py        
2020-02-02 01:00:00.000000000 +0100
@@ -9,7 +9,9 @@
    *only* when the response actually carries a ``Retry-After`` header.
 2. The ``for idx in range(1, attempts + 1)`` loop in ``_send_multipart_req``,
    which retries only ``LangSmithConnectionError`` / 
``LangSmithRequestTimeout``
-   / ``LangSmithAPIError`` (500) and swallows everything else.
+   / ``LangSmithAPIError`` (500), re-raises ``LangSmithNotFoundError`` (404)
+   so ``_multipart_ingest_ops`` can fall back to ``/runs/batch``, and swallows
+   everything else.
 
 A ``MagicMock`` session cannot see layer 1 at all, so these tests drive a real
 local HTTP server through the real adapter and count sockets hit.
@@ -41,8 +43,10 @@
     def __init__(self):
         self.count = 0
         self.status = 200
+        self.status_by_path: dict[str, int] = {}
         self.retry_after = None
         self.url = ""
+        self.paths: list[str] = []
 
 
 @pytest.fixture
@@ -65,8 +69,9 @@
                     if size == 0:
                         break
             ep.count += 1
+            ep.paths.append(self.path)
             body = b"{}"
-            self.send_response(ep.status)
+            self.send_response(ep.status_by_path.get(self.path, ep.status))
             if ep.retry_after is not None:
                 self.send_header("Retry-After", str(ep.retry_after))
             self.send_header("Content-Type", "application/json")
@@ -96,14 +101,8 @@
         yield slept
 
 
-def _payload(n_runs=1):
-    """Build a payload through the real serializer, so the log context is real.
-
-    The per-op context string is ``trace=<trace_id>,id=<run_id>``
-    (``_operations.py``), and ``join_multipart_parts_and_context`` concatenates
-    one of those per operation with ``"; "``. Hand-writing the context in a 
test
-    would pin a message shape that never occurs in production.
-    """
+def _ops(n_runs=1):
+    """Build serialized run ops; ``_payload`` turns them into a multipart 
body."""
     ops = []
     ids = []
     for _ in range(n_runs):
@@ -133,6 +132,18 @@
                 attachments=None,
             )
         )
+    return ops, ids
+
+
+def _payload(n_runs=1):
+    """Build a payload through the real serializer, so the log context is real.
+
+    The per-op context string is ``trace=<trace_id>,id=<run_id>``
+    (``_operations.py``), and ``join_multipart_parts_and_context`` concatenates
+    one of those per operation with ``"; "``. Hand-writing the context in a 
test
+    would pin a message shape that never occurs in production.
+    """
+    ops, ids = _ops(n_runs)
     parts = [
         serialized_run_operation_to_multipart_parts_and_context(op)[0] for op 
in ops
     ]
@@ -173,7 +184,8 @@
     # Plain client errors are never retried.
     (401, None, 1, "none"),
     (403, None, 1, "none"),
-    (404, None, 1, "none"),
+    # 404 is re-raised so the caller can fall back to /runs/batch.
+    (404, None, 1, "none (raised)"),
     (422, None, 1, "none"),
 ]
 
@@ -202,7 +214,11 @@
     endpoint.retry_after = retry_after
     client = _client(endpoint)
 
-    client._send_multipart_req(_one_run_payload())
+    if status == 404:
+        with pytest.raises(ls_utils.LangSmithNotFoundError):
+            client._send_multipart_req(_one_run_payload())
+    else:
+        client._send_multipart_req(_one_run_payload())
 
     assert endpoint.count == expected_requests, (
         f"HTTP {status} (Retry-After={retry_after}) produced {endpoint.count} "
@@ -351,7 +367,6 @@
         (409, False),  # swallowed as a duplicate -- silently dropped, no dump
         (500, True),
         (429, True),
-        (404, True),
     ],
 )
 def test_exhausted_batch_is_dropped_not_requeued(
@@ -392,6 +407,18 @@
     assert isinstance(errors[0], ls_utils.LangSmithAPIError)
 
 
+def test_404_falls_back_to_batch_and_disables_multipart(endpoint, no_sleep):
+    """A 404 on /runs/multipart resends the runs via /runs/batch (LSDK-551)."""
+    endpoint.status_by_path["/runs/multipart"] = 404
+    client = _client(endpoint)
+    ops, _ = _ops(1)
+
+    client._multipart_ingest_ops(ops)
+
+    assert endpoint.paths == ["/runs/multipart", "/runs/batch"]
+    assert client._multipart_disabled is True
+
+
 # ---------------------------------------------------------------------------
 # The warning log is the ONLY customer-visible signal that traces were dropped
 # (the error callback is opt-in, the fallback dir is opt-in). These tests pin
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langsmith-0.14.7/tests/unit_tests/test_run_trees.py 
new/langsmith-0.14.8/tests/unit_tests/test_run_trees.py
--- old/langsmith-0.14.7/tests/unit_tests/test_run_trees.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/langsmith-0.14.8/tests/unit_tests/test_run_trees.py     2020-02-02 
01:00:00.000000000 +0100
@@ -6,7 +6,7 @@
 from concurrent.futures import ThreadPoolExecutor
 from datetime import datetime, timedelta, timezone
 from unittest.mock import MagicMock
-from uuid import UUID
+from uuid import UUID, uuid4
 
 import pytest
 from multipart import MultipartParser, parse_options_header
@@ -808,6 +808,40 @@
         assert sent is None
 
 
+def test_patch_sends_project_id():
+    """A run addressed by project id keeps that id on the update, so the patch
+    resolves the same project as the post even if the project was renamed."""
+    client = MagicMock()
+    project_id = uuid4()
+    run_tree = RunTree(
+        name="test_run",
+        run_type="chain",
+        inputs={"a": 1},
+        client=client,
+        project_name="test-project",
+        project_id=project_id,
+    )
+    run_tree.patch()
+
+    sent = client.update_run.call_args.kwargs
+    assert sent["session_id"] == project_id
+    assert sent["session_name"] == "test-project"
+
+
+def test_patch_without_project_id_sends_none():
+    client = MagicMock()
+    run_tree = RunTree(
+        name="test_run",
+        run_type="chain",
+        inputs={"a": 1},
+        client=client,
+        project_name="test-project",
+    )
+    run_tree.patch()
+
+    assert client.update_run.call_args.kwargs["session_id"] is None
+
+
 def test_patch_exclude_inputs_env_flag_with_replicas(
     monkeypatch, _reset_exclude_inputs_cache
 ):

Reply via email to