Skip to content

Commit

Permalink
feat: add async support (#146)
Browse files Browse the repository at this point in the history
Replace requests with niquests, and drop support for Python 3.6.
  • Loading branch information
Ousret authored Mar 29, 2024
1 parent b6bcd2a commit 5a1b160
Show file tree
Hide file tree
Showing 59 changed files with 3,951 additions and 217 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: ["ubuntu-20.04"]
os: ["ubuntu-latest"]
python-version: [
'3.6', '3.12',
'pypy-3.6', 'pypy-3.10',
'3.7', '3.12',
'pypy-3.7', 'pypy-3.10',
]
steps:
- uses: actions/checkout@v4
Expand All @@ -58,7 +58,7 @@ jobs:
python -m pip install --editable=.[test,develop]
- name: Check code style
if: matrix.python-version != '3.6' && matrix.python-version != 'pypy-3.6'
if: matrix.python-version != '3.7' && matrix.python-version != 'pypy-3.7'
run: |
poe lint
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## unreleased

* Change the HTTP backend Requests for Niquests.
In certain conditions, you will need to adjust your code, as it no longer propagates
`requests.exceptions.Timeout` exceptions, but uses `GrafanaTimeoutError` instead.
[Niquests](https://github.com/jawah/niquests) is a drop-in replacement of Requests and therefore remains compatible.
* Add asynchronous interface via `AsyncGrafanaClient`.
* Remove Python 3.6 support.

## 3.11.2 (2024-03-07)

Expand Down
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ grafana.organization.create_organization(
organization={"name": "new_organization"})
```

Or using asynchronous code... the interfaces are identical except for the fact that you will handle coroutines (async/await).

```python
from grafana_client import AsyncGrafanaApi
import asyncio

async def main():
# Connect to Grafana API endpoint using the `GrafanaApi` class
grafana = AsyncGrafanaApi.from_url("https://username:[email protected]/grafana/")

# Create user
user = await grafana.admin.create_user({
"name": "User",
"email": "[email protected]",
"login": "user",
"password": "userpassword",
"OrgId": 1,
})

# Change user password
user = await grafana.admin.change_user_password(2, "newpassword")

asyncio.run(main())
```

### Example programs

There are complete example programs to get you started within the [examples
Expand Down Expand Up @@ -133,7 +158,7 @@ grafana = GrafanaApi.from_env()
```

Please note that, on top of the specific examples above, the object obtained by
`credential` can be an arbitrary `requests.auth.AuthBase` instance.
`credential` can be an arbitrary `niquests.auth.AuthBase` instance.

## Selecting Organizations

Expand Down Expand Up @@ -166,14 +191,24 @@ scalar `float` value, or as a tuple of `(<read timeout>, <connect timeout>)`.

## Proxy

The underlying `requests` library honors the `HTTP_PROXY` and `HTTPS_PROXY`
The underlying `niquests` library honors the `HTTP_PROXY` and `HTTPS_PROXY`
environment variables. Setting them before invoking an application using
`grafana-client` has been confirmed to work. For example:
```
export HTTP_PROXY=10.10.1.10:3128
export HTTPS_PROXY=10.10.1.11:1080
```

## DNS Resolver

`niquests` support using a custom DNS resolver, like but not limited, DNS-over-HTTPS, and DNS-over-QUIC.
You will have to set `NIQUESTS_DNS_URL` environment variable. For example:
```
export NIQUESTS_DNS_URL="doh+cloudflare://"
```

See the [documentation](https://niquests.readthedocs.io/en/latest/user/quickstart.html#set-dns-via-environment) to learn
more about accepted URL parameters and protocols.

## Details

Expand Down
4 changes: 4 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ coverage:
patch:
default:
informational: true
ignore:
- grafana_client/elements/_async/*
- test/*
- test/elements/*
10 changes: 10 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ python -m unittest -k preference -vvv

Before creating a PR, you can run `poe format`, in order to resolve code style issues.

### Async code

If you update any piece of code in `grafana_client/elements/*`, please run:

```
python script/generate_async.py
```

Do not edit files in `grafana_client/elements/_async/*` manually.

## Run Grafana
```
docker run --rm -it --publish=3000:3000 --env='GF_SECURITY_ADMIN_PASSWORD=admin' grafana/grafana:9.3.6
Expand Down
53 changes: 53 additions & 0 deletions examples/async-folders-dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
About
=====
Example program for listing folders and getting a dashboard of a remote Grafana
instance. By default, it uses `play.grafana.org`. Please adjust to your needs.
Synopsis
========
::
source .venv/bin/activate
python examples/folders-dashboard.py | jq
"""

import asyncio
import json
import sys
from time import perf_counter

from grafana_client import AsyncGrafanaApi


async def fetch_dashboard(grafana, uid):
print(f"## Dashboard with UID {uid} at play.grafana.org", file=sys.stderr)
dashboard = await grafana.dashboard.get_dashboard(uid)
print(json.dumps(dashboard, indent=2))


async def main():
before = perf_counter()
# Connect to public Grafana instance of Grafana Labs fame.
grafana = AsyncGrafanaApi(host="play.grafana.org")

print("## All folders on play.grafana.org", file=sys.stderr)
folders = await grafana.folder.get_all_folders()
print(json.dumps(folders, indent=2))

tasks = []

for folder in folders:
if folder["id"] > 0:
tasks.append(fetch_dashboard(grafana, folder["uid"]))
if len(tasks) == 4:
break

await asyncio.gather(*tasks)
print(f"## Completed in {perf_counter() - before}s", file=sys.stderr)


if __name__ == "__main__":
asyncio.run(main())
4 changes: 2 additions & 2 deletions examples/datasource-health-check.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys
from optparse import OptionParser

import requests
import niquests
from verlib2 import Version

from grafana_client import GrafanaApi
Expand Down Expand Up @@ -79,7 +79,7 @@ def run(grafana: GrafanaApi):

try:
grafana_client.connect()
except requests.exceptions.ConnectionError:
except niquests.exceptions.ConnectionError:
logger.exception("Connecting to Grafana failed")
raise SystemExit(1)

Expand Down
4 changes: 2 additions & 2 deletions examples/datasource-health-probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys
from optparse import OptionParser

import requests
import niquests
from verlib2 import Version

from grafana_client import GrafanaApi
Expand Down Expand Up @@ -129,7 +129,7 @@ def run(grafana: GrafanaApi, grafana_version: Version = None):

try:
grafana_client.connect()
except requests.exceptions.ConnectionError:
except niquests.exceptions.ConnectionError:
logger.exception("Connecting to Grafana failed")
raise SystemExit(1)

Expand Down
4 changes: 2 additions & 2 deletions examples/datasource-smartquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
import logging
from optparse import OptionParser

import requests
import niquests

from grafana_client import GrafanaApi
from grafana_client.model import DatasourceIdentifier
Expand Down Expand Up @@ -97,7 +97,7 @@ def run(grafana: GrafanaApi):

try:
grafana_client.connect()
except requests.exceptions.ConnectionError:
except niquests.exceptions.ConnectionError:
logger.exception("Connecting to Grafana failed")
raise SystemExit(1)

Expand Down
11 changes: 8 additions & 3 deletions examples/folders-dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,26 @@

import json
import sys
from time import perf_counter

from grafana_client import GrafanaApi


def main():
before = perf_counter()
# Connect to public Grafana instance of Grafana Labs fame.
grafana = GrafanaApi(host="play.grafana.org")

print("## All folders on play.grafana.org", file=sys.stderr)
folders = grafana.folder.get_all_folders()
print(json.dumps(folders, indent=2))

print("## Dashboard with UID 000000012 at play.grafana.org", file=sys.stderr)
dashboard_000000012 = grafana.dashboard.get_dashboard("000000012")
print(json.dumps(dashboard_000000012, indent=2))
for folder in folders[:4]:
print(f"## Dashboard with UID {folder['uid']} at play.grafana.org", file=sys.stderr)
dashboard = grafana.dashboard.get_dashboard(folder["uid"])
print(json.dumps(dashboard, indent=2))

print(f"## Completed in {perf_counter() - before}s", file=sys.stderr)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion grafana_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
except ModuleNotFoundError: # pragma:nocover
from importlib_metadata import PackageNotFoundError, version

from .api import GrafanaApi # noqa:E402,F401
from .api import AsyncGrafanaApi, GrafanaApi # noqa:E402,F401
from .client import HeaderAuth, TokenAuth # noqa:E402,F401

__appname__ = "grafana-client"
Expand Down
Loading

0 comments on commit 5a1b160

Please sign in to comment.