Metadata-Version: 2.4
Name: vexxhost-budgetclient
Version: 0.1.0
Summary: Python client and OpenStack CLI plugin for Budget
Author: VEXXHOST
License-Expression: Apache-2.0
Project-URL: Repository, https://github.com/vexxhost/python-budgetclient
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: keystoneauth1>=5.3
Requires-Dist: osc-lib>=3.0
Requires-Dist: vexxhost-clientcore<0.2,>=0.1.1
Provides-Extra: cli
Requires-Dist: python-openstackclient>=7.0; extra == "cli"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: requests-mock>=1.12; extra == "test"
Requires-Dist: python-openstackclient>=7.0; extra == "test"
Dynamic: license-file

# vexxhost-budgetclient

Python client and OpenStack CLI plugin for the VEXXHOST Budget service: spending
budgets with alert thresholds, measured by cost-explorer.

## Install

Install it into the same environment as the `openstack` CLI:

```sh
pip install 'vexxhost-budgetclient[cli]'
```

The client finds the service in the Keystone catalog under service type
`budget`. Until the catalog has that entry, point it at the endpoint:

```sh
export OS_BUDGET_ENDPOINT=https://budget.public.global.vexxhost.net
```

or pass `--os-budget-endpoint`, or set `budget_endpoint_override` for the cloud
in `clouds.yaml`. Authentication comes from `clouds.yaml` or the usual `OS_*`
variables, and the other per-service settings (`budget_interface`,
`budget_region_name`, `budget_status_code_retries`) work as they do for every
built-in `openstack` service.

## Budgets

```sh
# Alert the "ops" channel at 80% of $1,000 spent this month, and when the
# month is forecast to exceed $1,200.
openstack budget create team-monthly \
  --amount 1000 --period monthly \
  --threshold name=warn,value=80,channel=ops \
  --threshold name=overrun,value=1200,unit=absolute,basis=forecasted,channel=ops

openstack budget list
openstack budget show team-monthly
openstack budget set team-monthly --amount 1500 --service compute
openstack budget window list team-monthly
openstack budget notification list team-monthly
openstack budget delete team-monthly
```

A threshold is `name=<name>,value=<decimal>` plus optional keys: `basis`
(`actual` spend so far, the default, or the `forecasted` total for the window),
`unit` (`percentage` of the budget, the default, or an `absolute` amount),
`comparison` (default `greater_than_or_equal`), `channel` (repeatable),
`message` and `enabled`. Values cannot contain commas.

`--period custom` takes `--start` and `--end` as dates (midnight UTC) or
RFC 3339 times with an offset. `--service`, `--region` and `--usage-type`
narrow what is counted; without them every one counts.

`budget set` reads the budget, applies your changes and writes it back with the
revision it read. If someone else changed it in between, the write is refused
with a conflict rather than overwriting their change. `--threshold` on `set`
replaces all thresholds; `--no-service`, `--no-region` and `--no-usage-type`
clear a filter.

Budgets and channels can be named by ID or by name. A name that matches more
than one is refused; use the ID. Administrators can pass `--project` (name or
ID) to act on another project.

## Notification channels

```sh
openstack budget channel create ops --type webhook --url https://hooks.example/budget
openstack budget channel create me --type email --address me@example.com
openstack budget channel list
openstack budget channel secret rotate ops
openstack budget channel confirmation resend me
openstack budget channel delete ops
```

A webhook's signing secret is shown only when the channel is created and when
it is rotated. An email address receives nothing until its owner follows the
confirmation link.

All commands support the standard formatters, for example `-f json`. Amounts
stay decimal strings, so no precision is lost.

## Python

```python
from budgetclient import Client
from budgetclient.v1.budgets import to_update_spec

# session is any keystoneauth1 Session, e.g. from openstack.config.
client = Client("1", session=session, region_name="ca-ymq-1")

for budget in client.budgets.list():
    print(budget["name"], (budget["status"] or {}).get("actual_spend"))

budget = client.budgets.find("team-monthly")
spec = to_update_spec(budget)
spec["amount"] = "1500"
client.budgets.update(budget["id"], **spec)  # exceptions.Conflict if stale
```

Managers: `client.budgets` (`list`, `get`, `find`, `create`, `update`,
`delete`, `windows`, `notifications`) and `client.notification_channels`
(`list`, `get`, `find`, `create`, `delete`, `resend_confirmation`,
`rotate_secret`). Errors are `budgetclient.exceptions.ClientException`
subclasses carrying the status, problem document and request ID; each is also
the matching `keystoneauth1.exceptions` HTTP error.

## Development

The transport, errors, CLI wiring and model generation come from
[vexxhost-clientcore](https://github.com/vexxhost/python-clientcore), shared with
vexxhost-costexplorerclient.

```sh
pip install -e '.[test]'
pytest
```

The tests drive the real `openstack` CLI against an in-memory budget API.

`budgetclient/v1/models.py` is generated from the schema budget serves, and the
CLI reads its choices (periods, services, threshold bases, units, comparisons,
channel types) from it. Regenerate with:

```sh
make generate SPEC_URL=https://budget.public.global.vexxhost.net/openapi-3.0.yaml
```

The `regenerate` workflow does this daily when the repository variable
`SPEC_URL` is set, and opens a pull request when the schema changed.
