Spaces:
Running
Running
Add /common sub directory
Browse files- tools/common/__init__.py +0 -0
- tools/common/base/__init__.py +0 -0
- tools/common/base/api_base.py +42 -0
- tools/common/org_search_component.py +223 -0
tools/common/__init__.py
ADDED
File without changes
|
tools/common/base/__init__.py
ADDED
File without changes
|
tools/common/base/api_base.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Optional, Any
|
2 |
+
|
3 |
+
from urllib3.util.retry import Retry
|
4 |
+
from requests.adapters import HTTPAdapter
|
5 |
+
import requests
|
6 |
+
|
7 |
+
|
8 |
+
class BaseAPI:
|
9 |
+
|
10 |
+
def __init__(
|
11 |
+
self,
|
12 |
+
url: str,
|
13 |
+
headers: Optional[Dict[str, Any]] = None,
|
14 |
+
total_retries: int = 3,
|
15 |
+
backoff_factor: int = 2
|
16 |
+
) -> None:
|
17 |
+
total_retries = max(total_retries, 10)
|
18 |
+
|
19 |
+
adapter = HTTPAdapter(
|
20 |
+
max_retries=Retry(
|
21 |
+
total=total_retries,
|
22 |
+
status_forcelist=[429, 500, 502, 503, 504],
|
23 |
+
allowed_methods=frozenset({"HEAD", "GET", "POST", "OPTIONS"}),
|
24 |
+
backoff_factor=backoff_factor,
|
25 |
+
)
|
26 |
+
)
|
27 |
+
self.session = requests.Session()
|
28 |
+
self.session.mount("https://", adapter)
|
29 |
+
self.session.mount("http://", adapter)
|
30 |
+
|
31 |
+
self.__url = url
|
32 |
+
self.__headers = headers
|
33 |
+
|
34 |
+
def get(self, **request_kwargs):
|
35 |
+
r = self.session.get(url=self.__url, headers=self.__headers, params=request_kwargs, timeout=30)
|
36 |
+
r.raise_for_status()
|
37 |
+
return r.json()
|
38 |
+
|
39 |
+
def post(self, payload: Dict[str, Any]):
|
40 |
+
r = self.session.post(url=self.__url, headers=self.__headers, json=payload, timeout=30)
|
41 |
+
r.raise_for_status()
|
42 |
+
return r.json()
|
tools/common/org_search_component.py
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Tuple, Optional, Any
|
2 |
+
import os
|
3 |
+
|
4 |
+
import gradio as gr
|
5 |
+
|
6 |
+
try:
|
7 |
+
from base.api_base import BaseAPI
|
8 |
+
except ImportError:
|
9 |
+
from .base.api_base import BaseAPI
|
10 |
+
|
11 |
+
|
12 |
+
class OrgSearch(BaseAPI):
|
13 |
+
|
14 |
+
def __init__(self):
|
15 |
+
super().__init__(
|
16 |
+
url=f"{os.getenv('CDS_API_URL')}/v1/organization/search",
|
17 |
+
headers={"x-api-key": os.getenv('CDS_API_KEY')}
|
18 |
+
)
|
19 |
+
|
20 |
+
def __call__(self, name: str, **kwargs):
|
21 |
+
is_valid = False
|
22 |
+
|
23 |
+
payload = {
|
24 |
+
"names": [{
|
25 |
+
"value": name,
|
26 |
+
"type": "main"
|
27 |
+
}],
|
28 |
+
"status": "authorized"
|
29 |
+
}
|
30 |
+
|
31 |
+
if kwargs.get("ein"):
|
32 |
+
ein = kwargs.get("ein")
|
33 |
+
if "-" not in ein:
|
34 |
+
ein = f"{ein[:2]}-{ein[2:]}"
|
35 |
+
payload["ids"] = [{
|
36 |
+
"value": ein,
|
37 |
+
"type": "ein"
|
38 |
+
}]
|
39 |
+
is_valid = True
|
40 |
+
|
41 |
+
if kwargs.get("street") or kwargs.get("city") or kwargs.get("state") or kwargs.get("postal_code"):
|
42 |
+
payload["addresses"] = [{
|
43 |
+
"street1": kwargs.get("street") or "",
|
44 |
+
"city": kwargs.get("city") or "",
|
45 |
+
"state": kwargs.get("state") or "",
|
46 |
+
"postal_code": kwargs.get("postal_code") or ""
|
47 |
+
}]
|
48 |
+
is_valid = True
|
49 |
+
|
50 |
+
if not is_valid:
|
51 |
+
return None
|
52 |
+
|
53 |
+
result = self.post(payload=payload)
|
54 |
+
return result.get("payload", [])
|
55 |
+
|
56 |
+
|
57 |
+
search_org = OrgSearch()
|
58 |
+
|
59 |
+
|
60 |
+
def callback_to_state(event: gr.SelectData, state: gr.State) -> Tuple[List[Any], int]:
|
61 |
+
"""Handles DataFrame `select` events. Updates the internal state for either the recipient or funder based on
|
62 |
+
selection. Also sends along the selected Candid entity ID to the proposal generation tab.
|
63 |
+
|
64 |
+
Parameters
|
65 |
+
----------
|
66 |
+
event : gr.SelectData
|
67 |
+
state : gr.State
|
68 |
+
|
69 |
+
Returns
|
70 |
+
-------
|
71 |
+
Tuple[List[Any], int]
|
72 |
+
(Updated state, Candid entity ID)
|
73 |
+
"""
|
74 |
+
|
75 |
+
row, _ = event.index
|
76 |
+
|
77 |
+
if len(state) == 0:
|
78 |
+
return [], None
|
79 |
+
|
80 |
+
# the state should be a nested list of lists
|
81 |
+
# if the state is a single list with non-list elements then we just want a pass-through
|
82 |
+
if all(isinstance(s, list) for s in state):
|
83 |
+
return state[row], state[row][0]
|
84 |
+
return state, state[0]
|
85 |
+
|
86 |
+
|
87 |
+
def lookup_organization(
|
88 |
+
name: str,
|
89 |
+
ein: Optional[str] = None,
|
90 |
+
# street: Optional[str] = None,
|
91 |
+
city: Optional[str] = None,
|
92 |
+
state: Optional[str] = None,
|
93 |
+
postal: Optional[str] = None,
|
94 |
+
) -> Tuple[List[List[str]], List[List[str]]]:
|
95 |
+
"""Performs a simple search using the CDS organization search API. Results are sent to the DataFrame table and also
|
96 |
+
populate the state for the recipient information.
|
97 |
+
|
98 |
+
Parameters
|
99 |
+
----------
|
100 |
+
name : str
|
101 |
+
Org name
|
102 |
+
ein : Optional[str], optional
|
103 |
+
Org EIN, by default None
|
104 |
+
street : Optional[str], optional
|
105 |
+
Street address, by default None
|
106 |
+
city : Optional[str], optional
|
107 |
+
Address city, by default None
|
108 |
+
state : Optional[str], optional
|
109 |
+
Address state, by default None
|
110 |
+
postal : Optional[str], optional
|
111 |
+
Address postal code, by default None
|
112 |
+
|
113 |
+
Returns
|
114 |
+
-------
|
115 |
+
Tuple[List[List[str]], List[List[str]]]
|
116 |
+
(recip data, recip data)
|
117 |
+
|
118 |
+
Raises
|
119 |
+
------
|
120 |
+
gr.Error
|
121 |
+
Raised if not enough information was entered to run a search
|
122 |
+
gr.Error
|
123 |
+
Raised if no search results were returned
|
124 |
+
"""
|
125 |
+
|
126 |
+
results = search_org(name=name, ein=ein, city=city, state=state, postal=postal)
|
127 |
+
if results is None:
|
128 |
+
raise gr.Error("You must provide a name, and either an EIN or an address.")
|
129 |
+
if not results:
|
130 |
+
raise gr.Error("No organizations could be found. Please refine the search criteria.")
|
131 |
+
|
132 |
+
data = []
|
133 |
+
for applicant_data in results:
|
134 |
+
address = applicant_data.get("addresses", [{}])[0].get("normalized")
|
135 |
+
seal = (applicant_data.get("current_seal", {}) or {}).get("image")
|
136 |
+
|
137 |
+
record = [
|
138 |
+
applicant_data.get('candid_entity_id'),
|
139 |
+
applicant_data.get('main_sort_name'),
|
140 |
+
address
|
141 |
+
]
|
142 |
+
|
143 |
+
if seal:
|
144 |
+
record.append(f"")
|
145 |
+
else:
|
146 |
+
record.append("")
|
147 |
+
|
148 |
+
data.append(record)
|
149 |
+
return data, data
|
150 |
+
|
151 |
+
|
152 |
+
def render(org_id_element: Optional[gr.Blocks] = None) -> Tuple[gr.Blocks, gr.State]:
|
153 |
+
"""Main blocks build and render function.
|
154 |
+
|
155 |
+
Parameters
|
156 |
+
----------
|
157 |
+
org_id_element : Optional[gr.Blocks], optional
|
158 |
+
Callback Gradio element, by default None
|
159 |
+
|
160 |
+
Returns
|
161 |
+
-------
|
162 |
+
Tuple[gr.Blocks, gr.State]
|
163 |
+
(component, selected org state)
|
164 |
+
"""
|
165 |
+
|
166 |
+
with gr.Blocks() as component:
|
167 |
+
org_data = gr.State([])
|
168 |
+
selected_org_data = gr.State([])
|
169 |
+
|
170 |
+
with gr.Row():
|
171 |
+
with gr.Column(scale=2):
|
172 |
+
name = gr.Textbox(label="Name of organization", lines=1)
|
173 |
+
ein = gr.Textbox(label="EIN of organization", lines=1)
|
174 |
+
with gr.Column(scale=3):
|
175 |
+
with gr.Group():
|
176 |
+
with gr.Row():
|
177 |
+
with gr.Column():
|
178 |
+
# street = gr.Textbox(label="Street address", lines=1)
|
179 |
+
city = gr.Textbox(label="City", lines=1)
|
180 |
+
with gr.Column():
|
181 |
+
state = gr.Textbox(label="State/province", lines=1)
|
182 |
+
postal = gr.Textbox(label="Postal code", lines=1)
|
183 |
+
|
184 |
+
search_button = gr.Button("Find organization", variant="primary")
|
185 |
+
org_info = gr.DataFrame(
|
186 |
+
label="Organizations",
|
187 |
+
type="array",
|
188 |
+
headers=["Candid ID", "Name", "Address", "Seal"],
|
189 |
+
col_count=(4, "fixed"),
|
190 |
+
datatype=["number", "str", "str", "markdown"],
|
191 |
+
wrap=True,
|
192 |
+
column_widths=["20%", "30%", "30%", "20%"]
|
193 |
+
)
|
194 |
+
|
195 |
+
if org_id_element is None:
|
196 |
+
org_id_element = gr.Textbox(label="Selected Candid entity ID", lines=1)
|
197 |
+
|
198 |
+
# pylint: disable=no-member
|
199 |
+
search_button.click(
|
200 |
+
fn=lambda name, ein, city, state, postal: lookup_organization(
|
201 |
+
name=name,
|
202 |
+
ein=ein,
|
203 |
+
# street=street,
|
204 |
+
city=city,
|
205 |
+
state=state,
|
206 |
+
postal=postal
|
207 |
+
),
|
208 |
+
# inputs=[name, ein, street, city, state, postal],
|
209 |
+
inputs=[name, ein, city, state, postal],
|
210 |
+
outputs=[org_info, org_data],
|
211 |
+
api_name=False,
|
212 |
+
show_api=False
|
213 |
+
)
|
214 |
+
|
215 |
+
# pylint: disable=no-member
|
216 |
+
org_info.select(
|
217 |
+
fn=callback_to_state,
|
218 |
+
inputs=org_data,
|
219 |
+
outputs=[selected_org_data, org_id_element],
|
220 |
+
api_name=False,
|
221 |
+
show_api=False
|
222 |
+
)
|
223 |
+
return component, selected_org_data
|