Merge pull request 'feat: Add gNMI discovery CLI tools for YANG path exploration' (#21) from feat-discoverytools into main

Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-01-07 12:00:05 +00:00
12 changed files with 2225 additions and 120 deletions

View File

@@ -70,7 +70,7 @@ Reference: [arista-evpn-vxlan-clab](https://gitea.arnodo.fr/Damien/arista-evpn-v
Progress is tracked via issues. See [all issues](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues) or filter by phase: Progress is tracked via issues. See [all issues](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues) or filter by phase:
| Phase | Description | Issues | | Phase | Description | Issues |
|-------|-------------|--------| | ----------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Phase 1** | YANG Path Discovery - Map EOS 4.35.0F YANG models, validate gNMI | [phase-1-yang-discovery](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=1) | | **Phase 1** | YANG Path Discovery - Map EOS 4.35.0F YANG models, validate gNMI | [phase-1-yang-discovery](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=1) |
| **Phase 2** | Minimal Reconciler - VLANs/VNIs, diff engine, CLI plan/apply | [phase-2-minimal-reconciler](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=2) | | **Phase 2** | Minimal Reconciler - VLANs/VNIs, diff engine, CLI plan/apply | [phase-2-minimal-reconciler](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=2) |
| **Phase 3** | Full Fabric - BGP, MLAG, VRFs, dependency ordering | [phase-3-full-fabric](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=3) | | **Phase 3** | Full Fabric - BGP, MLAG, VRFs, dependency ordering | [phase-3-full-fabric](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=3) |
@@ -116,7 +116,7 @@ fabric-orchestrator/
## 🛠️ Technology Stack ## 🛠️ Technology Stack
| Component | Technology | Purpose | | Component | Technology | Purpose |
|-----------|------------|---------| | --------------- | -------------------------- | ------------------------------------ |
| Source of Truth | NetBox | Intent definition via ConfigContexts | | Source of Truth | NetBox | Intent definition via ConfigContexts |
| Transport | gNMI | Configuration and telemetry | | Transport | gNMI | Configuration and telemetry |
| Data Models | YANG (OpenConfig + Arista) | Structured configuration | | Data Models | YANG (OpenConfig + Arista) | Structured configuration |

626
docs/cli-user-guide.md Normal file
View File

@@ -0,0 +1,626 @@
# Fabric Orchestrator CLI User Guide
This guide covers the `fabric-orch` command-line interface for exploring YANG paths and managing Arista EVPN-VXLAN fabrics via gNMI.
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Commands Overview](#commands-overview)
- [discover capabilities](#discover-capabilities)
- [discover get](#discover-get)
- [discover set](#discover-set)
- [discover subscribe](#discover-subscribe)
- [discover paths](#discover-paths)
- [Output Formats](#output-formats)
- [Error Handling](#error-handling)
- [Best Practices](#best-practices)
---
## Installation
Ensure you have Python 3.12+ and `uv` installed, then:
```bash
# Clone the repository
git clone https://gitea.arnodo.fr/Damien/fabric-orchestrator.git
cd fabric-orchestrator
# Install dependencies
uv sync
# Verify installation
uv run fabric-orch --version
```
---
## Quick Start
```bash
# Set environment variables (optional but recommended)
export GNMI_TARGET="172.16.0.50:6030"
export GNMI_USERNAME="admin"
export GNMI_PASSWORD="admin"
# List device capabilities
uv run fabric-orch discover capabilities
# Get interface state
uv run fabric-orch discover get --path "/interfaces/interface[name=Ethernet1]/state"
# Show common YANG paths reference
uv run fabric-orch discover paths
```
---
## Environment Variables
The CLI supports these environment variables to avoid passing credentials repeatedly:
| Variable | Description | Default |
| --------------- | ----------------------------------- | ---------- |
| `GNMI_TARGET` | Target device in `host:port` format | (required) |
| `GNMI_USERNAME` | Username for gNMI authentication | `admin` |
| `GNMI_PASSWORD` | Password for gNMI authentication | `admin` |
**Example:**
```bash
export GNMI_TARGET="leaf1:6030"
export GNMI_USERNAME="admin"
export GNMI_PASSWORD="admin"
# Now you can omit --target, --username, --password
uv run fabric-orch discover capabilities
```
---
## Commands Overview
```
fabric-orch
└── discover # YANG path discovery tools
├── capabilities # List device capabilities
├── get # Get config/state data
├── set # Set configuration
├── subscribe # Subscribe to updates
└── paths # Show common paths reference
```
---
## discover capabilities
List device capabilities and supported YANG models.
### Usage
```bash
fabric-orch discover capabilities [OPTIONS]
```
### Options
| Option | Short | Description |
| -------------------------- | ----- | -------------------------------------- |
| `--target` | `-t` | Target device (host:port) |
| `--username` | `-u` | Username for authentication |
| `--password` | `-p` | Password for authentication |
| `--insecure/--no-insecure` | | Skip TLS verification (default: True) |
| `--output` | `-o` | Output format: pretty, json, file:path |
### Examples
```bash
# Basic usage with all options
uv run fabric-orch discover capabilities \
--target 172.16.0.50:6030 \
--username admin \
--password admin \
--insecure
# Using environment variables
export GNMI_TARGET="172.16.0.50:6030"
uv run fabric-orch discover capabilities
# Output as JSON
uv run fabric-orch discover capabilities --output json
# Save to file
uv run fabric-orch discover capabilities --output file:capabilities.json
```
### Sample Output
```
Device Capabilities - 172.16.0.50:6030
gNMI Version: 0.8.0
Supported Encodings: json_ietf, json, proto
Supported Models (156):
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Model Name ┃ Organization ┃ Version ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ arista-exp-eos-evpn │ Arista Networks │ 2023-01-01 │
│ arista-exp-eos-mlag │ Arista Networks │ 2023-01-01 │
│ arista-exp-eos-vxlan │ Arista Networks │ 2023-01-01 │
│ openconfig-interfaces │ OpenConfig │ 2.4.3 │
│ openconfig-network-instance │ OpenConfig │ 1.1.0 │
│ ... │ ... │ ... │
└───────────────────────────────────┴─────────────────┴────────────┘
```
---
## discover get
Get configuration or state data at a YANG path.
### Usage
```bash
fabric-orch discover get [OPTIONS]
```
### Options
| Option | Short | Description |
| -------------------------- | ----- | -------------------------------------------- |
| `--target` | `-t` | Target device (host:port) |
| `--username` | `-u` | Username for authentication |
| `--password` | `-p` | Password for authentication |
| `--insecure/--no-insecure` | | Skip TLS verification |
| `--output` | `-o` | Output format: pretty, json, file:path |
| `--path` | `-P` | YANG path to get data from **(required)** |
| `--type` | `-T` | Data type: config, state, all (default: all) |
| `--depth` | `-d` | Maximum depth (not implemented yet) |
### Examples
```bash
# Get all data at a path
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]"
# Get config only
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]" \
--type config
# Get state only
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/state" \
--type state
# Get BGP neighbor state
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state"
# Get VXLAN VNI mappings
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis"
# Save output to file
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/interfaces/interface" \
--output file:interfaces.json
# Machine-readable JSON output
uv run fabric-orch discover get \
--target 172.16.0.50:6030 \
--path "/system/config" \
--output json | jq '.notification[0].update[0].val'
```
### Path Syntax
YANG paths use a specific syntax:
- **Basic path**: `/interfaces/interface`
- **With key**: `/interfaces/interface[name=Ethernet1]`
- **Multiple keys**: `/protocols/protocol[identifier=BGP][name=BGP]`
- **Nested path**: `/interfaces/interface[name=Ethernet1]/state/oper-status`
### Common Paths
| Category | Path | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------- |
| Interfaces | `/interfaces/interface[name=Ethernet1]/state` | Interface state |
| BGP | `/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state` | BGP neighbors |
| VXLAN | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis` | VNI mappings |
| MLAG | `/arista/eos/mlag/config` | MLAG config |
| System | `/system/config/hostname` | Hostname |
---
## discover set
Set configuration at a YANG path.
> ⚠️ **Warning**: This command modifies device configuration. Use with caution!
### Usage
```bash
fabric-orch discover set [OPTIONS]
```
### Options
| Option | Short | Description |
| -------------------------- | ----- | ---------------------------------------------------- |
| `--target` | `-t` | Target device (host:port) |
| `--username` | `-u` | Username for authentication |
| `--password` | `-p` | Password for authentication |
| `--insecure/--no-insecure` | | Skip TLS verification |
| `--output` | `-o` | Output format: pretty, json, file:path |
| `--path` | `-P` | YANG path to set **(required)** |
| `--value` | `-v` | Value to set (JSON or string) **(required)** |
| `--operation` | `-O` | Operation: update, replace, delete (default: update) |
| `--dry-run/--no-dry-run` | | Dry-run mode (default: True) |
### Examples
```bash
# Dry-run (default) - shows what would be set
uv run fabric-orch discover set \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/config/description" \
--value "Uplink to Spine1"
# Actually apply the change
uv run fabric-orch discover set \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/config/description" \
--value "Uplink to Spine1" \
--no-dry-run
# Set JSON value
uv run fabric-orch discover set \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/config" \
--value '{"description": "Uplink", "enabled": true}' \
--no-dry-run
# Replace operation (overwrites existing config)
uv run fabric-orch discover set \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/config" \
--value '{"description": "New config"}' \
--operation replace \
--no-dry-run
# Delete configuration
uv run fabric-orch discover set \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/config/description" \
--value "" \
--operation delete \
--no-dry-run
```
### Operations
| Operation | Description |
| --------- | ------------------------------------------- |
| `update` | Merge with existing configuration (default) |
| `replace` | Replace existing configuration at path |
| `delete` | Delete configuration at path |
### Dry-Run Mode
By default, the `set` command runs in **dry-run mode**, which shows what would be changed without applying it. This is a safety feature.
```bash
# Dry-run output
⚠ DRY-RUN MODE - No changes will be applied
SET (dry-run) /interfaces/interface[name=Ethernet1]/config/description
Operation: update
Value: Uplink to Spine1
Use --no-dry-run to apply this change
```
---
## discover subscribe
Subscribe to YANG path updates in real-time.
### Usage
```bash
fabric-orch discover subscribe [OPTIONS]
```
### Options
| Option | Short | Description |
| -------------------------- | ----- | ------------------------------------------------------------ |
| `--target` | `-t` | Target device (host:port) |
| `--username` | `-u` | Username for authentication |
| `--password` | `-p` | Password for authentication |
| `--insecure/--no-insecure` | | Skip TLS verification |
| `--output` | `-o` | Output format: pretty, json, file:path |
| `--path` | `-P` | YANG path(s) to subscribe to (can repeat) **(required)** |
| `--mode` | `-m` | Mode: on-change, sample, target-defined (default: on-change) |
| `--interval` | `-i` | Sample interval in seconds (for sample mode) |
| `--count` | `-c` | Number of updates before exiting |
### Examples
```bash
# Subscribe to interface operational status changes
uv run fabric-orch discover subscribe \
--target 172.16.0.50:6030 \
--path "/interfaces/interface/state/oper-status" \
--mode on-change
# Sample interface counters every 10 seconds
uv run fabric-orch discover subscribe \
--target 172.16.0.50:6030 \
--path "/interfaces/interface[name=Ethernet1]/state/counters" \
--mode sample \
--interval 10
# Subscribe to multiple paths
uv run fabric-orch discover subscribe \
--target 172.16.0.50:6030 \
--path "/interfaces/interface/state/oper-status" \
--path "/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state/session-state"
# Get only 5 updates then exit
uv run fabric-orch discover subscribe \
--target 172.16.0.50:6030 \
--path "/interfaces/interface/state/counters" \
--mode sample \
--interval 5 \
--count 5
# Output as JSON (useful for piping)
uv run fabric-orch discover subscribe \
--target 172.16.0.50:6030 \
--path "/interfaces/interface/state/oper-status" \
--output json
```
### Subscription Modes
| Mode | Description |
| ---------------- | ------------------------------------------------- |
| `on-change` | Receive updates only when values change (default) |
| `sample` | Receive periodic updates at specified interval |
| `target-defined` | Let the device decide the update strategy |
### Important Note for Arista EOS
For **ON_CHANGE** subscriptions on Arista EOS, use native paths **without** module prefixes:
```bash
# ✅ Correct - native path
--path "/interfaces/interface[name=Ethernet1]/state"
# ❌ Incorrect - with module prefix (may fail)
--path "/openconfig-interfaces:interfaces/interface[name=Ethernet1]/state"
```
### Stopping Subscriptions
Press `Ctrl+C` to stop a subscription gracefully.
---
## discover paths
Display a reference table of commonly used YANG paths for Arista EOS.
### Usage
```bash
uv run fabric-orch discover paths
```
This command doesn't require a target connection - it displays a static reference.
### Example Output
```
Common YANG Paths for Arista EOS
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Category ┃ Path ┃ Notes ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Interfaces │ /interfaces/interface[name=Ethernet1]/state │ Full interface state │
│ Interfaces │ /interfaces/interface[name=Ethernet1]/state/oper-status │ Operational status │
│ Interfaces │ /interfaces/interface[name=Ethernet1]/state/counters │ Interface counters │
│ Loopbacks │ /interfaces/interface[name=Loopback0] │ Loopback interface │
│ VLANs │ /network-instances/network-instance[name=default]/vlans/vlan │ All VLANs │
│ BGP │ .../bgp/neighbors/neighbor/state │ All BGP neighbors │
│ VXLAN │ /interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis │ VLAN-to-VNI mappings │
│ VXLAN │ /interfaces/interface[name=Vxlan1]/arista-vxlan/config │ VXLAN config │
│ MLAG │ /arista/eos/mlag/config │ MLAG config (config only) │
│ EVPN │ /arista/eos/evpn │ EVPN config (config only) │
│ System │ /system/config/hostname │ System hostname │
└────────────┴──────────────────────────────────────────────────────────────────┴───────────────────────────┘
```
---
## Output Formats
All discovery commands support three output formats via the `--output` / `-o` option:
### Pretty (default)
Rich formatted output with syntax highlighting and tables.
```bash
uv run fabric-orch discover get --path "/system/config" --output pretty
```
### JSON
Machine-readable JSON output, suitable for piping to `jq` or other tools.
```bash
uv run fabric-orch discover get --path "/system/config" --output json | jq '.'
```
### File
Save output directly to a file.
```bash
uv run fabric-orch discover get --path "/interfaces/interface" --output file:interfaces.json
```
---
## Error Handling
The CLI provides helpful error messages with hints:
### Connection Errors
```
✗ Connection Error: Failed to connect to 172.16.0.50:6030
Hints:
• Check if the target is reachable
• Verify gNMI is enabled on the device
• Check credentials
• Try --insecure flag for self-signed certs
```
### Path Errors
```
✗ Path Error: Path not found or invalid: /invalid/path
Hints:
• Use 'discover capabilities' to see supported models
• Check path syntax (e.g., /interfaces/interface[name=Ethernet1])
• Try a parent path first
```
---
## Best Practices
### 1. Use Environment Variables
Set credentials once to avoid repetition:
```bash
export GNMI_TARGET="172.16.0.50:6030"
export GNMI_USERNAME="admin"
export GNMI_PASSWORD="admin"
```
### 2. Start with Capabilities
Before exploring paths, check what models the device supports:
```bash
uv run fabric-orch discover capabilities
```
### 3. Explore Incrementally
Start with broad paths and narrow down:
```bash
# Start broad
uv run fabric-orch discover get --path "/interfaces"
# Then narrow down
uv run fabric-orch discover get --path "/interfaces/interface[name=Ethernet1]"
# Then specific leaves
uv run fabric-orch discover get --path "/interfaces/interface[name=Ethernet1]/state/oper-status"
```
### 4. Use Dry-Run for Set Operations
Always test with dry-run first:
```bash
# Test first
uv run fabric-orch discover set --path "..." --value "..."
# Then apply
uv run fabric-orch discover set --path "..." --value "..." --no-dry-run
```
### 5. Save Exploration Results
Save interesting discoveries to files for documentation:
```bash
uv run fabric-orch discover get --path "/arista/eos/evpn" --output file:evpn-config.json
```
### 6. Use JSON Output for Scripting
For automation, use JSON output:
```bash
# Get interface names
uv run fabric-orch discover get \
--path "/interfaces/interface" \
--output json | jq -r '.notification[0].update[0].val | keys[]'
```
---
## Troubleshooting
### gNMI Not Responding
1. Verify gNMI is enabled on the device:
```
switch# show management api gnmi
```
2. Check the correct port (default: 6030 for Arista)
3. Ensure network connectivity:
```bash
nc -zv 172.16.0.50 6030
```
### Certificate Issues
For lab environments with self-signed certificates, always use `--insecure`:
```bash
uv run fabric-orch discover capabilities --target leaf1:6030 --insecure
```
### Path Not Found
1. Check path syntax - use exact key names
2. Verify the model is supported: `discover capabilities`
3. Try the parent path first
4. Check if it's config-only or state-only data
---
## See Also
- [YANG Paths Reference](yang-paths.md) - Detailed path documentation
- [gNMI Module README](../src/gnmi/README.md) - Python API documentation

View File

@@ -9,7 +9,7 @@ This document provides the complete reference of validated gNMI YANG paths for m
## Quick Reference ## Quick Reference
| Feature | Model Type | Config | State | Subscribe ON_CHANGE | | Feature | Model Type | Config | State | Subscribe ON_CHANGE |
|---------|------------|--------|-------|---------------------| | ---------- | ---------- | ------ | ----- | ------------------- |
| Interfaces | OpenConfig | ✅ | ✅ | ✅ | | Interfaces | OpenConfig | ✅ | ✅ | ✅ |
| Loopbacks | OpenConfig | ✅ | ✅ | ✅ | | Loopbacks | OpenConfig | ✅ | ✅ | ✅ |
| VLANs | OpenConfig | ✅ | ✅ | ✅ | | VLANs | OpenConfig | ✅ | ✅ | ✅ |
@@ -25,7 +25,7 @@ This document provides the complete reference of validated gNMI YANG paths for m
**Model**: `openconfig-interfaces` **Model**: `openconfig-interfaces`
| Operation | Path | | Operation | Path |
|-----------|------| | ------------------ | ---------------------------------------------------------- |
| All interfaces | `/interfaces/interface` | | All interfaces | `/interfaces/interface` |
| Specific interface | `/interfaces/interface[name=Ethernet1]` | | Specific interface | `/interfaces/interface[name=Ethernet1]` |
| Interface config | `/interfaces/interface[name=Ethernet1]/config` | | Interface config | `/interfaces/interface[name=Ethernet1]/config` |
@@ -56,7 +56,7 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
**Model**: `openconfig-interfaces` **Model**: `openconfig-interfaces`
| Operation | Path | | Operation | Path |
|-----------|------| | ---------------- | ---------------------------------------------- |
| Loopback0 | `/interfaces/interface[name=Loopback0]` | | Loopback0 | `/interfaces/interface[name=Loopback0]` |
| Loopback1 (VTEP) | `/interfaces/interface[name=Loopback1]` | | Loopback1 (VTEP) | `/interfaces/interface[name=Loopback1]` |
| Loopback config | `/interfaces/interface[name=Loopback0]/config` | | Loopback config | `/interfaces/interface[name=Loopback0]/config` |
@@ -68,7 +68,7 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
**Model**: `openconfig-network-instance`, `openconfig-vlan` **Model**: `openconfig-network-instance`, `openconfig-vlan`
| Operation | Path | | Operation | Path |
|-----------|------| | ------------- | --------------------------------------------------------------------------------- |
| All VLANs | `/network-instances/network-instance[name=default]/vlans/vlan` | | All VLANs | `/network-instances/network-instance[name=default]/vlans/vlan` |
| Specific VLAN | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]` | | Specific VLAN | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]` |
| VLAN config | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]/config` | | VLAN config | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]/config` |
@@ -89,7 +89,7 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
### Paths ### Paths
| Operation | Path (relative to base) | | Operation | Path (relative to base) |
|-----------|-------------------------| | ---------------------- | ----------------------------------------------------------------------------------------------- |
| BGP global config | `/global/config` | | BGP global config | `/global/config` |
| BGP global state | `/global/state` | | BGP global state | `/global/state` |
| Router ID | `/global/config/router-id` | | Router ID | `/global/config/router-id` |
@@ -124,7 +124,7 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
**Model**: `arista-exp-eos-vxlan` (augments `openconfig-interfaces`) **Model**: `arista-exp-eos-vxlan` (augments `openconfig-interfaces`)
| Operation | Path | | Operation | Path |
|-----------|------| | ------------------------ | ----------------------------------------------------------------------------------- |
| VXLAN interface | `/interfaces/interface[name=Vxlan1]` | | VXLAN interface | `/interfaces/interface[name=Vxlan1]` |
| VXLAN augment | `/interfaces/interface[name=Vxlan1]/arista-vxlan` | | VXLAN augment | `/interfaces/interface[name=Vxlan1]/arista-vxlan` |
| VXLAN config | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config` | | VXLAN config | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config` |
@@ -160,14 +160,14 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
> ⚠️ **Limitation**: Only configuration is exposed via gNMI. Operational state (peer status, role, negotiation) requires eAPI. > ⚠️ **Limitation**: Only configuration is exposed via gNMI. Operational state (peer status, role, negotiation) requires eAPI.
| Operation | Path | | Operation | Path |
|-----------|------| | ----------- | ------------------------- |
| Full MLAG | `/arista/eos/mlag` | | Full MLAG | `/arista/eos/mlag` |
| MLAG config | `/arista/eos/mlag/config` | | MLAG config | `/arista/eos/mlag/config` |
### Config Fields ### Config Fields
| Field | Description | | Field | Description |
|-------|-------------| | ------------------------------ | ------------------------------------- |
| `domain-id` | MLAG domain identifier | | `domain-id` | MLAG domain identifier |
| `local-intf` | Local VLAN interface (e.g., Vlan4090) | | `local-intf` | Local VLAN interface (e.g., Vlan4090) |
| `peer-address` | MLAG peer IP address | | `peer-address` | MLAG peer IP address |
@@ -201,7 +201,7 @@ curl -X POST https://switch/command-api \
> ⚠️ **Limitation**: Only configuration is exposed via gNMI. Learned routes and MACs require eAPI. > ⚠️ **Limitation**: Only configuration is exposed via gNMI. Learned routes and MACs require eAPI.
| Operation | Path | | Operation | Path |
|-----------|------| | ----------------- | -------------------------------------------------------- |
| All EVPN | `/arista/eos/evpn` | | All EVPN | `/arista/eos/evpn` |
| EVPN instances | `/arista/eos/evpn/evpn-instances` | | EVPN instances | `/arista/eos/evpn/evpn-instances` |
| Specific instance | `/arista/eos/evpn/evpn-instances/evpn-instance[name=40]` | | Specific instance | `/arista/eos/evpn/evpn-instances/evpn-instance[name=40]` |
@@ -209,7 +209,7 @@ curl -X POST https://switch/command-api \
### Config Fields ### Config Fields
| Field | Path | | Field | Path |
|-------|------| | ------------------- | ------------------------------------------------------ |
| Name | `.../evpn-instance[name=X]/config/name` | | Name | `.../evpn-instance[name=X]/config/name` |
| Route Distinguisher | `.../evpn-instance[name=X]/config/route-distinguisher` | | Route Distinguisher | `.../evpn-instance[name=X]/config/route-distinguisher` |
| Redistribute | `.../evpn-instance[name=X]/config/redistribute` | | Redistribute | `.../evpn-instance[name=X]/config/redistribute` |
@@ -231,7 +231,7 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
**Model**: `openconfig-interfaces`, `openconfig-if-aggregate` **Model**: `openconfig-interfaces`, `openconfig-if-aggregate`
| Operation | Path | | Operation | Path |
|-----------|------| | ---------------------- | -------------------------------------------------------------------- |
| Port-Channel interface | `/interfaces/interface[name=Port-Channel999]` | | Port-Channel interface | `/interfaces/interface[name=Port-Channel999]` |
| LAG config | `/interfaces/interface[name=Port-Channel999]/aggregation/config` | | LAG config | `/interfaces/interface[name=Port-Channel999]/aggregation/config` |
| LAG state | `/interfaces/interface[name=Port-Channel999]/aggregation/state` | | LAG state | `/interfaces/interface[name=Port-Channel999]/aggregation/state` |

View File

@@ -5,6 +5,33 @@ description = "Declarative Network Fabric Orchestrator - Terraform-like infrastr
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"dataclasses>=0.8", "click>=8.1.0",
"typing>=3.10.0.0", "pygnmi>=0.8.0",
"rich>=13.0.0",
]
[project.scripts]
fabric-orch = "src.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.hatch.build.targets.sdist]
include = ["src"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "W"]
ignore = ["E501"]
[dependency-groups]
dev = [
"ruff>=0.14.10",
] ]

3
src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Fabric Orchestrator source package.
"""

568
src/cli.py Normal file
View File

@@ -0,0 +1,568 @@
"""
Fabric Orchestrator CLI.
This module provides the CLI for fabric-orchestrator using Click.
Main command groups:
- discover: YANG path exploration and discovery tools
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
import click
from rich.console import Console
from rich.json import JSON
from rich.panel import Panel
from rich.table import Table
from src.gnmi import GNMIClient, GNMIConnectionError, GNMIError, GNMIPathError
# =============================================================================
# Constants
# =============================================================================
console = Console()
error_console = Console(stderr=True)
# Environment variable defaults
ENV_TARGET = "GNMI_TARGET"
ENV_USERNAME = "GNMI_USERNAME"
ENV_PASSWORD = "GNMI_PASSWORD"
# =============================================================================
# Output Handlers
# =============================================================================
def output_result(
data: Any,
output_format: str = "pretty",
title: str | None = None,
) -> None:
"""
Output result in specified format.
Args:
data: Data to output
output_format: Format - "pretty", "json", or "file:path"
title: Optional title for pretty output
"""
# Convert to JSON string
json_str = json.dumps(data, indent=2, default=str)
if output_format == "json":
# Raw JSON to stdout
click.echo(json_str)
elif output_format.startswith("file:"):
# Save to file
file_path = output_format[5:]
Path(file_path).write_text(json_str)
console.print(f"[green]✓[/green] Results saved to [bold]{file_path}[/bold]")
else:
# Pretty print with rich
if title:
console.print(Panel(JSON(json_str), title=title, border_style="blue"))
else:
console.print(JSON(json_str))
def parse_target(target: str) -> tuple[str, int]:
"""Parse target string into host and port."""
if ":" in target:
host, port_str = target.rsplit(":", 1)
return host, int(port_str)
return target, 6030
def handle_error(e: Exception, context: str = "") -> None:
"""Handle and display error."""
if isinstance(e, GNMIConnectionError):
error_console.print(f"[red]✗ Connection Error:[/red] {e}")
error_console.print(
"\n[dim]Hints:[/dim]\n"
" • Check if the target is reachable\n"
" • Verify gNMI is enabled on the device\n"
" • Check credentials\n"
" • Try --insecure flag for self-signed certs"
)
elif isinstance(e, GNMIPathError):
error_console.print(f"[red]✗ Path Error:[/red] {e}")
error_console.print(
"\n[dim]Hints:[/dim]\n"
" • Use 'discover capabilities' to see supported models\n"
" • Check path syntax (e.g., /interfaces/interface[name=Ethernet1])\n"
" • Try a parent path first"
)
elif isinstance(e, GNMIError):
error_console.print(f"[red]✗ gNMI Error:[/red] {e}")
else:
error_console.print(f"[red]✗ Error:[/red] {e}")
if context:
error_console.print(f"[dim]Context: {context}[/dim]")
sys.exit(1)
# =============================================================================
# CLI Groups
# =============================================================================
@click.group()
@click.version_option(version="0.1.0", prog_name="fabric-orch")
def cli():
"""
Fabric Orchestrator - Terraform-like management for Arista EVPN-VXLAN fabrics.
Use 'fabric-orch discover' commands to explore YANG paths on devices.
"""
pass
@cli.group()
def discover():
"""
YANG path discovery and exploration tools.
Use these commands to explore YANG paths, get device capabilities,
and interactively discover configuration and state data.
"""
pass
# =============================================================================
# Common Options
# =============================================================================
def common_options(f):
"""Common options for all discover commands."""
f = click.option(
"--target", "-t",
envvar=ENV_TARGET,
required=True,
help="Target device (host:port). Env: GNMI_TARGET",
)(f)
f = click.option(
"--username", "-u",
envvar=ENV_USERNAME,
default="admin",
help="Username for authentication. Env: GNMI_USERNAME",
)(f)
f = click.option(
"--password", "-p",
envvar=ENV_PASSWORD,
default="admin",
help="Password for authentication. Env: GNMI_PASSWORD",
)(f)
f = click.option(
"--insecure/--no-insecure",
default=True,
help="Skip TLS verification (default: True for lab)",
)(f)
f = click.option(
"--output", "-o",
default="pretty",
help="Output format: pretty, json, or file:path",
)(f)
return f
# =============================================================================
# Discover Commands
# =============================================================================
@discover.command("capabilities")
@common_options
def discover_capabilities(
target: str,
username: str,
password: str,
insecure: bool,
output: str,
):
"""
List device capabilities and supported YANG models.
Example:
fabric-orch discover capabilities --target leaf1:6030
"""
host, port = parse_target(target)
try:
with GNMIClient(
host=host,
port=port,
username=username,
password=password,
insecure=insecure,
) as client:
caps = client.capabilities()
if output == "pretty":
# Pretty print capabilities
console.print(
f"\n[bold blue]Device Capabilities[/bold blue] - {target}\n"
)
console.print(f"gNMI Version: [green]{caps.get('gnmi_version', 'unknown')}[/green]")
# Encodings
encodings = caps.get("supported_encodings", [])
console.print(f"Supported Encodings: [cyan]{', '.join(encodings)}[/cyan]")
# Models table
models = caps.get("supported_models", [])
if models:
console.print(f"\n[bold]Supported Models ({len(models)}):[/bold]\n")
table = Table(show_header=True, header_style="bold")
table.add_column("Model Name", style="cyan")
table.add_column("Organization")
table.add_column("Version", style="green")
for model in sorted(models, key=lambda x: x.get("name", "")):
table.add_row(
model.get("name", ""),
model.get("organization", ""),
model.get("version", ""),
)
console.print(table)
else:
output_result(caps, output, "Device Capabilities")
except Exception as e:
handle_error(e, f"Getting capabilities from {target}")
@discover.command("get")
@common_options
@click.option(
"--path", "-P",
required=True,
help="YANG path to get data from",
)
@click.option(
"--type", "-T",
"data_type",
type=click.Choice(["config", "state", "all"]),
default="all",
help="Type of data to retrieve (default: all)",
)
def discover_get(
target: str,
username: str,
password: str,
insecure: bool,
output: str,
path: str,
data_type: str,
):
"""
Get configuration or state data at a YANG path.
Examples:
# Get all data at path
fabric-orch discover get --target leaf1:6030 --path "/interfaces/interface[name=Ethernet1]"
# Get config only
fabric-orch discover get --target leaf1:6030 --path "/interfaces" --type config
# Get state only
fabric-orch discover get --target leaf1:6030 --path "/interfaces/interface[name=Ethernet1]/state" --type state
# Save to file
fabric-orch discover get --target leaf1:6030 --path "/" --output file:result.json
"""
host, port = parse_target(target)
try:
with GNMIClient(
host=host,
port=port,
username=username,
password=password,
insecure=insecure,
) as client:
result = client.get(path, data_type=data_type)
if output == "pretty":
console.print(
f"\n[bold blue]GET {path}[/bold blue] ({data_type})\n"
)
output_result(result, "pretty")
else:
output_result(result, output, f"GET {path}")
except Exception as e:
handle_error(e, f"Getting data at {path}")
@discover.command("set")
@common_options
@click.option(
"--path", "-P",
required=True,
help="YANG path to set",
)
@click.option(
"--value", "-v",
required=True,
help="Value to set (JSON string or simple value)",
)
@click.option(
"--operation", "-O",
type=click.Choice(["update", "replace", "delete"]),
default="update",
help="Set operation type (default: update)",
)
@click.option(
"--dry-run/--no-dry-run",
default=True,
help="Dry-run mode - don't apply changes (default: True)",
)
def discover_set(
target: str,
username: str,
password: str,
insecure: bool,
output: str,
path: str,
value: str,
operation: str,
dry_run: bool,
):
"""
Set configuration at a YANG path.
By default runs in dry-run mode. Use --no-dry-run to actually apply changes.
Examples:
# Dry-run (default)
fabric-orch discover set --target leaf1:6030 \\
--path "/interfaces/interface[name=Ethernet1]/config/description" \\
--value "Uplink to Spine"
# Actually apply
fabric-orch discover set --target leaf1:6030 \\
--path "/interfaces/interface[name=Ethernet1]/config/description" \\
--value "Uplink to Spine" \\
--no-dry-run
# Delete config
fabric-orch discover set --target leaf1:6030 \\
--path "/interfaces/interface[name=Ethernet1]/config/description" \\
--value "" --operation delete --no-dry-run
"""
host, port = parse_target(target)
if dry_run:
console.print("[yellow]⚠ DRY-RUN MODE[/yellow] - No changes will be applied\n")
else:
console.print("[red]⚠ LIVE MODE[/red] - Changes WILL be applied!\n")
try:
with GNMIClient(
host=host,
port=port,
username=username,
password=password,
insecure=insecure,
) as client:
result = client.set(
path=path,
value=value,
operation=operation,
dry_run=dry_run,
)
if output == "pretty":
if dry_run:
console.print(f"[bold blue]SET (dry-run)[/bold blue] {path}\n")
console.print(f"Operation: [cyan]{operation}[/cyan]")
console.print(f"Value: [green]{value}[/green]")
console.print("\n[dim]Use --no-dry-run to apply this change[/dim]")
else:
console.print(f"[bold green]SET (applied)[/bold green] {path}\n")
output_result(result, "pretty")
else:
output_result(result, output, f"SET {path}")
except Exception as e:
handle_error(e, f"Setting data at {path}")
@discover.command("subscribe")
@common_options
@click.option(
"--path", "-P",
required=True,
multiple=True,
help="YANG path(s) to subscribe to (can specify multiple)",
)
@click.option(
"--mode", "-m",
type=click.Choice(["on-change", "sample", "target-defined"]),
default="on-change",
help="Subscription mode (default: on-change)",
)
@click.option(
"--interval", "-i",
type=int,
default=10,
help="Sample interval in seconds (for sample mode)",
)
@click.option(
"--count", "-c",
type=int,
default=None,
help="Number of updates to receive before exiting",
)
def discover_subscribe(
target: str,
username: str,
password: str,
insecure: bool,
output: str,
path: tuple[str, ...],
mode: str,
interval: int,
count: int | None,
):
"""
Subscribe to YANG path updates.
Note: For Arista EOS, use native paths WITHOUT module prefixes
for ON_CHANGE subscriptions.
Examples:
# Subscribe to interface state changes
fabric-orch discover subscribe --target leaf1:6030 \\
--path "/interfaces/interface/state/oper-status" \\
--mode on-change
# Sample interface counters every 10 seconds
fabric-orch discover subscribe --target leaf1:6030 \\
--path "/interfaces/interface[name=Ethernet1]/state/counters" \\
--mode sample --interval 10
# Subscribe to multiple paths
fabric-orch discover subscribe --target leaf1:6030 \\
--path "/interfaces/interface/state/oper-status" \\
--path "/network-instances/network-instance/protocols/protocol/bgp/neighbors/neighbor/state/session-state"
"""
host, port = parse_target(target)
paths = list(path)
console.print(f"\n[bold blue]Subscribing to {len(paths)} path(s)[/bold blue]\n")
for p in paths:
console.print(f"{p}")
console.print(f"\nMode: [cyan]{mode}[/cyan]")
if mode == "sample":
console.print(f"Interval: [cyan]{interval}s[/cyan]")
console.print("\n[dim]Press Ctrl+C to stop[/dim]\n")
console.print("" * 60)
try:
with GNMIClient(
host=host,
port=port,
username=username,
password=password,
insecure=insecure,
) as client:
subscription = client.subscribe(
paths=paths,
mode="stream",
stream_mode=mode,
sample_interval=interval,
)
update_count = 0
try:
for update in subscription:
update_count += 1
if output == "pretty":
console.print(f"\n[green]Update #{update_count}[/green]")
output_result(update, "pretty")
else:
output_result(update, output)
if count is not None and update_count >= count:
console.print(f"\n[yellow]Reached {count} updates, stopping[/yellow]")
break
except KeyboardInterrupt:
console.print(f"\n\n[yellow]Stopped after {update_count} updates[/yellow]")
except Exception as e:
handle_error(e, f"Subscribing to {paths}")
# =============================================================================
# Additional Commands
# =============================================================================
@discover.command("paths")
def discover_paths():
"""
Show commonly used YANG paths for Arista EOS.
This displays a reference of validated paths that can be used
with the get, set, and subscribe commands.
"""
console.print("\n[bold blue]Common YANG Paths for Arista EOS[/bold blue]\n")
table = Table(show_header=True, header_style="bold")
table.add_column("Category", style="cyan")
table.add_column("Path")
table.add_column("Notes", style="dim")
paths = [
("Interfaces", "/interfaces/interface[name=Ethernet1]/state", "Full interface state"),
("Interfaces", "/interfaces/interface[name=Ethernet1]/state/oper-status", "Operational status"),
("Interfaces", "/interfaces/interface[name=Ethernet1]/state/counters", "Interface counters"),
("Loopbacks", "/interfaces/interface[name=Loopback0]", "Loopback interface"),
("VLANs", "/network-instances/network-instance[name=default]/vlans/vlan", "All VLANs"),
("BGP", "/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state", "All BGP neighbors"),
("VXLAN", "/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis", "VLAN-to-VNI mappings"),
("VXLAN", "/interfaces/interface[name=Vxlan1]/arista-vxlan/config", "VXLAN config"),
("MLAG", "/arista/eos/mlag/config", "MLAG config (config only)"),
("EVPN", "/arista/eos/evpn", "EVPN config (config only)"),
("System", "/system/config/hostname", "System hostname"),
]
for category, path, notes in paths:
table.add_row(category, path, notes)
console.print(table)
console.print("\n[bold]Path Syntax Tips:[/bold]")
console.print(" • Use brackets for keys: [name=Ethernet1]")
console.print(" • Multiple keys: [identifier=BGP][name=BGP]")
console.print(" • Wildcards not supported in GET, but can omit keys for all")
console.print("\n[bold]Subscription Notes:[/bold]")
console.print(" • Use native paths WITHOUT module prefix for ON_CHANGE")
console.print(" • ✅ /interfaces/interface[name=Ethernet1]/state")
console.print(" • ❌ /openconfig-interfaces:interfaces/...")
# =============================================================================
# Entry Point
# =============================================================================
def main():
"""Main entry point."""
cli()
if __name__ == "__main__":
main()

183
src/gnmi/README.md Normal file
View File

@@ -0,0 +1,183 @@
# gNMI Client Module
This module provides a high-level gNMI client wrapper for interacting with Arista devices using pygnmi.
## Features
- Connection management with context manager support
- Get capabilities (list supported YANG models)
- Get configuration and state data at any YANG path
- Set configuration with dry-run support
- Subscribe to path updates (on-change, sample modes)
- Error handling with meaningful exceptions
## Installation
Ensure you have the required dependencies:
```bash
uv add pygnmi click rich
```
## Usage
### Basic Connection
```python
from gnmi import GNMIClient
# Using context manager (recommended)
with GNMIClient(
host="172.16.0.50",
port=6030,
username="admin",
password="admin",
insecure=True
) as client:
# Use the client
caps = client.capabilities()
print(caps)
```
### Get Capabilities
```python
with GNMIClient(host="172.16.0.50", port=6030) as client:
# Get raw capabilities
caps = client.capabilities()
print(f"gNMI Version: {caps['gnmi_version']}")
# Get parsed models
models = client.get_models()
for model in models:
print(f" {model.name} ({model.organization}) v{model.version}")
```
### Get Data
```python
with GNMIClient(host="172.16.0.50", port=6030) as client:
# Get all data (config + state)
result = client.get("/interfaces/interface[name=Ethernet1]")
# Get config only
result = client.get(
"/interfaces/interface[name=Ethernet1]",
data_type="config"
)
# Get state only
result = client.get(
"/interfaces/interface[name=Ethernet1]/state",
data_type="state"
)
# Get multiple paths
result = client.get([
"/interfaces/interface[name=Ethernet1]/state",
"/interfaces/interface[name=Ethernet2]/state",
])
```
### Set Configuration
```python
with GNMIClient(host="172.16.0.50", port=6030) as client:
# Dry-run (default) - shows what would be set
result = client.set(
path="/interfaces/interface[name=Ethernet1]/config/description",
value="Uplink to Spine",
dry_run=True
)
# Actually apply the change
result = client.set(
path="/interfaces/interface[name=Ethernet1]/config/description",
value="Uplink to Spine",
dry_run=False
)
# Replace operation
result = client.set(
path="/interfaces/interface[name=Ethernet1]/config",
value={"description": "New config", "enabled": True},
operation="replace",
dry_run=False
)
# Delete operation
result = client.set(
path="/interfaces/interface[name=Ethernet1]/config/description",
value=None,
operation="delete",
dry_run=False
)
```
### Subscribe to Updates
```python
with GNMIClient(host="172.16.0.50", port=6030) as client:
# Subscribe to interface state changes
subscription = client.subscribe(
paths="/interfaces/interface/state/oper-status",
mode="stream",
stream_mode="on-change"
)
# Process updates
for update in subscription:
print(update)
```
## Error Handling
The module provides specific exceptions for different error types:
```python
from gnmi import GNMIClient, GNMIError, GNMIConnectionError, GNMIPathError
try:
with GNMIClient(host="172.16.0.50", port=6030) as client:
result = client.get("/invalid/path")
except GNMIConnectionError as e:
print(f"Connection failed: {e}")
except GNMIPathError as e:
print(f"Invalid path: {e}")
except GNMIError as e:
print(f"gNMI error: {e}")
```
## Environment Variables
The CLI supports these environment variables for defaults:
- `GNMI_TARGET` - Target device (host:port)
- `GNMI_USERNAME` - Username for authentication
- `GNMI_PASSWORD` - Password for authentication
## Path Format Notes
For Arista EOS, use native paths **without** module prefixes for subscriptions:
```python
# ✅ Correct - native path
"/interfaces/interface[name=Ethernet1]/state"
# ❌ Incorrect - with module prefix (may fail for subscriptions)
"/openconfig-interfaces:interfaces/interface[name=Ethernet1]/state"
```
## Validated Paths
These paths have been validated against Arista cEOS 4.35.0F:
| Category | Path | Notes |
|----------|------|-------|
| Interfaces | `/interfaces/interface[name=Ethernet1]/state` | Full state |
| BGP | `/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state` | All neighbors |
| VXLAN | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis` | VNI mappings |
| MLAG | `/arista/eos/mlag/config` | Config only |
| EVPN | `/arista/eos/evpn` | Config only |
See `docs/yang-paths.md` for complete path documentation.

23
src/gnmi/__init__.py Normal file
View File

@@ -0,0 +1,23 @@
"""
gNMI Client Module for Fabric Orchestrator.
This module provides a gNMI client wrapper for interacting with
Arista devices using pygnmi.
Usage:
from gnmi import GNMIClient
with GNMIClient(
host="leaf1",
port=6030,
username="admin",
password="admin",
insecure=True
) as client:
caps = client.capabilities()
print(caps)
"""
from .client import GNMIClient, GNMIConnectionError, GNMIError, GNMIPathError
__all__ = ["GNMIClient", "GNMIError", "GNMIConnectionError", "GNMIPathError"]

383
src/gnmi/client.py Normal file
View File

@@ -0,0 +1,383 @@
"""
gNMI Client Wrapper for Fabric Orchestrator.
This module provides a high-level gNMI client using pygnmi with:
- Connection management
- Error handling with meaningful exceptions
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any, Literal
from pygnmi.client import gNMIclient
# =============================================================================
# Exceptions
# =============================================================================
class GNMIError(Exception):
"""Base exception for gNMI operations."""
pass
class GNMIConnectionError(GNMIError):
"""Raised when connection to device fails."""
pass
class GNMIPathError(GNMIError):
"""Raised when path is invalid or not found."""
pass
# =============================================================================
# Data Types
# =============================================================================
DataType = Literal["config", "state", "all"]
SetOperation = Literal["update", "replace", "delete"]
SubscribeMode = Literal["once", "stream", "poll"]
StreamMode = Literal["on-change", "sample", "target-defined"]
@dataclass
class Capability:
"""Represents a gNMI capability/model."""
name: str
organization: str
version: str
def __str__(self) -> str:
return f"{self.name} ({self.organization}) v{self.version}"
@dataclass
class SubscriptionUpdate:
"""Represents a subscription update."""
path: str
value: Any
timestamp: int
# =============================================================================
# gNMI Client
# =============================================================================
class GNMIClient:
"""
gNMI Client wrapper using pygnmi.
Provides high-level methods for gNMI operations with proper
error handling and context manager support.
Or synchronously:
with GNMIClient(host="leaf1", port=6030, ...) as client:
caps = client.capabilities()
"""
def __init__(
self,
host: str,
port: int = 6030,
username: str = "admin",
password: str = "admin",
insecure: bool = True,
skip_verify: bool = True,
timeout: int = 10,
):
"""
Initialize gNMI client.
Args:
host: Target hostname or IP
port: gNMI port (default: 6030)
username: Username for authentication
password: Password for authentication
insecure: Skip TLS verification (default: True for lab)
skip_verify: Skip certificate verification
timeout: Connection timeout in seconds
"""
self.host = host
self.port = port
self.username = username
self.password = password
self.insecure = insecure
self.skip_verify = skip_verify
self.timeout = timeout
self._client: gNMIclient | None = None
@property
def target(self) -> str:
"""Get target string (host:port)."""
return f"{self.host}:{self.port}"
def _ensure_connected(self) -> gNMIclient:
"""Ensure client is connected."""
if self._client is None:
raise GNMIConnectionError("Client not connected. Use context manager.")
return self._client
def __enter__(self) -> GNMIClient:
"""Enter context manager (sync)."""
try:
self._client = gNMIclient(
target=(self.host, self.port),
username=self.username,
password=self.password,
insecure=self.insecure,
skip_verify=self.skip_verify,
)
self._client.__enter__()
return self
except Exception as e:
raise GNMIConnectionError(
f"Failed to connect to {self.target}: {e}"
) from e
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Exit context manager (sync)."""
if self._client:
try:
self._client.__exit__(exc_type, exc_val, exc_tb)
except Exception:
pass # Ignore cleanup errors
self._client = None
# =========================================================================
# gNMI Operations
# =========================================================================
def capabilities(self) -> dict[str, Any]:
"""
Get device capabilities.
Returns:
Dictionary containing:
- gnmi_version: gNMI protocol version
- supported_models: List of supported YANG models
- supported_encodings: List of supported encodings
"""
client = self._ensure_connected()
try:
result = client.capabilities()
return result
except Exception as e:
raise GNMIError(f"Failed to get capabilities: {e}") from e
def get_models(self) -> list[Capability]:
"""
Get list of supported YANG models.
Returns:
List of Capability objects
"""
caps = self.capabilities()
models = []
for model in caps.get("supported_models", []):
models.append(
Capability(
name=model.get("name", ""),
organization=model.get("organization", ""),
version=model.get("version", ""),
)
)
return models
def get(
self,
path: str | list[str],
data_type: DataType = "all",
encoding: str = "json_ietf",
) -> dict[str, Any]:
"""
Get data at path.
Args:
path: YANG path or list of paths
data_type: Type of data to retrieve:
- "config": Configuration data only
- "state": State/operational data only
- "all": Both config and state (default)
encoding: Data encoding (default: json_ietf)
Returns:
Dictionary with path data
"""
client = self._ensure_connected()
paths = [path] if isinstance(path, str) else path
# Map data_type to pygnmi datatype parameter
datatype_map = {
"config": "config",
"state": "state",
"all": "all",
}
datatype = datatype_map.get(data_type, "all")
try:
result = client.get(path=paths, datatype=datatype, encoding=encoding)
return result
except Exception as e:
error_msg = str(e).lower()
if "not found" in error_msg or "invalid" in error_msg:
raise GNMIPathError(f"Path not found or invalid: {path}") from e
raise GNMIError(f"Failed to get data at {path}: {e}") from e
def set(
self,
path: str,
value: Any,
operation: SetOperation = "update",
encoding: str = "json_ietf",
dry_run: bool = True,
) -> dict[str, Any]:
"""
Set configuration at path.
Args:
path: YANG path
value: Value to set (dict or JSON string)
operation: Set operation type:
- "update": Merge with existing config
- "replace": Replace existing config
- "delete": Delete config at path
encoding: Data encoding (default: json_ietf)
dry_run: If True, only validate without applying (default: True)
Returns:
Result of set operation
"""
if dry_run:
# For dry-run, just return what would be set
return {
"dry_run": True,
"operation": operation,
"path": path,
"value": value,
"message": "Dry-run mode - no changes applied",
}
client = self._ensure_connected()
# Parse value if it's a JSON string
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
pass # Keep as string - not valid JSON, will be set as string value
try:
if operation == "delete":
result = client.set(delete=[path])
elif operation == "replace":
result = client.set(replace=[(path, value)])
else: # update
result = client.set(update=[(path, value)])
return result
except Exception as e:
raise GNMIError(f"Failed to set {path}: {e}") from e
def subscribe(
self,
paths: str | list[str],
mode: SubscribeMode = "stream",
stream_mode: StreamMode = "on-change",
sample_interval: int = 10,
encoding: str = "json_ietf",
) -> Any:
"""
Subscribe to path updates.
Args:
paths: YANG path(s) to subscribe to
mode: Subscription mode:
- "once": Get data once and close
- "stream": Continuous updates
- "poll": Poll on demand
stream_mode: Stream mode (for mode="stream"):
- "on-change": Update on value change
- "sample": Periodic sampling
- "target-defined": Device decides
sample_interval: Interval in seconds for sample mode
encoding: Data encoding
Yields:
Subscription updates
"""
client = self._ensure_connected()
path_list = [paths] if isinstance(paths, str) else paths
# Build subscription list
subscribe_list = []
for p in path_list:
sub = {
"path": p,
"mode": stream_mode.replace("-", "_"), # on-change -> on_change
}
if stream_mode == "sample":
sub["sample_interval"] = sample_interval * 1_000_000_000 # nanoseconds
subscribe_list.append(sub)
subscribe_request = {
"subscription": subscribe_list,
"mode": mode,
"encoding": encoding,
}
try:
return client.subscribe2(subscribe=subscribe_request)
except Exception as e:
raise GNMIError(f"Failed to subscribe to {paths}: {e}") from e
# =========================================================================
# Utility Methods
# =========================================================================
def explore(self, path: str = "/", depth: int | None = None) -> dict[str, Any]:
"""
Explore available paths under given path.
Args:
path: Base path to explore (default: root)
depth: Maximum depth to explore (None = unlimited)
Returns:
Dictionary with discovered paths and data
"""
result = self.get(path, data_type="all")
if depth is not None and depth > 0:
# Limit depth by filtering result
# This is a simplified implementation
pass
return result
def validate_path(self, path: str) -> bool:
"""
Check if path exists on device.
Args:
path: YANG path to validate
Returns:
True if path exists, False otherwise
"""
try:
self.get(path)
return True
except GNMIPathError:
return False
except GNMIError:
return False

View File

@@ -6,18 +6,18 @@ managing Arista EVPN-VXLAN fabrics via gNMI.
""" """
from .paths import ( from .paths import (
BGP,
EVPN,
MLAG,
VXLAN,
AfiSafi,
FabricSubscriptions,
Interfaces, Interfaces,
Loopbacks, Loopbacks,
VLANs,
BGP,
AfiSafi,
VXLAN,
MLAG,
EVPN,
PortChannel, PortChannel,
System,
SubscriptionPath, SubscriptionPath,
FabricSubscriptions, System,
VLANs,
) )
__all__ = [ __all__ = [

View File

@@ -16,7 +16,6 @@ Usage:
from dataclasses import dataclass from dataclasses import dataclass
# ============================================================================= # =============================================================================
# Interfaces (OpenConfig) # Interfaces (OpenConfig)
# ============================================================================= # =============================================================================

321
uv.lock generated
View File

@@ -3,34 +3,327 @@ revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
[[package]] [[package]]
name = "dataclasses" name = "cffi"
version = "0.8" version = "2.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/12/7919c5d8b9c497f9180db15ea8ead6499812ea8264a6ae18766d93c59fe5/dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97", size = 36581, upload-time = "2020-11-13T14:40:30.139Z" } dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", size = 19041, upload-time = "2020-11-13T14:40:29.194Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
]
[[package]]
name = "dictdiffer"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/7b/35cbccb7effc5d7e40f4c55e2b79399e1853041997fcda15c9ff160abba0/dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578", size = 31513, upload-time = "2021-07-22T13:24:29.276Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" },
] ]
[[package]] [[package]]
name = "fabric-orchestrator" name = "fabric-orchestrator"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "dataclasses" }, { name = "click" },
{ name = "typing" }, { name = "pygnmi" },
{ name = "rich" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "dataclasses", specifier = ">=0.8" }, { name = "click", specifier = ">=8.1.0" },
{ name = "typing", specifier = ">=3.10.0.0" }, { name = "pygnmi", specifier = ">=0.8.0" },
{ name = "rich", specifier = ">=13.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.14.10" }]
[[package]]
name = "grpcio"
version = "1.76.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
{ url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
{ url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
{ url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
{ url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
{ url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
{ url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
{ url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
{ url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
{ url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
{ url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
{ url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
{ url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
{ url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
{ url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
{ url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
{ url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
{ url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
{ url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
{ url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
{ url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
{ url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
{ url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
{ url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
{ url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
{ url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
{ url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
{ url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
] ]
[[package]] [[package]]
name = "typing" name = "markdown-it-py"
version = "3.10.0.0" version = "4.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/1b/835d4431805939d2996f8772aca1d2313a57e8860fec0e48e8e7dfe3a477/typing-3.10.0.0.tar.gz", hash = "sha256:13b4ad211f54ddbf93e5901a9967b1e07720c1d1b78d596ac6a439641aa1b130", size = 78962, upload-time = "2021-05-01T18:03:58.186Z" } dependencies = [
wheels = [ { name = "mdurl" },
{ url = "https://files.pythonhosted.org/packages/f2/5d/865e17349564eb1772688d8afc5e3081a5964c640d64d1d2880ebaed002d/typing-3.10.0.0-py3-none-any.whl", hash = "sha256:12fbdfbe7d6cca1a42e485229afcb0b0c8259258cfb919b8a5e2a5c953742f89", size = 26320, upload-time = "2021-05-01T18:03:56.398Z" }, ]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "protobuf"
version = "6.33.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" },
{ url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" },
{ url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" },
{ url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" },
{ url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" },
{ url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" },
{ url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pygnmi"
version = "0.8.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "dictdiffer" },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/e8/aae8362da667452e5352875a7df700cc182740d14a01a88b22004bc318db/pygnmi-0.8.15.tar.gz", hash = "sha256:9b43d6334ac7ea73c0b8da395306b7618f4630adab96aaa2f28a79b3b52fdf58", size = 42938, upload-time = "2025-03-10T22:15:34.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/d8/1958e627d66f5f0c8dbdb1962850bc9e256fc65129ceb0212c0cb3e0d121/pygnmi-0.8.15-py3-none-any.whl", hash = "sha256:e72859070c7dc3618b578e48c76521db6d627b64a92b496ab886f3ff93a1a396", size = 35561, upload-time = "2025-03-10T22:15:32.981Z" },
]
[[package]]
name = "rich"
version = "14.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
]
[[package]]
name = "ruff"
version = "0.14.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
] ]