Skip to content

Commit 5189d43

Browse files
committed
Slight refactoring
- Use Flask Blueprint for configuring service component - Handle data generator registration within different file
1 parent b2ae560 commit 5189d43

File tree

5 files changed

+161
-63
lines changed

5 files changed

+161
-63
lines changed

README.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ Grafana Pandas Datasource
66
*****
77
About
88
*****
9-
A Python-native Grafana datasource using Pandas for timeseries and table data.
10-
Inspired by and compatible with the simple json datasource.
9+
A REST API based on Flask for serving Pandas Dataframes to Grafana.
10+
11+
This way, a native Python application can be used to directly supply
12+
data to Grafana both easily and powerfully.
13+
14+
It was inspired by and is compatible with the simple json datasource.
1115

1216
https://gist.github.com/linar-jether/95ff412f9d19fdf5e51293eb0c09b850
1317

examples/sinewave-midnights/demo.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
1+
"""
2+
Copyright 2017 Linar <[email protected]>
3+
Copyright 2020 Andreas Motl <[email protected]>
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
"""
17+
import pandas as pd
18+
import numpy as np
19+
from grafana_pandas_datasource import create_app
20+
from grafana_pandas_datasource.registry import data_generators as dg
21+
from grafana_pandas_datasource.service import pandas_component
22+
123
"""
224
Demo for grafana-pandas-datasource.
325
26+
This is a demo program which generates data using NumPy and Pandas.
27+
It creates
28+
- a sine wave for data and
29+
- midnight times for annotations
30+
431
To query the reader, use ``<reader_name>:<query_string>``, e.g.
532
- ``sine_wave:24``
633
- ``midnights:xx``
734
"""
8-
import pandas as pd
9-
import numpy as np
10-
from grafana_pandas_datasource.service import get_application, add_reader, add_annotation_reader
1135

1236

13-
def main():
37+
def define_and_register_data():
1438

1539
# Sample timeseries reader.
1640
def get_sine(freq, ts_range):
@@ -22,12 +46,23 @@ def get_sine(freq, ts_range):
2246
def get_midnights(query_string, ts_range):
2347
return pd.Series(index=pd.date_range(ts_range['$gt'], ts_range['$lte'], freq='D', normalize=True), dtype='float64').fillna('Text for annotation - midnight')
2448

25-
# Register data generation functions.
26-
add_reader('sine_wave', get_sine)
27-
add_annotation_reader('midnights', get_midnights)
49+
# Register data generators.
50+
dg.add_metric_reader("sine_wave", get_sine)
51+
dg.add_annotation_reader("midnights", get_midnights)
52+
53+
54+
def main():
55+
56+
# Define and register data generators.
57+
define_and_register_data()
58+
59+
# Create Flask application.
60+
app = create_app()
61+
62+
# Register Pandas component.
63+
app.register_blueprint(pandas_component, url_prefix="/")
2864

29-
# Get Flask application and invoke it.
30-
app = get_application()
65+
# Invoke Flask application.
3166
app.run(host='127.0.0.1', port=3003, debug=True)
3267

3368

grafana_pandas_datasource/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Copyright 2017 Linar <[email protected]>
3+
Copyright 2020 Andreas Motl <[email protected]>
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
"""
17+
from flask import Flask
18+
from flask_cors import CORS
19+
20+
21+
def create_app(test_config=None) -> Flask:
22+
"""
23+
Create and configure the Flask application, with CORS.
24+
25+
- https://flask.palletsprojects.com/en/1.1.x/tutorial/factory/
26+
- https://flask-cors.readthedocs.io/
27+
28+
:param test_config:
29+
:return: Configured Flask application.
30+
"""
31+
32+
# Create Flask application.
33+
app = Flask(__name__)
34+
35+
# Initialize Cross Origin Resource sharing support for
36+
# the application on all routes, for all origins and methods.
37+
CORS(app)
38+
app.config['CORS_HEADERS'] = 'Content-Type'
39+
40+
return app

grafana_pandas_datasource/registry.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Copyright 2017 Linar <[email protected]>
3+
Copyright 2020 Andreas Motl <[email protected]>
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
"""
17+
from typing import Dict, Callable
18+
from dataclasses import dataclass, field
19+
20+
21+
@dataclass
22+
class DataGenerators:
23+
"""
24+
Store references to data generator functions
25+
yielding Pandas data frames.
26+
"""
27+
28+
metric_readers: Dict[str, Callable] = field(default_factory=dict)
29+
metric_finders: Dict[str, Callable] = field(default_factory=dict)
30+
annotation_readers: Dict[str, Callable] = field(default_factory=dict)
31+
panel_readers: Dict[str, Callable] = field(default_factory=dict)
32+
33+
def add_metric_reader(self, name, reader):
34+
self.metric_readers[name] = reader
35+
36+
def add_metric_finder(self, name, finder):
37+
self.metric_finders[name] = finder
38+
39+
def add_annotation_reader(self, name, reader):
40+
self.annotation_readers[name] = reader
41+
42+
def add_panel_reader(self, name, reader):
43+
self.panel_readers[name] = reader
44+
45+
46+
"""
47+
@dataclass
48+
class DataGeneratorRegistry:
49+
generators: Dict[str, DataGenerators] = field(default_factory=dict)
50+
"""
51+
52+
53+
# Global reference to instance of DataGenerators.
54+
data_generators: DataGenerators = DataGenerators()

grafana_pandas_datasource/service.py

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
"""
2-
A REST API based on Flask for serving Pandas Dataframes to Grafana.
3-
4-
The idea is to use ``flask_restful`` and create a Blueprint to be
5-
used by a parent project (i.e. a larger API project where ``/grafana/``
6-
endpoints are used by Grafana's SimpleJson plugin).
7-
8-
----
9-
102
Copyright 2017 Linar <[email protected]>
3+
Copyright 2020 Andreas Motl <[email protected]>
114
125
Licensed under the Apache License, Version 2.0 (the "License");
136
you may not use this file except in compliance with the License.
@@ -21,50 +14,25 @@
2114
See the License for the specific language governing permissions and
2215
limitations under the License.
2316
"""
24-
from flask import Flask, request, jsonify, abort
25-
from flask_cors import CORS, cross_origin
17+
from flask import Blueprint, request, jsonify, abort
18+
from flask_cors import cross_origin
2619
import pandas as pd
2720

21+
from grafana_pandas_datasource.registry import data_generators as dg
2822
from grafana_pandas_datasource.util import dataframe_to_response, dataframe_to_json_table, annotations_to_response
2923

30-
31-
app = Flask(__name__)
32-
33-
cors = CORS(app)
34-
app.config['CORS_HEADERS'] = 'Content-Type'
35-
24+
pandas_component = Blueprint('pandas-component', __name__)
3625
methods = ('GET', 'POST')
3726

38-
metric_finders = {}
39-
metric_readers = {}
40-
annotation_readers = {}
41-
panel_readers = {}
42-
43-
44-
def add_reader(name, reader):
45-
metric_readers[name] = reader
46-
4727

48-
def add_finder(name, finder):
49-
metric_finders[name] = finder
50-
51-
52-
def add_annotation_reader(name, reader):
53-
annotation_readers[name] = reader
54-
55-
56-
def add_panel_reader(name, reader):
57-
panel_readers[name] = reader
58-
59-
60-
@app.route('/', methods=methods)
28+
@pandas_component.route('/', methods=methods)
6129
@cross_origin()
6230
def hello_world():
6331
print(request.headers, request.get_json())
6432
return 'Jether\'s Grafana Pandas Datasource, used for rendering HTML panels and timeseries data.'
6533

6634

67-
@app.route('/search', methods=methods)
35+
@pandas_component.route('/search', methods=methods)
6836
@cross_origin()
6937
def find_metrics():
7038
print(request.headers, request.get_json())
@@ -77,19 +45,20 @@ def find_metrics():
7745
else:
7846
finder = target
7947

80-
if not target or finder not in metric_finders:
48+
if not target or finder not in dg.metric_finders:
8149
metrics = []
8250
if target == '*':
83-
metrics += metric_finders.keys() + metric_readers.keys()
51+
metrics += dg.metric_finders.keys()
52+
metrics += dg.metric_readers.keys()
8453
else:
8554
metrics.append(target)
8655

8756
return jsonify(metrics)
8857
else:
89-
return jsonify(list(metric_finders[finder](target)))
58+
return jsonify(list(dg.metric_finders[finder](target)))
9059

9160

92-
@app.route('/query', methods=methods)
61+
@pandas_component.route('/query', methods=methods)
9362
@cross_origin(max_age=600)
9463
def query_metrics():
9564
print(request.headers, request.get_json())
@@ -112,7 +81,7 @@ def query_metrics():
11281
req_type = target.get('type', 'timeserie')
11382

11483
finder, target = target['target'].split(':', 1)
115-
query_results = metric_readers[finder](target, ts_range)
84+
query_results = dg.metric_readers[finder](target, ts_range)
11685

11786
if req_type == 'table':
11887
results.extend(dataframe_to_json_table(target, query_results))
@@ -122,7 +91,7 @@ def query_metrics():
12291
return jsonify(results)
12392

12493

125-
@app.route('/annotations', methods=methods)
94+
@pandas_component.route('/annotations', methods=methods)
12695
@cross_origin(max_age=600)
12796
def query_annotations():
12897
print(request.headers, request.get_json())
@@ -139,12 +108,12 @@ def query_annotations():
139108
abort(404, Exception('Target must be of type: <finder>:<metric_query>, got instead: ' + query))
140109

141110
finder, target = query.split(':', 1)
142-
results.extend(annotations_to_response(query, annotation_readers[finder](target, ts_range)))
111+
results.extend(annotations_to_response(query, dg.annotation_readers[finder](target, ts_range)))
143112

144113
return jsonify(results)
145114

146115

147-
@app.route('/panels', methods=methods)
116+
@pandas_component.route('/panels', methods=methods)
148117
@cross_origin()
149118
def get_panel():
150119
print(request.headers, request.get_json())
@@ -159,8 +128,4 @@ def get_panel():
159128
abort(404, Exception('Target must be of type: <finder>:<metric_query>, got instead: ' + query))
160129

161130
finder, target = query.split(':', 1)
162-
return panel_readers[finder](target, ts_range)
163-
164-
165-
def get_application():
166-
return app
131+
return dg.panel_readers[finder](target, ts_range)

0 commit comments

Comments
 (0)