Skip to main content

vs-graph

JSON-driven LangGraph wrapper for building agentic workflows — define graph topology in JSON, implement nodes and edges in Python, and get a fully wired, observable, checkpointed execution graph.


Overview

vs-graph wraps LangGraph and eliminates the boilerplate of building agentic graphs: wiring nodes, routing edges, setting up checkpointers, and propagating trace events. You declare the graph structure in a JSON file, implement node logic by extending VsBaseNode, and wire routing logic by extending VsBaseConditionalEdge or VsDynamicFanOutEdge. The library handles everything else.

The @node and @edge decorators self-register implementations at import time. The @graph decorator on a graph class specifies which packages to auto-import so no manual import list is required.


The Problem It Solves

Without vs-graph

# Manual LangGraph wiring — every graph requires this boilerplate
from langgraph.graph import StateGraph, START, END

sg = StateGraph(MyState)
sg.add_node("classifier", classifier_node.invoke)
sg.add_node("executor", executor_node.invoke)
sg.add_node("responder", responder_node.invoke)
sg.add_edge(START, "classifier")
sg.add_conditional_edges("classifier", routing_fn, {"execute": "executor", "default": "responder"})
sg.add_edge("executor", "responder")
sg.add_edge("responder", END)
checkpointer = MemorySaver()
graph = sg.compile(checkpointer=checkpointer)

Every graph has this same boilerplate. Adding a node requires updating both code and wiring.

With vs-graph

# graph topology is in JSON
# Python only contains business logic

@graph(nodes=["myapp.graph.nodes"], edges=["myapp.graph.edges"])
class MyGraph(VsBaseGraph[MyState]):
    def __init__(self):
        with open("my_graph.json") as f:
            graph_json = json.load(f)
        super().__init__(graph_json=graph_json, state_class=MyState)
        self.build()

graph = MyGraph()
result = await graph.invoke({"trace_id": "", "error": None, "user_input": "hello"})

How It All Fits Together

@graph(nodes=[...], edges=[...])
class MyGraph(VsBaseGraph[MyState])
    └── __init__: load JSON, call build()

build()
    ├── _auto_import_packages()     # walks node/edge packages, triggers @node/@edge registration
    ├── _instantiate_nodes()        # VsNodeRegistry.create() for each JSON node definition
    ├── _instantiate_edges()        # VsEdgeRegistry.create() for conditional/dynamic_fan_out
    ├── _validate_graph()           # checks all node ids, edge targets, and exit nodes
    ├── _load_checkpointer()        # reads config.ini [graph] section
    └── _build_langgraph()          # compiles StateGraph with checkpointer

invoke(state)
    ├── auto-generates trace_id if not set
    └── StateGraph.ainvoke(state)
            └── per node: VsBaseNode.invoke()
                    ├── trace: "node started"
                    ├── execute(state)          ← your business logic
                    └── trace: "node completed" / "node failed"
            └── per conditional / dynamic_fan_out edge: async VsBaseEdge.route()
                    ├── _evaluate(state) / get_sends(state)   ← your routing logic
                    └── trace: "routing to next_node" / "fanning out to N branch(es)"
            (direct and fan_out edges are plain LangGraph edges — no wrapper, no trace)

Graph JSON

The JSON file declares the graph topology. Python classes are referenced by the name field in each node/edge definition — matching the name passed to @node(name="...") or @edge(name="...").

{
  "version": "1.0",
  "name": "my_graph",
  "entry_node": "classifier",
  "exit_nodes": ["responder"],
  "nodes": [
    { "id": "classifier", "name": "classifier_node", "config": {} },
    { "id": "executor",   "name": "executor_node",   "config": {} },
    { "id": "responder",  "name": "responder_node",  "config": {} }
  ],
  "edges": [
    {
      "type": "conditional",
      "source": "classifier",
      "edge_class": "classifier_routing_edge",
      "route": {
        "execute": "executor",
        "default": "responder"
      }
    },
    { "type": "direct", "source": "executor", "target": "responder" }
  ]
}

JSON Fields

Field Required Description
name Yes Graph name — used in logs and error messages
version No Version string (default "1.0")
entry_node Yes id of the node that receives the initial state
exit_nodes Yes List of node ids that connect to END
nodes Yes Array of node definitions
nodes[].id Yes Unique node id within this graph — used in edge source/target fields
nodes[].name Yes Registry name — must match the @node(name="...") decorator value
nodes[].config No Arbitrary dict passed to the node constructor as self.config
edges No Array of edge definitions
edges[].type Yes direct, fan_out, conditional, or dynamic_fan_out
edges[].source Yes Node id this edge originates from
edges[].target For direct Target node id
edges[].targets For fan_out Array of target node ids (minimum 2)
edges[].edge_class For conditional, dynamic_fan_out Registry name matching @edge(name="...")
edges[].route For conditional Map of route keys to target node ids — must include "default"
edges[].config No Arbitrary dict passed to the edge constructor as self.config

Edge Types

Type Python class needed Description
direct No Fixed single target — fully declared in JSON
fan_out No Fixed parallel targets — fully declared in JSON
conditional Yes — extends VsBaseConditionalEdge _evaluate(state) returns a route key; key is looked up in route map
dynamic_fan_out Yes — extends VsDynamicFanOutEdge get_sends(state) returns a list of Send objects for runtime branching

State

All graph state must extend VsBaseGraphState:

from typing import Optional
from vs_graph.schema.vs_base_graph_state import VsBaseGraphState

class MyState(VsBaseGraphState):
    user_input: str
    intent: Optional[str]
    response: Optional[str]

VsBaseGraphState is a TypedDict providing:

Field Type Description
trace_id str Auto-generated per invoke() call if not set. Links all trace events for this execution.
error Optional[VsGraphError] Populated by the framework when a node raises VsUserException. Contains message, cause, stack_trace.
current_message Optional[str] Convenience field for the current user-facing message. Not set by the framework.

Nodes

Extend VsBaseNode and implement execute. Decorate with @node(name="...") to self-register in VsNodeRegistry.

from vs_graph.node.vs_base_node import VsBaseNode
from vs_graph.decorator.vs_node_decorator import node
from vs_graph.exception.vs_graph_exceptions import VsUserException, VsNodeExecutionException

@node(name="classifier_node")
class ClassifierNode(VsBaseNode[MyState]):

    async def execute(self, state: MyState) -> dict:
        if not state.get("user_input"):
            raise VsUserException("Input is required.")
        state["intent"] = "execute"
        await self.trace(state["trace_id"], "intent classified", data={"intent": state["intent"]})
        return state

@node decorator

@node(name="classifier_node")   # registers under key "classifier_node"
@node                           # registers under the class name

The name value must match the "name" field in the JSON node definition.

What invoke() does (called by the graph, not you)

VsBaseNode.invoke() wraps your execute():

  1. Logs and publishes trace: "node '{id}' started"
  2. Calls await self.execute(state)
  3. On success: logs and publishes trace: "node '{id}' completed" with duration_seconds
  4. On VsUserException: stores error in state["error"] and returns state (graph continues)
  5. On VsNodeExecutionException: logs error and re-raises (graph stops)
  6. On any other exception: logs error and re-raises (graph stops)

Publishing trace events

async def execute(self, state: MyState) -> dict:
    await self.trace(
        state["trace_id"],
        "retrieved context",
        data={"num_chunks": 8},
    )
    return state

self.trace() is a no-op if no trace_publisher was passed to the graph.

Node constructor args

VsBaseNode.__init__ receives:

Parameter Source
node_id The id field from the JSON node definition
config The config dict from the JSON node definition
trace_publisher The publisher passed to VsBaseGraph.__init__

Access them as self.node_id, self.config, self.trace_publisher.


Prebuilt Nodes

Some nodes are the same in every agentic graph — only the "fetch from my store" part changes. vs-graph ships these half-built: the orchestration, tracing, and skip logic are done; you implement one method.

They're normal VsBaseNode subclasses, so you still decorate your concrete class with @node(name="...") and reference it in the JSON like any other node.

VsHydrationNode

The typical first node in a conversational graph. LangGraph loads the checkpoint for the current thread_id into state before the graph runs — but a checkpoint only exists while it's warm. New conversation, or Redis TTL expired after 30 days of silence, means an empty state. VsHydrationNode rebuilds it from your durable store (ctx-resolver, DB, …) so the agent keeps its memory across gaps the checkpointer can't cover.

execute() is already written:

  1. is_hydrated(state) → if true, the checkpoint was warm, return {} and move on
  2. otherwise await load(state) and return the delta — LangGraph checkpoints it from there

You implement load:

from vs_graph.node.vs_hydration_node import VsHydrationNode
from vs_graph.decorator.vs_node_decorator import node

@node(name="hydration_node")
class ConversationHydrationNode(VsHydrationNode[MyState]):

    hydration_keys = ["history", "resolved_context"]

    async def load(self, state: MyState) -> dict:
        cid = state["conversation_id"]
        return {
            "history": await conversation_store.get_history(cid),
            "resolved_context": await ctx_resolver.resolve(cid),
        }
Member Kind Description
load(state) Abstract — you implement Fetch prior context from the durable store (key off conversation_id / user_id in state) and return it as a state delta.
hydration_keys Class attr State keys that, once all non-None, mean hydration already happened. Set this and you never touch is_hydrated.
is_hydrated(state) Override (optional) Default: all hydration_keys are non-None. Override for custom "is the state warm?" logic.
execute(state) Provided — don't override The skip-or-load template above.

Emits node '{id}' hydrated or node '{id}' already hydrated trace events.


Edges

Direct edge

No Python class. Declared fully in JSON:

{ "type": "direct", "source": "executor", "target": "responder" }

Fan-out edge

No Python class. Declares fixed parallel targets in JSON (minimum 2):

{
  "type": "fan_out",
  "source": "splitter",
  "targets": ["worker_a", "worker_b", "worker_c"]
}

All targets receive the same state and execute in parallel via LangGraph's native fan-out.

Conditional edge

Extend VsBaseConditionalEdge and implement _evaluate. Decorate with @edge(name="..."):

from vs_graph.edge.vs_base_conditional_edge import VsBaseConditionalEdge
from vs_graph.decorator.vs_edge_decorator import edge

@edge(name="classifier_routing_edge")
class ClassifierRoutingEdge(VsBaseConditionalEdge[MyState]):

    def _evaluate(self, state: MyState) -> str:
        if state.get("intent") == "execute":
            return "execute"
        return "default"

_evaluate returns a route key. The base class's async route() wrapper looks it up in the route map from JSON, publishes the routing trace, and returns the target node id. If the key is not in the map, it falls back to "default". A "default" entry in the JSON route map is required.

JSON:

{
  "type": "conditional",
  "source": "classifier",
  "edge_class": "classifier_routing_edge",
  "route": {
    "execute": "executor",
    "default": "responder"
  }
}

Dynamic fan-out edge

Extend VsDynamicFanOutEdge and implement get_sends. Returns a list of LangGraph Send objects — each specifies a target node and a custom state for that branch:

from vs_graph.edge.vs_dynamic_fan_out_edge import VsDynamicFanOutEdge
from vs_graph.decorator.vs_edge_decorator import edge
from langgraph.types import Send

@edge(name="document_fan_out_edge")
class DocumentFanOutEdge(VsDynamicFanOutEdge[MyState]):

    def get_sends(self, state: MyState) -> list:
        return [
            Send("process_document", {**state, "document": doc})
            for doc in state["documents"]
        ]

JSON:

{
  "type": "dynamic_fan_out",
  "source": "splitter",
  "edge_class": "document_fan_out_edge"
}

@edge decorator

@edge(name="classifier_routing_edge")   # registers under key "classifier_routing_edge"
@edge                                   # registers under the class name

The name value must match the "edge_class" field in the JSON edge definition.


@graph Decorator — Auto-Import Packages

Instead of manually importing every node and edge module, use @graph on your graph class to declare which packages contain your implementations. The library scans and imports them automatically at build() time, triggering @node and @edge registration.

from vs_graph.decorator.vs_graph_decorator import graph
from vs_graph.vs_base_graph import VsBaseGraph

@graph(
    nodes=["myapp.graph.nodes"],
    edges=["myapp.graph.edges"],
)
class MyGraph(VsBaseGraph[MyState]):

    def __init__(self):
        with open("my_graph.json") as f:
            graph_json = json.load(f)
        super().__init__(graph_json=graph_json, state_class=MyState)
        self.build()

Both nodes and edges accept a list of package paths. Each package is walked recursively — all modules within it are imported. You can specify multiple packages:

@graph(
    nodes=["myapp.graph.nodes", "shared.common_nodes"],
    edges=["myapp.graph.edges"],
)
class MyGraph(VsBaseGraph[MyState]):
    ...

Without @graph, node and edge modules must be imported manually before build() is called:

import myapp.graph.nodes.classifier_node  # noqa
import myapp.graph.nodes.executor_node    # noqa

@graph with no packages, or subclasses without the decorator, are both valid — auto-import is skipped.


Registries

VsNodeRegistry

VsNodeRegistry is a static registry. @node populates it at import time.

Method Description
register(name, node_class) Called by @node — raises ValueError if name already registered
get(name) Returns the class — raises ValueError if not found
create(name, node_id, config, **kwargs) Instantiates the node class
is_registered(name) Returns True if name is registered
list_nodes() Returns sorted list of registered names
clear() Removes all registered nodes — for testing only

VsEdgeRegistry

Same pattern as VsNodeRegistry for edge classes.

Method Description
register(name, edge_class) Called by @edge — raises ValueError if name already registered
get(name) Returns the class — raises ValueError if not found
create(name, source, **kwargs) Instantiates the edge class
is_registered(name) Returns True if name is registered
list_edges() Returns sorted list of registered names
clear() Removes all registered edges — for testing only

Checkpointer

Configured via config.ini. Provides LangGraph thread-level state persistence across invoke() calls. The thread key defaults to state["trace_id"]; override it with config={"configurable": {"thread_id": ...}} on invoke().

[graph]
checkpointer = memory
[graph]
checkpointer             = redis
redis_url                = redis://localhost:6379
checkpoint_key_prefix    = vs:checkpoint:
checkpoint_ttl_seconds   = 3600
Key Default Description
graph.checkpointer memory Checkpointer type: memory or redis
graph.redis_url redis://localhost:6379 Redis connection URL (redis only)
graph.checkpoint_key_prefix vs:checkpoint: Key prefix for stored checkpoints (redis only)
graph.checkpoint_ttl_seconds None TTL for checkpoint keys in seconds (redis only)

Custom checkpointers can be registered via VsCheckpointerFactory:

from vs_graph.checkpointer.vs_checkpointer_factory import VsCheckpointerFactory

VsCheckpointerFactory.register(
    "postgres",
    lambda cfg: MyPostgresCheckpointer(config=cfg)
)

The factory lambda receives the same cfg dict that _load_checkpointer builds from config.ini.


Trace Publisher

Pass a VsBaseTracePublisher to the graph constructor to stream execution events to any external sink (queue, websocket, database):

from vs_common.publisher.vs_base_trace_publisher import VsBaseTracePublisher
from vs_common.schema.vs_trace_level import VsTraceLevel
from typing import Any, Dict, Optional

class MyTracePublisher(VsBaseTracePublisher):

    async def publish(
        self,
        trace_id: str,
        message: str,
        level: VsTraceLevel = VsTraceLevel.INFO,
        data: Optional[Dict[str, Any]] = None,
    ) -> None:
        await my_queue.push({
            "trace_id": trace_id,
            "message": message,
            "level": level.value,
            "data": data or {},
        })
graph = MyGraph(graph_json, state_class=MyState, trace_publisher=MyTracePublisher())

The publisher is automatically propagated to every node and edge instance. You do not pass it to nodes or edges directly.

Events published automatically by the framework:

Event Source
"node '{id}' started" VsBaseNode.invoke()
"node '{id}' completed" with duration_seconds VsBaseNode.invoke()
"node '{id}' user error" VsBaseNode.invoke() on VsUserException
"node '{id}' failed" VsBaseNode.invoke() on exception
"edge '{source}' routing to '{next_node}'" with route_key, next_node, duration_seconds VsBaseConditionalEdge.route()
"edge '{source}' fanning out to {n} branch(es)" with branch_count, targets, duration_seconds VsDynamicFanOutEdge.route()

Direct and static fan_out edges are pure topology — they compile to plain LangGraph edges and emit no trace events. The routing wrappers are async and await the trace publish before returning the next node, so the routing event is always ordered ahead of the next node's "started" event.


Exceptions

Exception When to raise Effect
VsUserException Error is safe to surface to the user Stored in state["error"] — graph continues to next node
VsNodeExecutionException Internal node failure Logged and re-raised — graph stops
VsGraphBuildException Invalid build state Raised during build()
VsEdgeEvaluationException Edge routing failure Raised from edge evaluate()
from vs_graph.exception.vs_graph_exceptions import VsUserException, VsNodeExecutionException

async def execute(self, state: MyState) -> dict:
    if not state.get("user_input"):
        raise VsUserException("Please provide an input.")

    try:
        result = await call_external_service()
    except Exception as e:
        raise VsNodeExecutionException(f"External service failed: {e}") from e

    state["result"] = result
    return state

state["error"] shape when VsUserException is raised:

{
  "message": "Please provide an input.",
  "cause": null,
  "stack_trace": null
}

Full Example

import json
from typing import Optional

from vs_graph.vs_base_graph import VsBaseGraph
from vs_graph.schema.vs_base_graph_state import VsBaseGraphState
from vs_graph.node.vs_base_node import VsBaseNode
from vs_graph.edge.vs_base_conditional_edge import VsBaseConditionalEdge
from vs_graph.decorator.vs_node_decorator import node
from vs_graph.decorator.vs_edge_decorator import edge
from vs_graph.decorator.vs_graph_decorator import graph
from vs_graph.exception.vs_graph_exceptions import VsUserException


# --- State ---

class MyState(VsBaseGraphState):
    user_input: str
    intent: Optional[str]
    response: Optional[str]


# --- Nodes ---

@node(name="classifier_node")
class ClassifierNode(VsBaseNode[MyState]):

    async def execute(self, state: MyState) -> dict:
        if not state.get("user_input"):
            raise VsUserException("Input is required.")
        state["intent"] = "search" if "find" in state["user_input"] else "chat"
        await self.trace(state["trace_id"], "classified", data={"intent": state["intent"]})
        return state


@node(name="responder_node")
class ResponderNode(VsBaseNode[MyState]):

    async def execute(self, state: MyState) -> dict:
        state["response"] = f"Handling intent: {state['intent']}"
        return state


# --- Edges ---

@edge(name="classifier_routing_edge")
class ClassifierRoutingEdge(VsBaseConditionalEdge[MyState]):

    def _evaluate(self, state: MyState) -> str:
        return state.get("intent", "default")


# --- Graph ---

@graph(nodes=["myapp.graph.nodes"], edges=["myapp.graph.edges"])
class MyGraph(VsBaseGraph[MyState]):

    def __init__(self):
        graph_json = {
            "version": "1.0",
            "name": "my_graph",
            "entry_node": "classifier",
            "exit_nodes": ["responder"],
            "nodes": [
                {"id": "classifier", "name": "classifier_node", "config": {}},
                {"id": "responder",  "name": "responder_node",  "config": {}},
            ],
            "edges": [
                {
                    "type": "conditional",
                    "source": "classifier",
                    "edge_class": "classifier_routing_edge",
                    "route": {"search": "responder", "chat": "responder", "default": "responder"}
                }
            ]
        }
        super().__init__(graph_json=graph_json, state_class=MyState)
        self.build()


# --- Run ---

import asyncio

async def main():
    g = MyGraph()
    result = await g.invoke({
        "trace_id": "",
        "error": None,
        "current_message": None,
        "user_input": "find me a product",
        "intent": None,
        "response": None,
    })
    print(result["response"])  # Handling intent: search

asyncio.run(main())

Class Reference


VsBaseGraph

Abstract base class for all graphs. Extend it and call build() in __init__.

Constructor:

Parameter Type Description
graph_json Dict[str, Any] Parsed graph JSON — validated on construction
state_class Type[S] TypedDict subclass used as LangGraph state type
trace_publisher Optional[VsBaseTracePublisher] Publisher injected into all nodes and edges

Methods:

Method Description
build() Auto-imports packages, instantiates nodes/edges, validates, loads checkpointer, compiles LangGraph. Call once at startup.
async invoke(state, config=None) Runs the compiled graph. Auto-generates trace_id if not set. Sets the checkpointer thread_id to state["trace_id"] unless you pass config={"configurable": {"thread_id": ...}} — an explicit thread_id wins, but a configurable dict without one still gets trace_id merged in.

VsHydrationNode

Prebuilt first node. Rebuilds state from a durable store when the checkpoint is cold (new thread or expired TTL). See Prebuilt Nodes.

Member Kind Description
load(state) Abstract Fetch prior context from your store, return a state delta.
hydration_keys Class attr (List[str]) Keys that mean "already hydrated" when all non-None.
is_hydrated(state) Optional override Default checks hydration_keys.
execute(state) Provided Skip-or-load template — do not override.

VsBaseNode

Abstract base class for all nodes.

Constructor parameters (injected by VsNodeRegistry.create()):

Parameter Type Description
node_id str Node id from JSON
config Optional[Dict[str, Any]] Node config dict from JSON
trace_publisher Optional[VsBaseTracePublisher] Propagated from the graph

Instance attributes:

Attribute Description
self.node_id The node id from JSON
self.config The config dict from JSON
self.trace_publisher The graph-level trace publisher
self.logger Logger instance keyed to the class name

Abstract method:

Method Signature Description
execute async execute(state: S) -> dict Implement your node logic here. Return the updated state dict.

Helper method:

Method Signature Description
trace async trace(trace_id, message, data=None, level=INFO) Publishes a trace event. No-op if no publisher.

VsBaseConditionalEdge

Abstract base class for conditional routing edges.

Constructor parameters:

Parameter Type Description
source str Source node id
route Dict[str, str] Map of route keys to target node ids — must include "default". Stored as self.route_map.
config Optional[Dict[str, Any]] Edge config dict from JSON
trace_publisher Optional[VsBaseTracePublisher] Propagated from the graph

Abstract method:

Method Signature Description
_evaluate _evaluate(state: S) -> str Return a route key. route() looks it up in route_map; falls back to "default".

Routing function (registered with LangGraph — you do not call it):

Method Signature Description
route async route(state: S) -> str Calls _evaluate, resolves the key against route_map (falling back to "default"), publishes the routing trace, returns the target node id. Raises if there is no default.

VsDynamicFanOutEdge

Abstract base class for runtime fan-out edges.

Abstract method:

Method Signature Description
get_sends get_sends(state: S) -> List Return a list of langgraph.types.Send objects. Each Send specifies a target node and a custom state dict for that branch.

Routing function (registered with LangGraph — you do not call it):

Method Signature Description
route async route(state: S) -> List Calls get_sends, publishes a fan-out trace with branch_count and targets, returns the Send list.

VsCheckpointerFactory

Static factory for checkpointer registration and creation.

Method Description
register(name, factory_fn) Register a custom checkpointer. factory_fn receives a config dict.
create(checkpointer_type, config) Instantiate and return the checkpointer.

Built-in types: "memory", "redis".


Running Tests

./run_tests.sh

Metadata

Release files for vs-graph 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vs-graph 0.1.1
File Size Uploaded Provenance
vs_graph-0.1.1.tar.gz 35.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for vs-graph 0.1.1
File Interpreter ABI Platform
vs_graph-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.4 kB

Release files / vs_graph-0.1.1.tar.gz

Download URL vs_graph-0.1.1.tar.gz
Size 35.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d29128cfb394b40459201fc0772829ee94a4deb957a80797b1ccbf28a7d2caf5
BLAKE2b-256 checksum
How to use checksums
8b9cae54022ed8b30a47426b351382faa6953ffa04ad073506c033acab2e5110
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / vs_graph-0.1.1-py3-none-any.whl

Download URL vs_graph-0.1.1-py3-none-any.whl
Size 34.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
74a6e814e66b39b46ebc20dee1f49b73d5407648306a7e1c2ba1716cdc132a2f
BLAKE2b-256 checksum
How to use checksums
c96c1e5f6cf8749d3af1825e1875d5ed331e483fe5e07371abf6c5475dd31617
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for vs-graph 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page