mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): bake release commit into dcode doctor (#4225)
`dcode doctor` now reports the exact commit a released wheel was built from (once the release workflow sets `DEEPAGENTS_CODE_BUILD_COMMIT`). --- Released wheels and sdists ship no `.git` metadata, so `dcode doctor` could only resolve a `Commit hash` for *editable* installs that probe a live working tree — every normal pip/pipx/wheel install correctly showed `unknown`. This stamps the release commit into the package at build time so installed wheels report the real commit. A hatchling build hook (`hatch_build.py`) writes a generated `_build_info.py` with `BUILD_COMMIT` **only** when `DEEPAGENTS_CODE_BUILD_COMMIT` is set (CI release builds); it validates the value is a hex SHA, registers the file as a build artifact, and removes it afterward so the working tree stays clean. `doctor` now prefers that stamped commit and falls back to the existing live `git rev-parse` probe, so editable/dev installs keep reporting the live working-tree commit (which reflects local changes). Made by [Open SWE](https://openswe.vercel.app/agents/f53414ff-5e6b-da73-65d1-9091c5d31a94) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -282,6 +282,10 @@ jobs:
|
||||
- name: Build project for distribution
|
||||
run: uv build
|
||||
working-directory: ${{ env.WORKING_DIR }}
|
||||
env:
|
||||
# Stamp the exact release commit into deepagents-code so `dcode doctor`
|
||||
# reports it on installed wheels. Other packages ignore this env var.
|
||||
DEEPAGENTS_CODE_BUILD_COMMIT: ${{ needs.setup.outputs.release-sha }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
|
||||
@@ -251,3 +251,6 @@ CLAUDE.md
|
||||
libs/cli/frontend/node_modules/
|
||||
libs/cli/frontend/dist/
|
||||
node_modules/
|
||||
|
||||
# Generated at build time by libs/code/hatch_build.py (stamps the release commit)
|
||||
libs/code/deepagents_code/_build_info.py
|
||||
|
||||
@@ -75,15 +75,49 @@ def _sdk_version() -> tuple[str, bool]:
|
||||
return "unknown", False
|
||||
|
||||
|
||||
def _build_commit() -> str | None:
|
||||
"""Return the commit stamped into the package at build time, if present.
|
||||
|
||||
Released wheels carry a generated `_build_info.py` (see `hatch_build.py`);
|
||||
editable and local installs do not, so this returns `None` for them.
|
||||
"""
|
||||
try:
|
||||
from deepagents_code._build_info import ( # ty: ignore[unresolved-import] # generated at build time
|
||||
BUILD_COMMIT,
|
||||
)
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception: # a corrupt stamp must never crash `doctor`
|
||||
logger.debug("Build-info module present but failed to import", exc_info=True)
|
||||
return None
|
||||
commit = (BUILD_COMMIT or "").strip()
|
||||
return commit or None
|
||||
|
||||
|
||||
def _commit_hash(path: str) -> str:
|
||||
"""Return the short git commit hash for a source path, if available.
|
||||
"""Return the short git commit hash for the install, if available.
|
||||
|
||||
Prefers the commit stamped into a released wheel at build time, but only for
|
||||
non-editable installs: an editable install may carry a stale stamp from a
|
||||
prior local build (the generated file is gitignored and survives a failed
|
||||
build), so it always probes the live git working tree, which reflects local
|
||||
changes.
|
||||
|
||||
Args:
|
||||
path: Directory used as the git command working directory.
|
||||
|
||||
Returns:
|
||||
The short commit hash, or `unknown` when git metadata cannot be read.
|
||||
The short commit hash, or `unknown` when no commit can be determined.
|
||||
"""
|
||||
baked = _build_commit()
|
||||
if baked:
|
||||
from deepagents_code.config import _is_editable_install
|
||||
|
||||
# A baked commit only describes a built wheel; ignore it for editable
|
||||
# installs so a stale stamp can't mask the live working-tree commit.
|
||||
if not _is_editable_install():
|
||||
return baked
|
||||
|
||||
import shutil
|
||||
import subprocess # noqa: S404 # fixed-argv git metadata probe
|
||||
from pathlib import Path
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Hatchling build hook that stamps the release commit into the package.
|
||||
|
||||
When `DEEPAGENTS_CODE_BUILD_COMMIT` is set (CI release builds), this writes
|
||||
`deepagents_code/_build_info.py` so `dcode doctor` can report the exact commit
|
||||
a wheel was built from. Editable and local builds leave the env var unset, so
|
||||
no file is generated and `dcode doctor` falls back to a live `git` probe of the
|
||||
working tree.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from hatchling.builders.hooks.plugin.interface import ( # ty: ignore[unresolved-import] # build-time-only dependency
|
||||
BuildHookInterface,
|
||||
)
|
||||
|
||||
_COMMIT_ENV = "DEEPAGENTS_CODE_BUILD_COMMIT"
|
||||
_COMMIT_RE = re.compile(r"^[0-9a-fA-F]{7,40}$")
|
||||
_TARGET = Path("deepagents_code") / "_build_info.py"
|
||||
|
||||
|
||||
class CustomBuildHook(BuildHookInterface):
|
||||
"""Stamps `_build_info.py` with the release commit when the env var is set."""
|
||||
|
||||
PLUGIN_NAME = "custom"
|
||||
|
||||
def initialize(self, version: str, build_data: dict[str, Any]) -> None: # noqa: ARG002 # `version` is part of the hatchling hook signature
|
||||
"""Write the generated build-info module before the build collects files.
|
||||
|
||||
Raises:
|
||||
ValueError: If the commit env var is set but not a hex git SHA.
|
||||
"""
|
||||
commit = os.environ.get(_COMMIT_ENV, "").strip()
|
||||
if not commit:
|
||||
return
|
||||
if not _COMMIT_RE.match(commit):
|
||||
msg = f"{_COMMIT_ENV} must be a hex git SHA, got: {commit!r}"
|
||||
raise ValueError(msg)
|
||||
short = commit[:7].lower()
|
||||
target = Path(self.root) / _TARGET
|
||||
target.write_text(
|
||||
'"""Generated at build time. Do not edit or commit."""\n\n'
|
||||
f'BUILD_COMMIT = "{short}"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
# `_TARGET` is gitignored, so hatchling excludes it from the build by
|
||||
# default; registering it as an artifact force-includes it in the dist.
|
||||
build_data.setdefault("artifacts", []).append(str(_TARGET))
|
||||
|
||||
def finalize(
|
||||
self,
|
||||
version: str, # noqa: ARG002 # part of the hatchling hook signature
|
||||
build_data: dict[str, Any], # noqa: ARG002 # part of the hook signature
|
||||
artifact_path: str, # noqa: ARG002 # part of the hook signature
|
||||
) -> None:
|
||||
"""Remove the generated module so the working tree stays clean.
|
||||
|
||||
Runs only when the stamp env var was set, mirroring `initialize`, so
|
||||
editable and local builds (which never wrote the file) skip the unlink.
|
||||
"""
|
||||
if not os.environ.get(_COMMIT_ENV, "").strip():
|
||||
return
|
||||
(Path(self.root) / _TARGET).unlink(missing_ok=True)
|
||||
@@ -139,6 +139,8 @@ Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
test = [
|
||||
"textual-dev>=1.8.0,<2.0.0",
|
||||
"build>=1.5.0,<2.0.0",
|
||||
# Build backend; imported by tests that exercise the `hatch_build.py` hook.
|
||||
"hatchling>=1.27.0,<2.0.0",
|
||||
"ruff>=0.15.18,<1.0.0",
|
||||
"ty>=0.0.52,<1.0.0",
|
||||
"langchain-quickjs>=0.3.1,<0.4.0",
|
||||
@@ -156,6 +158,10 @@ test = [
|
||||
"twine>=6.2.0,<7.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.hooks.custom]
|
||||
# Stamps deepagents_code/_build_info.py with the release commit when
|
||||
# DEEPAGENTS_CODE_BUILD_COMMIT is set (CI release builds). See hatch_build.py.
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["deepagents_code"]
|
||||
include = [
|
||||
|
||||
@@ -8,11 +8,13 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
from deepagents_code.doctor import (
|
||||
DiagnosticItem,
|
||||
DiagnosticSection,
|
||||
_build_commit,
|
||||
_commit_hash,
|
||||
collect_sections,
|
||||
run_doctor_command,
|
||||
@@ -174,6 +176,7 @@ class TestCommitHash:
|
||||
git.chmod(0o755)
|
||||
|
||||
with (
|
||||
patch("deepagents_code.doctor._build_commit", return_value=None),
|
||||
patch("shutil.which", return_value=str(git)),
|
||||
patch(
|
||||
"subprocess.run",
|
||||
@@ -189,6 +192,7 @@ class TestCommitHash:
|
||||
def test_missing_git_returns_unknown(self) -> None:
|
||||
"""Missing Git should degrade to `unknown` without spawning a process."""
|
||||
with (
|
||||
patch("deepagents_code.doctor._build_commit", return_value=None),
|
||||
patch("shutil.which", return_value=None),
|
||||
patch("subprocess.run") as run,
|
||||
):
|
||||
@@ -196,6 +200,60 @@ class TestCommitHash:
|
||||
|
||||
run.assert_not_called()
|
||||
|
||||
def test_baked_commit_preferred_over_git(self) -> None:
|
||||
"""A build-stamped commit wins for a wheel and skips the live git probe."""
|
||||
with (
|
||||
patch("deepagents_code.doctor._build_commit", return_value="deadbee"),
|
||||
patch("deepagents_code.config._is_editable_install", return_value=False),
|
||||
patch("shutil.which") as which,
|
||||
patch("subprocess.run") as run,
|
||||
):
|
||||
assert _commit_hash("/tmp") == "deadbee"
|
||||
|
||||
which.assert_not_called()
|
||||
run.assert_not_called()
|
||||
|
||||
def test_editable_install_ignores_baked_commit(self) -> None:
|
||||
"""An editable install ignores a (possibly stale) stamp and probes git."""
|
||||
with (
|
||||
patch("deepagents_code.doctor._build_commit", return_value="deadbee"),
|
||||
patch("deepagents_code.config._is_editable_install", return_value=True),
|
||||
patch("shutil.which", return_value=None),
|
||||
patch("subprocess.run") as run,
|
||||
):
|
||||
assert _commit_hash("/tmp") == "unknown"
|
||||
|
||||
run.assert_not_called()
|
||||
|
||||
def test_build_commit_missing_module(self) -> None:
|
||||
"""No generated module (editable/dev install) yields `None`."""
|
||||
with patch.dict(sys.modules, {"deepagents_code._build_info": None}):
|
||||
assert _build_commit() is None
|
||||
|
||||
def test_build_commit_reads_stamped_value(self) -> None:
|
||||
"""A generated module exposes its stamped commit."""
|
||||
stub = SimpleNamespace(BUILD_COMMIT="abc1234")
|
||||
with patch.dict(sys.modules, {"deepagents_code._build_info": stub}):
|
||||
assert _build_commit() == "abc1234"
|
||||
|
||||
@pytest.mark.parametrize("value", ["", " ", None])
|
||||
def test_build_commit_blank_value_is_none(self, value: str | None) -> None:
|
||||
"""A present module with a blank stamp yields `None` (falls back to git)."""
|
||||
stub = SimpleNamespace(BUILD_COMMIT=value)
|
||||
with patch.dict(sys.modules, {"deepagents_code._build_info": stub}):
|
||||
assert _build_commit() is None
|
||||
|
||||
def test_build_commit_corrupt_module_returns_none(self) -> None:
|
||||
"""A present-but-corrupt stamp degrades to `None` instead of crashing."""
|
||||
|
||||
class _Corrupt:
|
||||
def __getattr__(self, name: str) -> str:
|
||||
msg = "corrupt stamp"
|
||||
raise ValueError(msg)
|
||||
|
||||
with patch.dict(sys.modules, {"deepagents_code._build_info": _Corrupt()}):
|
||||
assert _build_commit() is None
|
||||
|
||||
|
||||
class TestRunDoctorCommand:
|
||||
"""Tests for the text and JSON rendering paths."""
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Unit tests for the `hatch_build.py` release-commit stamping hook.
|
||||
|
||||
`hatch_build.py` lives at the package root (not inside `deepagents_code`) and is
|
||||
only on `sys.path` during a build, so it is loaded here directly from its file
|
||||
path. The hook subclasses hatchling's `BuildHookInterface`, so these tests
|
||||
require `hatchling` (declared in the `test` dependency group).
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||
|
||||
_HATCH_BUILD_PATH = Path(__file__).resolve().parents[2] / "hatch_build.py"
|
||||
|
||||
|
||||
def _load_hatch_build() -> ModuleType:
|
||||
"""Load `hatch_build.py` from its on-disk path (it is not importable)."""
|
||||
spec = importlib.util.spec_from_file_location("hatch_build", _HATCH_BUILD_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
_hatch_build = _load_hatch_build()
|
||||
CustomBuildHook = _hatch_build.CustomBuildHook
|
||||
_COMMIT_ENV = _hatch_build._COMMIT_ENV
|
||||
|
||||
|
||||
def _make_hook(root: Path) -> BuildHookInterface:
|
||||
"""Build a hook rooted at `root`.
|
||||
|
||||
The hook only reads `self.root`, so the remaining `BuildHookInterface`
|
||||
constructor arguments (config, build config, metadata) are inert here.
|
||||
"""
|
||||
return CustomBuildHook(str(root), {}, None, None, str(root), "wheel")
|
||||
|
||||
|
||||
def _stamp_path(root: Path) -> Path:
|
||||
return root / "deepagents_code" / "_build_info.py"
|
||||
|
||||
|
||||
class TestInitialize:
|
||||
"""Tests for the `initialize` build hook (writes the stamp)."""
|
||||
|
||||
def test_env_unset_writes_nothing(self, tmp_path: Path, monkeypatch) -> None:
|
||||
"""With the env var unset, no file is written and no artifact is added."""
|
||||
monkeypatch.delenv(_COMMIT_ENV, raising=False)
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
build_data: dict[str, object] = {}
|
||||
|
||||
_make_hook(tmp_path).initialize("1.0", build_data)
|
||||
|
||||
assert not _stamp_path(tmp_path).exists()
|
||||
assert build_data == {}
|
||||
|
||||
def test_valid_sha_is_shortened_lowercased_and_registered(
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""A full uppercase SHA stamps a 7-char lowercase value and an artifact."""
|
||||
monkeypatch.setenv(_COMMIT_ENV, "ABCDEF1234567890ABCDEF1234567890ABCDEF12")
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
build_data: dict[str, object] = {}
|
||||
|
||||
_make_hook(tmp_path).initialize("1.0", build_data)
|
||||
|
||||
assert 'BUILD_COMMIT = "abcdef1"' in _stamp_path(tmp_path).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert build_data["artifacts"] == ["deepagents_code/_build_info.py"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad",
|
||||
[
|
||||
"main", # not hex
|
||||
"g123abc", # non-hex char
|
||||
"abcdef", # too short (< 7)
|
||||
"0" * 41, # too long (> 40)
|
||||
],
|
||||
)
|
||||
def test_invalid_sha_raises_and_writes_nothing(
|
||||
self, tmp_path: Path, monkeypatch, bad: str
|
||||
) -> None:
|
||||
"""A set-but-malformed SHA fails the build loudly and writes no file."""
|
||||
monkeypatch.setenv(_COMMIT_ENV, bad)
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
|
||||
with pytest.raises(ValueError, match=_COMMIT_ENV):
|
||||
_make_hook(tmp_path).initialize("1.0", {})
|
||||
|
||||
assert not _stamp_path(tmp_path).exists()
|
||||
|
||||
@pytest.mark.parametrize("blank", ["", " "])
|
||||
def test_blank_env_writes_nothing(
|
||||
self, tmp_path: Path, monkeypatch, blank: str
|
||||
) -> None:
|
||||
"""A blank or whitespace-only env var is treated as unset."""
|
||||
monkeypatch.setenv(_COMMIT_ENV, blank)
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
build_data: dict[str, object] = {}
|
||||
|
||||
_make_hook(tmp_path).initialize("1.0", build_data)
|
||||
|
||||
assert not _stamp_path(tmp_path).exists()
|
||||
assert build_data == {}
|
||||
|
||||
|
||||
class TestFinalize:
|
||||
"""Tests for the `finalize` build hook (cleans up the stamp)."""
|
||||
|
||||
def test_removes_stamp_when_env_set(self, tmp_path: Path, monkeypatch) -> None:
|
||||
"""Cleanup deletes the generated file when the env var is set."""
|
||||
monkeypatch.setenv(_COMMIT_ENV, "abc1234")
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
stamp = _stamp_path(tmp_path)
|
||||
stamp.write_text("x", encoding="utf-8")
|
||||
|
||||
_make_hook(tmp_path).finalize("1.0", {}, "/tmp/wheel") # not a real path
|
||||
|
||||
assert not stamp.exists()
|
||||
|
||||
def test_noop_when_env_unset(self, tmp_path: Path, monkeypatch) -> None:
|
||||
"""Cleanup is gated on the env var, so an unrelated file is untouched."""
|
||||
monkeypatch.delenv(_COMMIT_ENV, raising=False)
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
stamp = _stamp_path(tmp_path)
|
||||
stamp.write_text("x", encoding="utf-8")
|
||||
|
||||
_make_hook(tmp_path).finalize("1.0", {}, "/tmp/wheel") # not a real path
|
||||
|
||||
assert stamp.exists()
|
||||
|
||||
def test_missing_file_does_not_raise(self, tmp_path: Path, monkeypatch) -> None:
|
||||
"""Cleanup tolerates an already-absent file (`missing_ok=True`)."""
|
||||
monkeypatch.setenv(_COMMIT_ENV, "abc1234")
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
|
||||
_make_hook(tmp_path).finalize("1.0", {}, "/tmp/wheel") # not a real path
|
||||
|
||||
assert not _stamp_path(tmp_path).exists()
|
||||
|
||||
|
||||
def test_initialize_then_finalize_leaves_tree_clean(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""A full stamp/cleanup cycle writes the file then removes it."""
|
||||
monkeypatch.setenv(_COMMIT_ENV, "deadbeef")
|
||||
(tmp_path / "deepagents_code").mkdir()
|
||||
hook = _make_hook(tmp_path)
|
||||
|
||||
hook.initialize("1.0", {})
|
||||
assert _stamp_path(tmp_path).exists()
|
||||
|
||||
hook.finalize("1.0", {}, "/tmp/wheel") # not a real path
|
||||
assert not _stamp_path(tmp_path).exists()
|
||||
Generated
+26
@@ -1225,6 +1225,7 @@ xai = [
|
||||
[package.dev-dependencies]
|
||||
test = [
|
||||
{ name = "build" },
|
||||
{ name = "hatchling" },
|
||||
{ name = "langchain-quickjs" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
@@ -1309,6 +1310,7 @@ provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fir
|
||||
[package.metadata.requires-dev]
|
||||
test = [
|
||||
{ name = "build", specifier = ">=1.5.0,<2.0.0" },
|
||||
{ name = "hatchling", specifier = ">=1.27.0,<2.0.0" },
|
||||
{ name = "langchain-quickjs", editable = "../partners/quickjs" },
|
||||
{ name = "pytest", specifier = ">=9.1.1,<10.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.4.0,<2.0.0" },
|
||||
@@ -2068,6 +2070,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hatchling"
|
||||
version = "1.30.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "trove-classifiers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/4c/8717ccb844b4fa5a5ba6352e97d743ed24e9a22cf90b7c109c17030a46a1/hatchling-1.30.1.tar.gz", hash = "sha256:eee4fd45357f72ebb3d7a42e5d72cfb5e29ed426d79e8836288926c4258d5f2e", size = 56929, upload-time = "2026-06-02T00:09:41.487Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/49/2797ec0ef88008a653a8867bb8d1e5c223cd2df8e40390dd5c6a0279cbc5/hatchling-1.30.1-py3-none-any.whl", hash = "sha256:161eacafb3c6f91526e92116d21426369f2c36e98c36a864f11a96345ad4ee31", size = 77489, upload-time = "2026-06-02T00:09:40.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hf-xet"
|
||||
version = "1.5.1"
|
||||
@@ -6126,6 +6143,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trove-classifiers"
|
||||
version = "2026.6.1.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.4"
|
||||
|
||||
Reference in New Issue
Block a user