Add plotting tools
Browse files- Gradio_UI.py +1 -0
- README.md +19 -1
- app.py +13 -118
- requirements.txt +1 -0
- tools/diagrams.py +201 -0
Gradio_UI.py
CHANGED
@@ -271,6 +271,7 @@ class GradioUI:
|
|
271 |
- List all the countries that are there.
|
272 |
- How many distinct schools are there in each country? Show as a table. Sort by the country names.
|
273 |
- Show the average download speed and latency of each school in Kenya.
|
|
|
274 |
'''
|
275 |
gr.Markdown(title)
|
276 |
|
|
|
271 |
- List all the countries that are there.
|
272 |
- How many distinct schools are there in each country? Show as a table. Sort by the country names.
|
273 |
- Show the average download speed and latency of each school in Kenya.
|
274 |
+
- Show the count of schools in each country as a bar diagram.
|
275 |
'''
|
276 |
gr.Markdown(title)
|
277 |
|
README.md
CHANGED
@@ -11,4 +11,22 @@ license: apache-2.0
|
|
11 |
short_description: 'QoScope '
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
short_description: 'QoScope '
|
12 |
---
|
13 |
|
14 |
+
# QoScope 🔭
|
15 |
+
|
16 |
+
QoS data analysis thousands of school networks under UNICEF/Giga
|
17 |
+
|
18 |
+
---
|
19 |
+
|
20 |
+
|
21 |
+
# Installation
|
22 |
+
|
23 |
+
```bash
|
24 |
+
pip install -r requirements.txt
|
25 |
+
python app.py
|
26 |
+
```
|
27 |
+
|
28 |
+
Add `HF_TOKEN` environment variable, e.g., in the `.env` file.
|
29 |
+
|
30 |
+
---
|
31 |
+
|
32 |
+
QoScope is released under an Apache 2.0 license.
|
app.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
import numpy as np
|
5 |
import yaml
|
6 |
-
|
7 |
import sqlalchemy as sqa
|
8 |
from Gradio_UI import GradioUI
|
9 |
-
from smolagents import CodeAgent, HfApiModel,
|
|
|
|
|
10 |
from tools.final_answer import FinalAnswerTool
|
11 |
|
|
|
12 |
DATABASE_PATH = 'sqlite:///resampled_daily_avg.sqlite'
|
13 |
|
14 |
db_engine = sqa.create_engine(DATABASE_PATH, echo=False)
|
@@ -20,7 +21,8 @@ def run_sql_query(sql_query: str) -> str:
|
|
20 |
Run a SQL query on a table in a SQLite database and return the output/result as a string.
|
21 |
The output contains one row of result(s) in each line.
|
22 |
The output is simple text without any Markdown styling. An agent should take this plain output
|
23 |
-
and format appropriately when required, e.g., as a Markdown table or summarizing the results
|
|
|
24 |
|
25 |
The only available table in the SQLite database is `school_measurements`.
|
26 |
The table's CREATE statement (DDL) is as follows:
|
@@ -37,8 +39,8 @@ def run_sql_query(sql_query: str) -> str:
|
|
37 |
iso3_format TEXT
|
38 |
)
|
39 |
|
40 |
-
IMPORTANT: You will ONLY execute SELECT queries.
|
41 |
-
|
42 |
|
43 |
Args:
|
44 |
sql_query: An appropriate, correct SQL query for a SQLite database
|
@@ -54,118 +56,11 @@ def run_sql_query(sql_query: str) -> str:
|
|
54 |
rows = con.execute(sqa.text(sql_query))
|
55 |
for row in rows:
|
56 |
# Each row is a tuple
|
57 |
-
print(f'\n\n>>>{row=}')
|
58 |
output += '\n' + str(row)
|
59 |
|
60 |
return output
|
61 |
|
62 |
|
63 |
-
# @tool
|
64 |
-
def plot_line_diagram(
|
65 |
-
x_values,
|
66 |
-
y_values_list,
|
67 |
-
labels=None,
|
68 |
-
title='Line Diagram',
|
69 |
-
xlabel='X-axis',
|
70 |
-
ylabel='Y-axis'
|
71 |
-
):
|
72 |
-
"""
|
73 |
-
Plots a line diagram with one or more y-values.
|
74 |
-
|
75 |
-
:param x_values: List of x-values.
|
76 |
-
:param y_values_list: List of lists containing y-values. Each inner list represents a separate line.
|
77 |
-
:param labels: List of labels for each line (optional).
|
78 |
-
:param title: Title of the plot (default: 'Line Diagram').
|
79 |
-
:param xlabel: Label for the X-axis (default: 'X-axis').
|
80 |
-
:param ylabel: Label for the Y-axis (default: 'Y-axis').
|
81 |
-
"""
|
82 |
-
plt.figure(figsize=(10, 6))
|
83 |
-
|
84 |
-
for i, y_values in enumerate(y_values_list):
|
85 |
-
label = labels[i] if labels and i < len(labels) else f'Line {i + 1}'
|
86 |
-
plt.plot(x_values, y_values, label=label)
|
87 |
-
|
88 |
-
plt.title(title)
|
89 |
-
plt.xlabel(xlabel)
|
90 |
-
plt.ylabel(ylabel)
|
91 |
-
plt.legend()
|
92 |
-
plt.grid(True)
|
93 |
-
plt.show()
|
94 |
-
|
95 |
-
|
96 |
-
@tool
|
97 |
-
def plot_bar_diagram(
|
98 |
-
x_values: list,
|
99 |
-
y_values_list: list[list[int | float]],
|
100 |
-
labels: list[str] | None = None,
|
101 |
-
title: str = 'Bar Diagram',
|
102 |
-
xlabel: str = 'X-axis',
|
103 |
-
ylabel: str = 'Y-axis'
|
104 |
-
) -> str:
|
105 |
-
"""
|
106 |
-
Plot a bar diagram with one or more y-values and save the image to a temporary file.
|
107 |
-
Return the path to the saved image file. The path can be used to display the image.
|
108 |
-
|
109 |
-
Args:
|
110 |
-
x_values: List of x-values.
|
111 |
-
y_values_list: List of lists containing y-values. Each inner list represents a separate set of bars.
|
112 |
-
labels: List of labels for each set of bars (optional).
|
113 |
-
title: Title of the plot (default: 'Bar Diagram').
|
114 |
-
xlabel: Label for the X-axis (default: 'X-axis').
|
115 |
-
ylabel: Label for the Y-axis (default: 'Y-axis').
|
116 |
-
|
117 |
-
Returns:
|
118 |
-
Path to the saved image file.
|
119 |
-
"""
|
120 |
-
bar_width = 0.2
|
121 |
-
n = len(y_values_list)
|
122 |
-
|
123 |
-
# Set positions of bars on X axis
|
124 |
-
r = [np.arange(len(x_values))]
|
125 |
-
for i in range(1, n):
|
126 |
-
r.append([x + bar_width for x in r[i - 1]])
|
127 |
-
|
128 |
-
plt.figure(figsize=(10, 6))
|
129 |
-
|
130 |
-
for i, y_values in enumerate(y_values_list):
|
131 |
-
label = labels[i] if labels and i < len(labels) else f'Set {i + 1}'
|
132 |
-
plt.bar(r[i], y_values, width=bar_width, label=label)
|
133 |
-
|
134 |
-
# Adding xticks
|
135 |
-
plt.xlabel(xlabel)
|
136 |
-
plt.ylabel(ylabel)
|
137 |
-
plt.title(title)
|
138 |
-
plt.xticks(
|
139 |
-
[r + bar_width * (n - 1) / 2 for r in range(len(x_values))],
|
140 |
-
x_values,
|
141 |
-
rotation=45,
|
142 |
-
ha='right'
|
143 |
-
)
|
144 |
-
plt.legend()
|
145 |
-
plt.grid(True, axis='y')
|
146 |
-
plt.tight_layout()
|
147 |
-
|
148 |
-
# Save the plot as an image file in a temporary directory
|
149 |
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
|
150 |
-
plt.savefig(temp_file.name)
|
151 |
-
plt.close()
|
152 |
-
|
153 |
-
return temp_file.name
|
154 |
-
|
155 |
-
|
156 |
-
# class TextToImageTool(Tool):
|
157 |
-
# description = "This tool creates an image according to a prompt, which is a text description."
|
158 |
-
# name = "image_generator"
|
159 |
-
# inputs = {"prompt": {"type": "string", "description": "The image generator prompt. Don't hesitate to add details in the prompt to make the image look better, like 'high-res, photorealistic', etc."}}
|
160 |
-
# output_type = "image"
|
161 |
-
# model_sdxl = "black-forest-labs/FLUX.1-schnell"
|
162 |
-
# client = InferenceClient(model_sdxl)
|
163 |
-
#
|
164 |
-
#
|
165 |
-
# def forward(self, prompt):
|
166 |
-
# return self.client.text_to_image(prompt)
|
167 |
-
|
168 |
-
|
169 |
### Main block ###
|
170 |
|
171 |
final_answer = FinalAnswerTool()
|
@@ -176,14 +71,14 @@ code_model = HfApiModel(
|
|
176 |
custom_role_conversions=None,
|
177 |
)
|
178 |
|
179 |
-
with open('prompts.yaml', 'r') as stream:
|
180 |
prompt_templates = yaml.safe_load(stream)
|
181 |
|
182 |
agent = CodeAgent(
|
183 |
model=code_model,
|
184 |
tools=[
|
185 |
run_sql_query,
|
186 |
-
|
187 |
final_answer,
|
188 |
],
|
189 |
max_steps=6,
|
|
|
1 |
+
"""
|
2 |
+
QoScope agent with tools.
|
3 |
+
"""
|
|
|
4 |
import yaml
|
|
|
5 |
import sqlalchemy as sqa
|
6 |
from Gradio_UI import GradioUI
|
7 |
+
from smolagents import CodeAgent, HfApiModel, tool
|
8 |
+
|
9 |
+
from tools.diagrams import PlotTool
|
10 |
from tools.final_answer import FinalAnswerTool
|
11 |
|
12 |
+
|
13 |
DATABASE_PATH = 'sqlite:///resampled_daily_avg.sqlite'
|
14 |
|
15 |
db_engine = sqa.create_engine(DATABASE_PATH, echo=False)
|
|
|
21 |
Run a SQL query on a table in a SQLite database and return the output/result as a string.
|
22 |
The output contains one row of result(s) in each line.
|
23 |
The output is simple text without any Markdown styling. An agent should take this plain output
|
24 |
+
and format appropriately when required, e.g., as a Markdown table or summarizing the results
|
25 |
+
in words.
|
26 |
|
27 |
The only available table in the SQLite database is `school_measurements`.
|
28 |
The table's CREATE statement (DDL) is as follows:
|
|
|
39 |
iso3_format TEXT
|
40 |
)
|
41 |
|
42 |
+
IMPORTANT: You will ONLY execute SELECT queries. You will NEVER execute any other types of
|
43 |
+
SQL queries, e.g., INSERT, DELETE, DROP, and so on, which changes the database in any way.
|
44 |
|
45 |
Args:
|
46 |
sql_query: An appropriate, correct SQL query for a SQLite database
|
|
|
56 |
rows = con.execute(sqa.text(sql_query))
|
57 |
for row in rows:
|
58 |
# Each row is a tuple
|
|
|
59 |
output += '\n' + str(row)
|
60 |
|
61 |
return output
|
62 |
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
### Main block ###
|
65 |
|
66 |
final_answer = FinalAnswerTool()
|
|
|
71 |
custom_role_conversions=None,
|
72 |
)
|
73 |
|
74 |
+
with open('prompts.yaml', 'r', encoding='utf-8') as stream:
|
75 |
prompt_templates = yaml.safe_load(stream)
|
76 |
|
77 |
agent = CodeAgent(
|
78 |
model=code_model,
|
79 |
tools=[
|
80 |
run_sql_query,
|
81 |
+
PlotTool(),
|
82 |
final_answer,
|
83 |
],
|
84 |
max_steps=6,
|
requirements.txt
CHANGED
@@ -4,3 +4,4 @@ pandas
|
|
4 |
SQLAlchemy
|
5 |
gradio
|
6 |
matplotlib
|
|
|
|
4 |
SQLAlchemy
|
5 |
gradio
|
6 |
matplotlib
|
7 |
+
Pillow
|
tools/diagrams.py
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import io
|
2 |
+
import tempfile
|
3 |
+
|
4 |
+
import numpy as np
|
5 |
+
from PIL import Image
|
6 |
+
from PIL.ImageFile import ImageFile
|
7 |
+
from matplotlib import pyplot as plt
|
8 |
+
from smolagents.tools import Tool
|
9 |
+
|
10 |
+
|
11 |
+
def _plot_line_diagram(
|
12 |
+
x_values: list,
|
13 |
+
y_values_list: list[list[int | float]],
|
14 |
+
labels: list[str] | None = None,
|
15 |
+
title: str = 'Bar Diagram',
|
16 |
+
xlabel: str = 'X-axis',
|
17 |
+
ylabel: str = 'Y-axis'
|
18 |
+
) -> str:
|
19 |
+
"""
|
20 |
+
Plot a line diagram with one or more y-values and save the image to a temporary file.
|
21 |
+
Return the path to the saved image file.
|
22 |
+
|
23 |
+
:param x_values: List of x-values.
|
24 |
+
:param y_values_list: List of lists containing y-values.
|
25 |
+
Each inner list represents a separate line.
|
26 |
+
:param labels: List of labels for each line (optional).
|
27 |
+
:param title: Title of the plot (default: 'Line Diagram').
|
28 |
+
:param xlabel: Label for the X-axis (default: 'X-axis').
|
29 |
+
:param ylabel: Label for the Y-axis (default: 'Y-axis').
|
30 |
+
:return: Path to the saved image file.
|
31 |
+
"""
|
32 |
+
|
33 |
+
plt.figure(figsize=(10, 6))
|
34 |
+
|
35 |
+
for i, y_values in enumerate(y_values_list):
|
36 |
+
label = labels[i] if labels and i < len(labels) else f'Line {i+1}'
|
37 |
+
plt.plot(x_values, y_values, label=label)
|
38 |
+
|
39 |
+
plt.title(title)
|
40 |
+
plt.xlabel(xlabel)
|
41 |
+
plt.ylabel(ylabel)
|
42 |
+
plt.legend()
|
43 |
+
plt.grid(True)
|
44 |
+
|
45 |
+
# Save the plot as an image file in a temporary directory
|
46 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
|
47 |
+
plt.savefig(temp_file.name)
|
48 |
+
plt.close()
|
49 |
+
|
50 |
+
return temp_file.name
|
51 |
+
|
52 |
+
|
53 |
+
def _plot_bar_diagram(
|
54 |
+
x_values: list,
|
55 |
+
y_values_list: list[list[int | float]],
|
56 |
+
labels: list[str] | None = None,
|
57 |
+
title: str = 'Bar Diagram',
|
58 |
+
xlabel: str = 'X-axis',
|
59 |
+
ylabel: str = 'Y-axis'
|
60 |
+
) -> str:
|
61 |
+
"""
|
62 |
+
Plot a bar diagram with one or more y-values and save the image to a temporary file.
|
63 |
+
Return the path to the saved image file.
|
64 |
+
|
65 |
+
:param x_values: List of x-values.
|
66 |
+
:param y_values_list: List of lists containing y-values.
|
67 |
+
Each inner list represents a separate set of bars.
|
68 |
+
:param labels: List of labels for each set of bars (optional).
|
69 |
+
:param title: Title of the plot (default: 'Bar Diagram').
|
70 |
+
:param xlabel: Label for the X-axis (default: 'X-axis').
|
71 |
+
:param ylabel: Label for the Y-axis (default: 'Y-axis').
|
72 |
+
:return: Path to the saved image file.
|
73 |
+
"""
|
74 |
+
|
75 |
+
bar_width = 0.2
|
76 |
+
n = len(y_values_list)
|
77 |
+
|
78 |
+
# Set positions of bars on X axis
|
79 |
+
r = [np.arange(len(x_values))]
|
80 |
+
for i in range(1, n):
|
81 |
+
r.append([x + bar_width for x in r[i - 1]])
|
82 |
+
|
83 |
+
plt.figure(figsize=(10, 6))
|
84 |
+
|
85 |
+
for i, y_values in enumerate(y_values_list):
|
86 |
+
label = labels[i] if labels and i < len(labels) else f'Set {i + 1}'
|
87 |
+
plt.bar(r[i], y_values, width=bar_width, label=label)
|
88 |
+
|
89 |
+
# Adding xticks
|
90 |
+
plt.xlabel(xlabel)
|
91 |
+
plt.ylabel(ylabel)
|
92 |
+
plt.title(title)
|
93 |
+
plt.xticks(
|
94 |
+
[r + bar_width * (n - 1) / 2 for r in range(len(x_values))],
|
95 |
+
x_values,
|
96 |
+
rotation=45,
|
97 |
+
ha='right'
|
98 |
+
)
|
99 |
+
plt.legend()
|
100 |
+
plt.grid(True, axis='y')
|
101 |
+
plt.tight_layout()
|
102 |
+
|
103 |
+
# Save the plot as an image file in a temporary directory
|
104 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
|
105 |
+
plt.savefig(temp_file.name)
|
106 |
+
plt.close()
|
107 |
+
|
108 |
+
return temp_file.name
|
109 |
+
|
110 |
+
|
111 |
+
class PlotTool(Tool):
|
112 |
+
"""
|
113 |
+
A tool to plot bar and line diagrams and return the images.
|
114 |
+
"""
|
115 |
+
|
116 |
+
name = 'plot_bar_line_diagrams'
|
117 |
+
description = (
|
118 |
+
'Plot a bar or line diagram with one or more y-values and save the image.'
|
119 |
+
' Return the saved image file as `ImageFile`.'
|
120 |
+
' An agent must take this `ImageFile` and display the image.'
|
121 |
+
)
|
122 |
+
inputs = {
|
123 |
+
'plot_type': {
|
124 |
+
'type': 'string',
|
125 |
+
'description': 'The type of plot. Only two values are valid: `bar` and `line`'
|
126 |
+
},
|
127 |
+
'x_values': {'type': 'array', 'description': 'List of x-values.'},
|
128 |
+
'y_values_list': {
|
129 |
+
'type': 'array',
|
130 |
+
'description': (
|
131 |
+
'A list of lists containing y-values (numbers).'
|
132 |
+
' Each inner list represents a separate set of bars.'
|
133 |
+
' The input type is list[list[int | float]]'
|
134 |
+
)
|
135 |
+
},
|
136 |
+
'labels': {
|
137 |
+
'type': 'array',
|
138 |
+
'nullable': True,
|
139 |
+
'description': (
|
140 |
+
'A list of labels for each set of bars (optional). Defaults to `None`.'
|
141 |
+
' If provided, the length of `labels` must be equal to the length of `x_values`.'
|
142 |
+
)
|
143 |
+
},
|
144 |
+
'title': {
|
145 |
+
'type': 'string',
|
146 |
+
'nullable': True,
|
147 |
+
'description': 'Title of the plot (default: "Bar Diagram")'
|
148 |
+
},
|
149 |
+
'xlabel': {
|
150 |
+
'type': 'string',
|
151 |
+
'nullable': True,
|
152 |
+
'description': 'Label for the X-axis (default: "X-axis")'
|
153 |
+
},
|
154 |
+
'ylabel': {
|
155 |
+
'type': 'string',
|
156 |
+
'nullable': True,
|
157 |
+
'description': 'Label for the Y-axis (default: "Y-axis").'
|
158 |
+
},
|
159 |
+
}
|
160 |
+
output_type = 'image'
|
161 |
+
|
162 |
+
def __init__(self, **kwargs):
|
163 |
+
super().__init__()
|
164 |
+
|
165 |
+
def forward(
|
166 |
+
self,
|
167 |
+
plot_type: str,
|
168 |
+
x_values: list,
|
169 |
+
y_values_list: list[list[int | float]],
|
170 |
+
labels: list[str] | None = None,
|
171 |
+
title: str = 'Bar Diagram',
|
172 |
+
xlabel: str = 'X-axis',
|
173 |
+
ylabel: str = 'Y-axis'
|
174 |
+
) -> ImageFile:
|
175 |
+
|
176 |
+
if plot_type == 'bar':
|
177 |
+
img_file_name = _plot_bar_diagram(
|
178 |
+
x_values=x_values,
|
179 |
+
y_values_list=y_values_list,
|
180 |
+
labels=labels,
|
181 |
+
xlabel=xlabel,
|
182 |
+
ylabel=ylabel,
|
183 |
+
title=title
|
184 |
+
)
|
185 |
+
elif plot_type == 'line':
|
186 |
+
img_file_name = _plot_line_diagram(
|
187 |
+
x_values=x_values,
|
188 |
+
y_values_list=y_values_list,
|
189 |
+
labels=labels,
|
190 |
+
xlabel=xlabel,
|
191 |
+
ylabel=ylabel,
|
192 |
+
title=title
|
193 |
+
)
|
194 |
+
else:
|
195 |
+
img_file_name = None
|
196 |
+
|
197 |
+
if img_file_name:
|
198 |
+
with open(img_file_name, 'rb') as in_file:
|
199 |
+
return Image.open(io.BytesIO(in_file.read()))
|
200 |
+
else:
|
201 |
+
return None
|