Skip to content

Commit 5a1b160

Browse files
authored
feat: add async support (#146)
Replace requests with niquests, and drop support for Python 3.6.
1 parent b6bcd2a commit 5a1b160

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+3951
-217
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ jobs:
3131
runs-on: ${{ matrix.os }}
3232
strategy:
3333
matrix:
34-
os: ["ubuntu-20.04"]
34+
os: ["ubuntu-latest"]
3535
python-version: [
36-
'3.6', '3.12',
37-
'pypy-3.6', 'pypy-3.10',
36+
'3.7', '3.12',
37+
'pypy-3.7', 'pypy-3.10',
3838
]
3939
steps:
4040
- uses: actions/checkout@v4
@@ -58,7 +58,7 @@ jobs:
5858
python -m pip install --editable=.[test,develop]
5959
6060
- name: Check code style
61-
if: matrix.python-version != '3.6' && matrix.python-version != 'pypy-3.6'
61+
if: matrix.python-version != '3.7' && matrix.python-version != 'pypy-3.7'
6262
run: |
6363
poe lint
6464

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## unreleased
44

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

612
## 3.11.2 (2024-03-07)
713

README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,31 @@ grafana.organization.create_organization(
7373
organization={"name": "new_organization"})
7474
```
7575

76+
Or using asynchronous code... the interfaces are identical except for the fact that you will handle coroutines (async/await).
77+
78+
```python
79+
from grafana_client import AsyncGrafanaApi
80+
import asyncio
81+
82+
async def main():
83+
# Connect to Grafana API endpoint using the `GrafanaApi` class
84+
grafana = AsyncGrafanaApi.from_url("https://username:password@daq.example.org/grafana/")
85+
86+
# Create user
87+
user = await grafana.admin.create_user({
88+
"name": "User",
89+
"email": "user@example.org",
90+
"login": "user",
91+
"password": "userpassword",
92+
"OrgId": 1,
93+
})
94+
95+
# Change user password
96+
user = await grafana.admin.change_user_password(2, "newpassword")
97+
98+
asyncio.run(main())
99+
```
100+
76101
### Example programs
77102

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

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

138163
## Selecting Organizations
139164

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

167192
## Proxy
168193

169-
The underlying `requests` library honors the `HTTP_PROXY` and `HTTPS_PROXY`
194+
The underlying `niquests` library honors the `HTTP_PROXY` and `HTTPS_PROXY`
170195
environment variables. Setting them before invoking an application using
171196
`grafana-client` has been confirmed to work. For example:
172197
```
173198
export HTTP_PROXY=10.10.1.10:3128
174199
export HTTPS_PROXY=10.10.1.11:1080
175200
```
176201

202+
## DNS Resolver
203+
204+
`niquests` support using a custom DNS resolver, like but not limited, DNS-over-HTTPS, and DNS-over-QUIC.
205+
You will have to set `NIQUESTS_DNS_URL` environment variable. For example:
206+
```
207+
export NIQUESTS_DNS_URL="doh+cloudflare://"
208+
```
209+
210+
See the [documentation](https://niquests.readthedocs.io/en/latest/user/quickstart.html#set-dns-via-environment) to learn
211+
more about accepted URL parameters and protocols.
177212

178213
## Details
179214

codecov.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ coverage:
88
patch:
99
default:
1010
informational: true
11+
ignore:
12+
- grafana_client/elements/_async/*
13+
- test/*
14+
- test/elements/*

docs/development.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ python -m unittest -k preference -vvv
2323

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

26+
### Async code
27+
28+
If you update any piece of code in `grafana_client/elements/*`, please run:
29+
30+
```
31+
python script/generate_async.py
32+
```
33+
34+
Do not edit files in `grafana_client/elements/_async/*` manually.
35+
2636
## Run Grafana
2737
```
2838
docker run --rm -it --publish=3000:3000 --env='GF_SECURITY_ADMIN_PASSWORD=admin' grafana/grafana:9.3.6

examples/async-folders-dashboard.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
About
3+
=====
4+
5+
Example program for listing folders and getting a dashboard of a remote Grafana
6+
instance. By default, it uses `play.grafana.org`. Please adjust to your needs.
7+
8+
9+
Synopsis
10+
========
11+
::
12+
13+
source .venv/bin/activate
14+
python examples/folders-dashboard.py | jq
15+
"""
16+
17+
import asyncio
18+
import json
19+
import sys
20+
from time import perf_counter
21+
22+
from grafana_client import AsyncGrafanaApi
23+
24+
25+
async def fetch_dashboard(grafana, uid):
26+
print(f"## Dashboard with UID {uid} at play.grafana.org", file=sys.stderr)
27+
dashboard = await grafana.dashboard.get_dashboard(uid)
28+
print(json.dumps(dashboard, indent=2))
29+
30+
31+
async def main():
32+
before = perf_counter()
33+
# Connect to public Grafana instance of Grafana Labs fame.
34+
grafana = AsyncGrafanaApi(host="play.grafana.org")
35+
36+
print("## All folders on play.grafana.org", file=sys.stderr)
37+
folders = await grafana.folder.get_all_folders()
38+
print(json.dumps(folders, indent=2))
39+
40+
tasks = []
41+
42+
for folder in folders:
43+
if folder["id"] > 0:
44+
tasks.append(fetch_dashboard(grafana, folder["uid"]))
45+
if len(tasks) == 4:
46+
break
47+
48+
await asyncio.gather(*tasks)
49+
print(f"## Completed in {perf_counter() - before}s", file=sys.stderr)
50+
51+
52+
if __name__ == "__main__":
53+
asyncio.run(main())

examples/datasource-health-check.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
from optparse import OptionParser
1111

12-
import requests
12+
import niquests
1313
from verlib2 import Version
1414

1515
from grafana_client import GrafanaApi
@@ -79,7 +79,7 @@ def run(grafana: GrafanaApi):
7979

8080
try:
8181
grafana_client.connect()
82-
except requests.exceptions.ConnectionError:
82+
except niquests.exceptions.ConnectionError:
8383
logger.exception("Connecting to Grafana failed")
8484
raise SystemExit(1)
8585

examples/datasource-health-probe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import sys
1010
from optparse import OptionParser
1111

12-
import requests
12+
import niquests
1313
from verlib2 import Version
1414

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

130130
try:
131131
grafana_client.connect()
132-
except requests.exceptions.ConnectionError:
132+
except niquests.exceptions.ConnectionError:
133133
logger.exception("Connecting to Grafana failed")
134134
raise SystemExit(1)
135135

examples/datasource-smartquery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
import logging
4949
from optparse import OptionParser
5050

51-
import requests
51+
import niquests
5252

5353
from grafana_client import GrafanaApi
5454
from grafana_client.model import DatasourceIdentifier
@@ -97,7 +97,7 @@ def run(grafana: GrafanaApi):
9797

9898
try:
9999
grafana_client.connect()
100-
except requests.exceptions.ConnectionError:
100+
except niquests.exceptions.ConnectionError:
101101
logger.exception("Connecting to Grafana failed")
102102
raise SystemExit(1)
103103

examples/folders-dashboard.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,26 @@
1616

1717
import json
1818
import sys
19+
from time import perf_counter
1920

2021
from grafana_client import GrafanaApi
2122

2223

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

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

31-
print("## Dashboard with UID 000000012 at play.grafana.org", file=sys.stderr)
32-
dashboard_000000012 = grafana.dashboard.get_dashboard("000000012")
33-
print(json.dumps(dashboard_000000012, indent=2))
33+
for folder in folders[:4]:
34+
print(f"## Dashboard with UID {folder['uid']} at play.grafana.org", file=sys.stderr)
35+
dashboard = grafana.dashboard.get_dashboard(folder["uid"])
36+
print(json.dumps(dashboard, indent=2))
37+
38+
print(f"## Completed in {perf_counter() - before}s", file=sys.stderr)
3439

3540

3641
if __name__ == "__main__":

0 commit comments

Comments
 (0)