brainsqueeze commited on
Commit
0699630
·
verified ·
1 Parent(s): 5d08d86

Add /common sub directory

Browse files
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"![{applicant_data.get('current_seal', {}).get('seal')}]({seal})")
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