Metadata-Version: 2.5
Name: readyview
Version: 1.20.0
Summary: ReadyLinks ReadyView API library.
Project-URL: Homepage, https://gitlab.com/readylinks
Project-URL: Issues, https://gitlab.com/readylinks
Author-email: ReadyLinks Developers <developers@ready-links.com>
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.8
Requires-Dist: pydantic<3,>=2.4
Requires-Dist: requests
Provides-Extra: test
Requires-Dist: pytest>=7; extra == 'test'
Requires-Dist: responses>=0.23; extra == 'test'
Description-Content-Type: text/markdown

# ReadyView Dashboard Python Library

The ReadyView Dashboard Python library provides all current ReadyView [dashboard API](https://api.readylinks.io/redoc) calls to interface with the ReadyLinks ReadyView cloud-managed platform. ReadyView generates the library based on dashboard API's OpenAPI spec to keep it up to date with the latest API releases, and provides the full source code for the library including the tools used to generate the library. ReadyLinks welcomes constructive pull requests that maintain backwards compatibility with prior versions. The library requires Python 3.8+, receives support from the community, and you can install it via [PyPI](https://pypi.org/project/readyview/):

```
pip install --upgrade readyview
```

## Usage
1. Export your Organization ID and API Key as an environment variable, for example:
```
export READYVIEW_DASHBOARD_ORG_ID=YOUR_ORG_ID
export READYVIEW_DASHBOARD_API_KEY=YOUR_API_KEY
```
2. Import the library with a single line at the top of your Python script.
```
import readyview
```
3. Instantiate the dashboard client. 
```
dashboard = readyview.Dashboard()
```
4. Start programmatically interacting with your ReadyView dashboard!
```
my_networks = dashboard.network.get_networks()
```

## Errors and strict mode
By default, a call that fails returns `None`, so existing scripts keep working. To find out why a call failed, turn on strict mode:

```python
import readyview

dashboard = readyview.Dashboard(raise_errors=True)

try:
    config = dashboard.switch.get_switch_configurations("AA:BB:CC:DD:EE:FF")
except readyview.ReadyViewAPIError as error:
    print(error)
    # GET /v1/devices/AA:BB:CC:DD:EE:FF/switch failed with HTTP 504: The device did not answer within the time limit.
```

In strict mode, a call that fails raises `readyview.ReadyViewAPIError`, which has:

- `status_code`: the HTTP status, such as 401, 404, 429 or 504. It is `None` when no reply arrived, because the API could not be reached or did not answer in time. Those calls raise the subclass `readyview.ReadyViewConnectionError`.
- `message`: why the call failed, as the API explained it.
- `method` and `path`: the call that failed, such as `GET` and `/v1/devices/AA:BB:CC:DD:EE:FF/switch`.
- `retry_after`: when the reply has a `Retry-After` header (usually with a 429, when you are rate limited), how many seconds it asks you to wait before trying again. Otherwise `None`.

The error is built from the API's reply only. It never repeats what you sent, or the values the API rejected, since those can be passwords.

Calls that work return the same values in both modes, with two differences in strict mode: any 2xx status counts as success, and a reply that is not JSON (say, a hotel network's sign-in page) raises `ReadyViewAPIError`.

### Logging
The library logs to the `readyview` logger with Python's `logging` module. It logs each reply at DEBUG. In the default mode, it also logs a failed call: an error status at INFO, and a request that got no reply at WARNING, which earlier versions printed. To see why calls return `None` without switching to strict mode:

```python
import logging

logging.basicConfig()
logging.getLogger("readyview").setLevel(logging.INFO)
```

## SNMPv3 users
`SnmpV3User` builds a user that a switch accepts. It checks the firmware's rules as it is built: a name of 3-16 characters, passwords of 9-15, printable ASCII without spaces, `'` or `\`. `dashboard.switch.add_snmp_user()` then adds it safely:

```python
import readyview
from readyview import SnmpV3User

dashboard = readyview.Dashboard()
user = SnmpV3User.auth_priv("nms-poller", auth_password="...", privacy_password="...")

for device in dashboard.network.get_devices("YOUR-NETWORK-ID"):
    if device["device_type"] == "Switch" and device["status"] == "Online":
        result = dashboard.switch.add_snmp_user(device["mac_address"], user, current=device)
        print(device["name"], result.status.value, result.detail)
```

For each switch, `add_snmp_user`:
- reads the switch first, and leaves it alone if it already has the user, so it is safe to run again
- reports a `conflict` when the name exists with other settings, since the API can add an SNMPv3 user but cannot change or delete one
- reports `full` when the switch already has 10 users
- turns the SNMP service on if it is off
- checks the reply for refused values, then reads the configuration back until the switch reports the user, the only real confirmation

Passing the switch's record as `current=` (the device records above include each switch's SNMP settings) saves the first read. When the record shows the switch without the user, the change goes out at once and the read-back is the only live read. The result still comes from the switch: a record that is out of date can cost a read, never change the outcome.

It never raises for a switch it could not configure: `result.status`, `result.detail` and `result.ok` say what happened. Its live configuration reads are spaced to stay under the API's limit of 30 a minute, and a read the API turns away with HTTP 429 is tried again after a pause. `SnmpV3User.auth_no_priv()` and `SnmpV3User.no_auth_no_priv()` build the other security levels, and `dashboard.switch.get_snmp_users(mac)` lists a switch's users without their passwords.

## Automation
### Operations that report an outcome
These `dashboard.switch` methods each make one change and return an `Outcome`:
- `ok`: whether the switch ends up as asked
- `status`: what happened, such as `sent`, `refused`, `up to date` or `error`
- `detail`: the API's reason, or what changed

They never raise for a switch they could not change, so a loop over many switches reports every one:

| Method | What it does |
| --- | --- |
| `apply_settings(mac, change)` | Sends a `SwitchPost` change and reports any value the switch refused |
| `use_template(mac, template_id)` | Applies a configuration template; reports `skipped` when the template is for another model |
| `upgrade_firmware(mac, version=None, channel=None)` | Starts an upgrade; reports `up to date` when the switch already has the firmware |
| `add_bandwidth_profiles(mac, profiles)` | Adds bandwidth profiles, or replaces those with the same ids |
| `bind_bandwidth_profile(mac, client_mac, profile_id)` | Finds the client's port and limits the client to a profile |
| `update_client(mac, client_mac, **fields)` | Changes a connected client, e.g. `location="Room 101"` |
| `add_snmp_user(mac, user, current=None)` | Adds an SNMPv3 user (see above) |

### Scripts that change many switches
`readyview.automation` holds the parts every automation script repeats. That is the `--network` / `--switch` / `--dry-run` / `--yes` / `--parallel N` command line, plus picking the online switches, with a firmware minimum if you set one. It shows the plan, asks before changing anything, reports each switch, and ends with a summary and an exit status. A complete script:

```python
from readyview import automation
from readyview.switch_post import Lldp, SwitchPost

parser = automation.command_line("Turn LLDP on.")
args = parser.parse_args()
change = SwitchPost(lldp=Lldp(enabled=True))
raise SystemExit(automation.run(
    args, "turn LLDP on",
    lambda dashboard, switch: dashboard.switch.apply_settings(switch["mac_address"], change),
    min_firmware=7.655,
))
```

`--parallel N` (or `run(..., parallel=N)`) changes up to N switches at the same time, 1-16. `automation.command_line(description, parallel=6)` sets a script's default; it is 1 otherwise. Each switch spends most of its time waiting: on its reply, and for the change to show up when it is read back. Running several at once overlaps those waits, while live switch reads stay paced across all of them, so the API's rate limit holds. The outcomes are printed in the order of the plan. Ctrl+C starts no more switches, finishes the ones already started, and reports them.

`automation.ask_secret(label, env_var)` reads a password from the environment, or has it typed twice at a hidden prompt.

### Reports
`readyview.reports.gather(dashboard, switches)` reads every switch's ports and recent metrics from ReadyView's cache, several at a time. A switch that cannot be read is listed with the reason. `device_rows()`, `client_rows()`, `fiber_rows()` and `firmware_rows()` turn that into report rows, and `find_device(mac, ...)` finds where a device is connected. The rows are plain dicts, ready for pandas:

```python
import pandas as pd
from readyview import reports

switches = dashboard.device.get_devices(params={"device_type": ["Switch"]})
data = reports.gather(dashboard, switches)
pd.DataFrame(reports.device_rows(switches, data)).to_excel("switches.xlsx", index=False)
```

## Contribute 
Create a branch to add your feature/fix then submit a merge request to main for review. Be sure to follow the correct format in your commit message to trigger the pipeline to 
submit a new release. Example commit below. 

```
feat: Added the ability to update switch configurations.
fix: Fixes some bug. 
BREAKING CHANGE: Updated function API. 
```

Run the tests before you submit:
```
pip install -e ".[test]"
pytest
```
