diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5e1c2cd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +permissions: + contents: write + issues: write + pull-requests: write + +on: + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: uv sync + + - name: Python Semantic Release + uses: python-semantic-release/python-semantic-release@v10 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f4e8626 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Test + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + services: + netbox: + image: netboxcommunity/netbox:latest + env: + SKIP_SUPERUSER: true + ports: + - 8000:8080 + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: uv sync + + - name: Run tests + run: uv run pytest -v + env: + NETBOX_URL: http://localhost:8000 + NETBOX_TOKEN: ${{ secrets.NETBOX_TOKEN }} diff --git a/.gitignore b/.gitignore index 5bbf0b8..da86319 100644 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,8 @@ logs/ *.bak *.swp *.swo + +# Git worktrees +.worktrees/ + .plans/* diff --git a/Dockerfile b/Dockerfile index abfae27..37b8782 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,4 +33,4 @@ ENV PATH="/app/.venv/bin:$PATH" EXPOSE 8000 -CMD ["python", "-u", "server.py"] +CMD ["netbox-mcp-server"] diff --git a/README.md b/README.md index 368bd96..8378657 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # NetBox MCP Server +> **⚠️ Breaking Change in v1.0.0**: The project structure has changed. +> If upgrading from v0.1.0, update your configuration: +> - Change `uv run server.py` to `uv run netbox-mcp-server` +> - Update Claude Desktop/Code configs to use `netbox-mcp-server` instead of `server.py` +> - Docker users: rebuild images with updated CMD +> - See [CHANGELOG.md](CHANGELOG.md) for full details + This is a simple read-only [Model Context Protocol](https://modelcontextprotocol.io/) server for NetBox. It enables you to interact with your data in NetBox directly via LLMs that support MCP. ## Tools @@ -26,7 +33,7 @@ This is a simple read-only [Model Context Protocol](https://modelcontextprotocol pip install -e . ``` -3. Verify the server can run: `NETBOX_URL=https://netbox.example.com/ NETBOX_TOKEN= uv run server.py` +3. Verify the server can run: `NETBOX_URL=https://netbox.example.com/ NETBOX_TOKEN= uv run netbox-mcp-server` 4. Add the MCP server to your LLM client. See below for some examples with Claude. @@ -40,7 +47,7 @@ Add the server using the `claude mcp add` command: claude mcp add --transport stdio netbox \ --env NETBOX_URL=https://netbox.example.com/ \ --env NETBOX_TOKEN= \ - -- uv --directory /path/to/netbox-mcp-server run server.py + -- uv --directory /path/to/netbox-mcp-server run netbox-mcp-server ``` **Important notes:** @@ -61,7 +68,7 @@ For HTTP transport, first start the server manually: NETBOX_URL=https://netbox.example.com/ \ NETBOX_TOKEN= \ TRANSPORT=http \ -uv run server.py +uv run netbox-mcp-server ``` Then add the running server to Claude Code: @@ -91,7 +98,7 @@ Add the server configuration to your Claude Desktop config file. On Mac, edit `~ "--directory", "/path/to/netbox-mcp-server", "run", - "server.py" + "netbox-mcp-server" ], "env": { "NETBOX_URL": "https://netbox.example.com/", @@ -176,7 +183,7 @@ For local Claude Desktop or Claude Code usage with stdio transport: "mcpServers": { "netbox": { "command": "uv", - "args": ["--directory", "/path/to/netbox-mcp-server", "run", "server.py"], + "args": ["--directory", "/path/to/netbox-mcp-server", "run", "netbox-mcp-server"], "env": { "NETBOX_URL": "https://netbox.example.com/", "NETBOX_TOKEN": "" @@ -198,10 +205,10 @@ export TRANSPORT=http export HOST=127.0.0.1 export PORT=8000 -uv run server.py +uv run netbox-mcp-server # Or using CLI arguments -uv run server.py \ +uv run netbox-mcp-server \ --netbox-url https://netbox.example.com/ \ --netbox-token \ --transport http \ @@ -237,11 +244,11 @@ LOG_LEVEL=INFO All configuration options can be overridden via CLI arguments: ```bash -uv run server.py --help +uv run netbox-mcp-server --help # Common examples: -uv run server.py --log-level DEBUG --no-verify-ssl # Development -uv run server.py --transport http --port 9000 # Custom HTTP port +uv run netbox-mcp-server --log-level DEBUG --no-verify-ssl # Development +uv run netbox-mcp-server --transport http --port 9000 # Custom HTTP port ``` ## Docker Usage diff --git a/claude.md b/claude.md index 70e8674..b7135e6 100644 --- a/claude.md +++ b/claude.md @@ -18,12 +18,20 @@ A read-only [Model Context Protocol](https://modelcontextprotocol.io/) server th ```text . -├── server.py # Main MCP server with tool definitions -├── netbox_client.py # NetBox REST API client abstraction -├── pyproject.toml # Dependencies and project metadata -├── README.md # User-facing documentation -├── SECURITY.md # Security policy and reporting -└── LICENSE # Apache 2.0 license +├── src/ +│ └── netbox_mcp_server/ +│ ├── __init__.py # Package initialization with __version__ +│ ├── __main__.py # Entry point for module execution +│ ├── server.py # Main MCP server with tool definitions +│ ├── netbox_client.py # NetBox REST API client abstraction +│ ├── netbox_types.py # NetBox object type mappings +│ └── config.py # Settings and logging configuration +├── tests/ # Test suite +├── .github/workflows/ # CI/CD automation +├── pyproject.toml # Dependencies and project metadata +├── README.md # User-facing documentation +├── CHANGELOG.md # Auto-generated release notes +└── LICENSE # Apache 2.0 license ``` **Design Pattern**: Clean separation between MCP server logic (`server.py`) and NetBox API client (`netbox_client.py`) to support future plugin-based implementations. @@ -35,13 +43,16 @@ A read-only [Model Context Protocol](https://modelcontextprotocol.io/) server th uv sync # Run the server locally (requires env vars) -NETBOX_URL=https://netbox.example.com/ NETBOX_TOKEN= uv run server.py +NETBOX_URL=https://netbox.example.com/ NETBOX_TOKEN= uv run netbox-mcp-server + +# Alternative: module execution +uv run -m netbox_mcp_server # Add to Claude Code (for development/testing) claude mcp add --transport stdio netbox \ --env NETBOX_URL=https://netbox.example.com/ \ --env NETBOX_TOKEN= \ - -- uv --directory /path/to/netbox-mcp-server run server.py + -- uv --directory /path/to/netbox-mcp-server run netbox-mcp-server ``` ## Development Philosophy @@ -57,6 +68,23 @@ claude mcp add --transport stdio netbox \ - **Functional where clear**: Use functional, stateless approaches when they improve clarity - **Clean core logic**: Keep business logic clean; push implementation details to the edges +## Version Management + +This project uses [python-semantic-release](https://python-semantic-release.readthedocs.io/) for automated version management. Versions are automatically determined from commit messages following [Conventional Commits](https://www.conventionalcommits.org/). + +**Release triggers:** + +- `feat:` commits trigger minor version bumps (1.0.0 → 1.1.0) +- `fix:` and `perf:` commits trigger patch version bumps (1.0.0 → 1.0.1) +- Commits with `BREAKING CHANGE:` in the body trigger major version bumps (1.0.0 → 2.0.0) +- `docs:`, `test:`, `chore:`, `ci:`, `refactor:` commits are logged but don't trigger releases + +**Workflow:** + +- Merge to `main` automatically triggers release analysis +- If commits warrant a release, version is bumped and CHANGELOG updated +- GitHub Release is created with auto-generated release notes + ## Code Standards ### Python Conventions @@ -260,7 +288,6 @@ Currently no automated test suite. When adding tests: - ❌ **NEVER commit directly to `main`** - Always use feature branches - ✅ **DO keep commits professional and concise** and focused on the change - ## Decision Heuristics ### When to Add a New Tool diff --git a/pyproject.toml b/pyproject.toml index 8e86e98..b75ebe5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "netbox-mcp-server" version = "0.1.0" @@ -12,8 +16,28 @@ dependencies = [ "pydantic-settings>=2.0", ] +[project.scripts] +netbox-mcp-server = "netbox_mcp_server.server:main" + [dependency-groups] dev = [ "pytest>=8.4.2", "pytest-cov>=7.0.0", + "python-semantic-release>=10.4.1", ] + +[tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] +version_variables = ["src/netbox_mcp_server/__init__.py:__version__"] +branch = "main" +upload_to_vcs_release = true +build_command = "uv build" +tag_format = "v{version}" + +[tool.semantic_release.commit_parser_options] +allowed_tags = ["feat", "fix", "docs", "chore", "refactor", "test", "ci", "perf"] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] + +[tool.semantic_release.changelog] +changelog_file = "CHANGELOG.md" diff --git a/src/netbox_mcp_server/__init__.py b/src/netbox_mcp_server/__init__.py new file mode 100644 index 0000000..6a277b9 --- /dev/null +++ b/src/netbox_mcp_server/__init__.py @@ -0,0 +1,9 @@ +"""NetBox MCP Server - Read-only MCP server for NetBox infrastructure data.""" + +__version__ = "0.1.0" # Auto-managed by semantic-release + +__all__ = ["NetBoxRestClient", "NETBOX_OBJECT_TYPES", "Settings"] + +from netbox_mcp_server.netbox_client import NetBoxRestClient +from netbox_mcp_server.netbox_types import NETBOX_OBJECT_TYPES +from netbox_mcp_server.config import Settings diff --git a/src/netbox_mcp_server/__main__.py b/src/netbox_mcp_server/__main__.py new file mode 100644 index 0000000..09b79b5 --- /dev/null +++ b/src/netbox_mcp_server/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for python -m netbox_mcp_server execution.""" + +from netbox_mcp_server.server import main + +if __name__ == "__main__": + main() diff --git a/config.py b/src/netbox_mcp_server/config.py similarity index 100% rename from config.py rename to src/netbox_mcp_server/config.py diff --git a/netbox_client.py b/src/netbox_mcp_server/netbox_client.py similarity index 100% rename from netbox_client.py rename to src/netbox_mcp_server/netbox_client.py diff --git a/netbox_types.py b/src/netbox_mcp_server/netbox_types.py similarity index 100% rename from netbox_types.py rename to src/netbox_mcp_server/netbox_types.py diff --git a/server.py b/src/netbox_mcp_server/server.py similarity index 96% rename from server.py rename to src/netbox_mcp_server/server.py index 3a5c653..0abf64e 100644 --- a/server.py +++ b/src/netbox_mcp_server/server.py @@ -6,9 +6,9 @@ from fastmcp import FastMCP from pydantic import Field -from config import Settings, configure_logging -from netbox_client import NetBoxRestClient -from netbox_types import NETBOX_OBJECT_TYPES +from netbox_mcp_server.config import Settings, configure_logging +from netbox_mcp_server.netbox_client import NetBoxRestClient +from netbox_mcp_server.netbox_types import NETBOX_OBJECT_TYPES def parse_cli_args() -> dict[str, Any]: @@ -368,7 +368,8 @@ def netbox_get_changelogs(filters: dict): Filtering options include: - user_id: Filter by user ID who made the change - user: Filter by username who made the change - - changed_object_type_id: Filter by ContentType ID of the changed object + - changed_object_type_id: Filter by numeric ContentType ID (e.g., 21 for dcim.device) + Note: This expects a numeric ID, not an object type string - changed_object_id: Filter by ID of the changed object - object_repr: Filter by object representation (usually contains object name) - action: Filter by action type (created, updated, deleted) @@ -376,9 +377,12 @@ def netbox_get_changelogs(filters: dict): - time_after: Filter for changes made after a given time (ISO 8601 format) - q: Search term to filter by object representation - Example: - To find all changes made to a specific device with ID 123: - {"changed_object_type_id": "dcim.device", "changed_object_id": 123} + Examples: + To find all changes made to a specific object by ID: + {"changed_object_id": 123} + + To find changes by object name pattern: + {"object_repr": "router-01"} To find all deletions in the last 24 hours: {"action": "delete", "time_after": "2023-01-01T00:00:00Z"} @@ -505,7 +509,11 @@ def _endpoint_for_type(object_type: str) -> str: """ return NETBOX_OBJECT_TYPES[object_type]['endpoint'] -if __name__ == "__main__": + +def main() -> None: + """Main entry point for the MCP server.""" + global netbox + cli_overlay: dict[str, Any] = parse_cli_args() try: @@ -561,3 +569,7 @@ def _endpoint_for_type(object_type: str) -> str: except Exception as e: logger.error(f"Failed to start MCP server: {e}") sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_brief.py b/tests/test_brief.py index 92eefe0..8b60f2a 100644 --- a/tests/test_brief.py +++ b/tests/test_brief.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from server import netbox_get_object_by_id, netbox_get_objects +from netbox_mcp_server.server import netbox_get_object_by_id, netbox_get_objects -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_brief_false_omits_parameter_get_objects(mock_netbox): """When brief=False (default), should not include brief in API params for netbox_get_objects.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -19,7 +19,7 @@ def test_brief_false_omits_parameter_get_objects(mock_netbox): assert "brief" not in params -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_brief_default_omits_parameter_get_objects(mock_netbox): """When brief not specified (uses default False), should not include brief in API params.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -33,7 +33,7 @@ def test_brief_default_omits_parameter_get_objects(mock_netbox): assert "brief" not in params -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_brief_true_includes_parameter_get_objects(mock_netbox): """When brief=True, should pass 'brief': '1' to API params for netbox_get_objects.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -46,7 +46,7 @@ def test_brief_true_includes_parameter_get_objects(mock_netbox): assert params["brief"] == "1" -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_brief_false_omits_parameter_get_by_id(mock_netbox): """When brief=False (default), should not include brief in API params for netbox_get_object_by_id.""" mock_netbox.get.return_value = {"id": 1, "name": "Test Site"} @@ -60,7 +60,7 @@ def test_brief_false_omits_parameter_get_by_id(mock_netbox): assert "brief" not in params -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_brief_default_omits_parameter_get_by_id(mock_netbox): """When brief not specified (uses default False), should not include brief in API params.""" mock_netbox.get.return_value = {"id": 1, "name": "Test Site"} @@ -74,7 +74,7 @@ def test_brief_default_omits_parameter_get_by_id(mock_netbox): assert "brief" not in params -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_brief_true_includes_parameter_get_by_id(mock_netbox): """When brief=True, should pass 'brief': '1' to API params for netbox_get_object_by_id.""" mock_netbox.get.return_value = {"id": 1, "url": "http://example.com/api/dcim/sites/1/"} diff --git a/tests/test_config.py b/tests/test_config.py index 4d9c66d..1a15aae 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,8 +7,8 @@ import pytest from pydantic import ValidationError -from config import Settings, configure_logging -from server import parse_cli_args +from netbox_mcp_server.config import Settings, configure_logging +from netbox_mcp_server.server import parse_cli_args def test_settings_requires_netbox_url(): diff --git a/tests/test_filter_validation.py b/tests/test_filter_validation.py index 6268683..8e8bd12 100644 --- a/tests/test_filter_validation.py +++ b/tests/test_filter_validation.py @@ -2,7 +2,7 @@ import pytest -from server import validate_filters +from netbox_mcp_server.server import validate_filters def test_direct_field_filters_pass(): diff --git a/tests/test_ordering.py b/tests/test_ordering.py index 02a53e5..5e997c5 100644 --- a/tests/test_ordering.py +++ b/tests/test_ordering.py @@ -5,7 +5,7 @@ import pytest from pydantic import TypeAdapter, ValidationError -from server import netbox_get_objects +from netbox_mcp_server.server import netbox_get_objects def test_ordering_rejects_invalid_types(): @@ -22,7 +22,7 @@ def test_ordering_rejects_invalid_types(): with pytest.raises(ValidationError): adapter.validate_python(["name", 123]) -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_ordering_none_omits_parameter(mock_netbox): """When ordering=None, should not include ordering in API params.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -36,7 +36,7 @@ def test_ordering_none_omits_parameter(mock_netbox): assert "ordering" not in params -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_ordering_empty_string_omits_parameter(mock_netbox): """When ordering='', should not include ordering in API params.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -50,7 +50,7 @@ def test_ordering_empty_string_omits_parameter(mock_netbox): assert "ordering" not in params -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_ordering_single_field_ascending(mock_netbox): """When ordering='name', should pass 'name' to API params.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -63,7 +63,7 @@ def test_ordering_single_field_ascending(mock_netbox): assert params["ordering"] == "name" -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_ordering_single_field_descending(mock_netbox): """When ordering='-id', should pass '-id' to API params.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -76,7 +76,7 @@ def test_ordering_single_field_descending(mock_netbox): assert params["ordering"] == "-id" -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_ordering_multiple_fields_as_list(mock_netbox): """When ordering=['facility', '-name'], should pass comma-separated string.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} @@ -90,7 +90,7 @@ def test_ordering_multiple_fields_as_list(mock_netbox): assert params["ordering"] == "facility,-name" -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_ordering_empty_list_omits_parameter(mock_netbox): """When ordering=[], should not include ordering in API params.""" mock_netbox.get.return_value = {"count": 0, "results": [], "next": None, "previous": None} diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 537a4f2..c0ec160 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -5,7 +5,7 @@ import pytest from pydantic import TypeAdapter, ValidationError -from server import netbox_get_objects +from netbox_mcp_server.server import netbox_get_objects def test_limit_validation_rejects_values_over_100(): diff --git a/tests/test_search.py b/tests/test_search.py index 96ea7b1..67c0f76 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -5,8 +5,8 @@ import pytest from pydantic import TypeAdapter, ValidationError -from netbox_types import NETBOX_OBJECT_TYPES -from server import netbox_search_objects +from netbox_mcp_server.netbox_types import NETBOX_OBJECT_TYPES +from netbox_mcp_server.server import netbox_search_objects # ============================================================================ # Parameter Validation Tests @@ -41,7 +41,7 @@ def test_invalid_object_type_raises_error(): # ============================================================================ -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_searches_default_types_when_none_specified(mock_netbox): """When object_types=None, should search 8 default common types.""" mock_netbox.get.return_value = { @@ -59,7 +59,7 @@ def test_searches_default_types_when_none_specified(mock_netbox): assert len(result) == 8 -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_custom_object_types_limits_search_scope(mock_netbox): """When object_types specified, should only search those types.""" mock_netbox.get.return_value = { @@ -81,7 +81,7 @@ def test_custom_object_types_limits_search_scope(mock_netbox): # ============================================================================ -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_field_projection_applied_to_queries(mock_netbox): """When fields specified, should apply to all queries as comma-separated string.""" mock_netbox.get.return_value = { @@ -106,7 +106,7 @@ def test_field_projection_applied_to_queries(mock_netbox): # ============================================================================ -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_result_structure_with_empty_and_populated_results(mock_netbox): """Should return dict with all types as keys, empty lists for no matches.""" @@ -140,7 +140,7 @@ def mock_get_side_effect(endpoint, params): # ============================================================================ -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_continues_searching_when_one_type_fails(mock_netbox): """If one object type fails, should continue searching others.""" @@ -171,7 +171,7 @@ def mock_get_side_effect(endpoint, params): # ============================================================================ -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_api_parameters_passed_correctly(mock_netbox): """Should pass query, limit, and fields to NetBox API correctly.""" mock_netbox.get.return_value = { @@ -193,7 +193,7 @@ def test_api_parameters_passed_correctly(mock_netbox): assert params["fields"] == "id" -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_uses_correct_api_endpoints(mock_netbox): """Should use correct API endpoints from NETBOX_OBJECT_TYPES mapping.""" mock_netbox.get.return_value = { @@ -215,7 +215,7 @@ def test_uses_correct_api_endpoints(mock_netbox): # ============================================================================ -@patch("server.netbox") +@patch("netbox_mcp_server.server.netbox") def test_extracts_results_from_paginated_response(mock_netbox): """Should extract 'results' array from NetBox paginated response structure. diff --git a/uv.lock b/uv.lock index 3f692e4..65a3ec0 100644 --- a/uv.lock +++ b/uv.lock @@ -101,14 +101,26 @@ wheels = [ [[package]] name = "click" -version = "8.2.2" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/87/105111999772ec9730e3d4d910c723ea9763ece2ec441533a5cea1e87e3c/click-8.2.2.tar.gz", hash = "sha256:068616e6ef9705a07b6db727cb9c248f4eb9dae437a30239f56fa94b18b852ef", size = 263977, upload-time = "2025-08-02T02:23:41.102Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/85/e7297e34133ae1cfde3bffd30c24e1ef055248251baa877834e048687a28/click-8.2.2-py3-none-any.whl", hash = "sha256:52e1e9f5d3db8c85aa76968c7c67ed41ddbacb167f43201511c8fd61eb5ba2ca", size = 103900, upload-time = "2025-08-02T02:23:39.299Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click-option-group" +version = "0.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, ] [[package]] @@ -211,6 +223,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/8b/2c95f0645c6f40211896375e6fa51f504b8ccb29c21f6ae661fe87ab044e/cyclopts-3.24.0-py3-none-any.whl", hash = "sha256:809d04cde9108617106091140c3964ee6fceb33cecdd537f7ffa360bde13ed71", size = 86154, upload-time = "2025-09-08T15:40:56.41Z" }, ] +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -238,6 +262,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, ] +[[package]] +name = "dotty-dict" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/ab/88d67f02024700b48cd8232579ad1316aa9df2272c63049c27cc094229d6/dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15", size = 7699, upload-time = "2022-07-09T18:50:57.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, +] + [[package]] name = "email-validator" version = "2.3.0" @@ -282,6 +315,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/c7/562ff39f25de27caec01e4c1e88cbb5fcae5160802ba3d90be33165df24f/fastmcp-2.12.4-py3-none-any.whl", hash = "sha256:56188fbbc1a9df58c537063f25958c57b5c4d715f73e395c41b51550b247d140", size = 329090, upload-time = "2025-09-26T16:43:25.314Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -337,6 +394,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -355,6 +421,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jsonschema" version = "4.25.0" @@ -502,7 +580,7 @@ wheels = [ [[package]] name = "netbox-mcp-server" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "fastmcp" }, { name = "httpx" }, @@ -515,6 +593,7 @@ dependencies = [ dev = [ { name = "pytest" }, { name = "pytest-cov" }, + { name = "python-semantic-release" }, ] [package.metadata] @@ -530,6 +609,7 @@ requires-dist = [ dev = [ { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "python-semantic-release", specifier = ">=10.4.1" }, ] [[package]] @@ -757,6 +837,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] +[[package]] +name = "python-gitlab" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/bd/b30f1d3b303cb5d3c72e2d57a847d699e8573cbdfd67ece5f1795e49da1c/python_gitlab-6.5.0.tar.gz", hash = "sha256:97553652d94b02de343e9ca92782239aa2b5f6594c5482331a9490d9d5e8737d", size = 400591, upload-time = "2025-10-17T21:40:02.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/bd/b0d440685fbcafee462bed793a74aea88541887c4c30556a55ac64914b8d/python_gitlab-6.5.0-py3-none-any.whl", hash = "sha256:494e1e8e5edd15286eaf7c286f3a06652688f1ee20a49e2a0218ddc5cc475e32", size = 144419, upload-time = "2025-10-17T21:40:01.233Z" }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -766,6 +859,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-semantic-release" +version = "10.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "click-option-group" }, + { name = "deprecated" }, + { name = "dotty-dict" }, + { name = "gitpython" }, + { name = "importlib-resources" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "python-gitlab" }, + { name = "requests" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/4e/ee80b30d85987414cdb2961797877177f65cb4213e1bf3cdae8143da7729/python_semantic_release-10.4.1.tar.gz", hash = "sha256:4bec21f7d3a419a2a62d16a9ff404481a90f011c762aef605caf48f8c11b3ed6", size = 605074, upload-time = "2025-09-13T03:29:58.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/e8/22fcba61fe7cb4cd5e0f0b6d4e0d02de3e68f83193dcb05ad87be11ed8d1/python_semantic_release-10.4.1-py3-none-any.whl", hash = "sha256:18a73619ffc6f1aca8e1106b03e139686bfbbf0120d1a97c948fc9620ab6beb5", size = 149618, upload-time = "2025-09-13T03:29:56.553Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -822,6 +939,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + [[package]] name = "rfc3339-validator" version = "0.1.4" @@ -895,6 +1024,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -904,6 +1042,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -937,6 +1084,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, ] +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1" @@ -991,3 +1147,22 @@ sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7 wheels = [ { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, ] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +]