leavoigt commited on
Commit
5cd41bc
·
1 Parent(s): e93edee

add whisp api call

Browse files
Files changed (5) hide show
  1. app.py +4 -3
  2. requirements.txt +2 -1
  3. utils/api_client.py +186 -0
  4. utils/main_processor.py +29 -0
  5. utils/utils.py +0 -11
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import gradio as gr
2
  import json
3
- from utils.utils import process_geojson
4
 
5
 
6
  # Interface
@@ -11,9 +11,10 @@ with gr.Blocks() as ui:
11
  file_input = gr.File(file_types=[".geojson"])
12
  submit_btn = gr.Button("Submit")
13
  with gr.Column():
14
- output = gr.JSON()
15
 
16
- submit_btn.click(process_geojson, file_input, output)
 
17
 
18
  # Launch app
19
  if __name__ == "__main__":
 
1
  import gradio as gr
2
  import json
3
+ from utils.main_processor import get_statistics
4
 
5
 
6
  # Interface
 
11
  file_input = gr.File(file_types=[".geojson"])
12
  submit_btn = gr.Button("Submit")
13
  with gr.Column():
14
+ output = gr.Text()
15
 
16
+ submit_btn.click(get_statistics,
17
+ file_input, output)
18
 
19
  # Launch app
20
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
  gradio==4.44.1
2
- pydantic==2.10.6
 
 
1
  gradio==4.44.1
2
+ pydantic==2.10.6
3
+ returns>=0.26.0
utils/api_client.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # imports
2
+ from collections.abc import Callable
3
+ from requests import post
4
+ from requests.exceptions import HTTPError
5
+ from returns.context import ReaderIOResult
6
+ from returns.io import IOResult
7
+ from returns.methods import unwrap_or_failure
8
+ from returns.result import Failure, Result, Success
9
+ from returns.unsafe import unsafe_perform_io
10
+ from typing import NamedTuple, Protocol, TypeAlias, Union
11
+
12
+ # Setup
13
+ HEADER_TEMPLATE: dict = {
14
+ "accept": "application/json",
15
+ "X-API-KEY": None,
16
+ "Content-Type": "application/json"
17
+ }
18
+
19
+ # Define URL
20
+ API_URL: str = "https://whisp.openforis.org/api/submit/geojson"
21
+
22
+ # type aliases
23
+ AnyJSON: TypeAlias = Union[
24
+ str,
25
+ int,
26
+ float,
27
+ bool,
28
+ None,
29
+ dict[str, 'AnyJSON'], # Recursive dict key-value pairs
30
+ list['AnyJSON'] # Recursive list of JSON values
31
+ ] # covers all JSON incl. GeoJSON
32
+
33
+ ApiResponse: TypeAlias = dict[str, Union[int, dict, AnyJSON]]
34
+
35
+ # settings interface
36
+ class _Options(Protocol):
37
+ URL: str
38
+ HEADER: dict
39
+
40
+ class _Settings(NamedTuple):
41
+ # implements the _Options interface
42
+ URL: str
43
+ HEADER: dict
44
+
45
+ # internal helper functions
46
+
47
+ def _get_api_response(input: AnyJSON) -> ReaderIOResult[ApiResponse, ApiResponse, _Settings]:
48
+ def _post_call(settings: _Options) -> IOResult[ApiResponse, ApiResponse]:
49
+ try:
50
+ response = post(settings.URL, headers=settings.HEADER, json=input)
51
+ status = response.status_code
52
+ payload = response.json()
53
+ response.raise_for_status()
54
+ return IOResult.from_value({'status': status, 'payload': payload})
55
+ except HTTPError:
56
+ return IOResult.from_failure({'status': status, 'payload': payload})
57
+ except Exception as e:
58
+ return IOResult.from_failure({'status': 499, 'payload': str(e)})
59
+ return ReaderIOResult(_post_call)
60
+
61
+ # whisp request functions
62
+
63
+ def safe_whisp_request(input: AnyJSON, api_key: str) -> Result[AnyJSON, AnyJSON]:
64
+ """
65
+ Safely sends a request to the Whisp API and returns the parsed JSON response or an error.
66
+
67
+ This function wraps the API interaction inside result containers for safe error and side effect handling.
68
+ It prepares the required headers (including the API key), performs the request using
69
+ internal helper functions, and safely returns either the successful response payload
70
+ or an error message encapsulated in a `Result` container.
71
+
72
+ Parameters
73
+ ----------
74
+ input : AnyJSON incl. GeoJSON
75
+ The data payload (JSON-serializable) to send in the API request.
76
+ api_key : str
77
+ The authentication token to be included as the "X-API-KEY" header.
78
+
79
+ Returns
80
+ -------
81
+ Result[AnyJSON, AnyJSON]
82
+ On success: Returns a Success container containing the JSON response from the API.
83
+ On failure: Returns a Failure container containing a dictionary with the error message.
84
+
85
+ Raises
86
+ ------
87
+ Exception
88
+ May raise network, serialization/deserialization, or system-level exceptions
89
+ if low-level I/O or unexpected errors occur outside of safe handling.
90
+
91
+ Examples
92
+ --------
93
+ >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <...>}]}, "secret-api-key-123")
94
+ Success< {'status': 200, 'payload': {"type": "FeatureCollection", "features": [{"type": Feature", "geometry": < ... > }]} >
95
+ >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <invalid geometry or CRS>, < ... >}]}, "wrong-key")
96
+ Failure< {'status': 499, 'payload': {'error': 'Something went wrong'}} >
97
+ """
98
+ _settings = _Settings(API_URL, HEADER_TEMPLATE)
99
+ _settings.HEADER.update({"X-API-KEY":api_key})
100
+ response = _get_api_response(input)(_settings)
101
+ return unsafe_perform_io(response)
102
+
103
+ def raw_whisp_request(input: AnyJSON, api_key: str) -> AnyJSON:
104
+ """
105
+ Sends a request to the Whisp API and returns the result as raw JSON. Raised errors are wrapped in JSON and returned.
106
+
107
+ This function calls the safe_whisp_request wrapper, but instead of returning
108
+ a result container, it unwraps the api call result and returns the raw JSON in case of success and the raised error wrapped in JSON in case of failure.
109
+ Use this function when you need a simpler interface which returns JSON as a result rather than explicit error handling.
110
+
111
+ Parameters
112
+ ----------
113
+ input : AnyJSON incl. GeoJSON
114
+ The JSON-serializable payload to be sent in the API request.
115
+ api_key : str
116
+ The authentication token inserted as the "X-API-KEY" header.
117
+
118
+ Returns
119
+ -------
120
+ AnyJSON
121
+ The JSON-decoded response payload from the API if the request was successful.
122
+ If the request fails, the exception is returned wrapped in JSON.
123
+
124
+ Raises
125
+ ------
126
+ Exception
127
+ Raises the contained error or a generic exception if the API request fails
128
+ or if low-level issues occur (for example, network or deserialization errors).
129
+
130
+ Examples
131
+ --------
132
+ >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": < ... >}]}, "secret-api-key-123")
133
+ {'status': 200, 'payload': {"type": "FeatureCollection", "features": [{"type": Feature", "geometry": < ... > }]}
134
+ >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <invalid geometry or CRS>, <...>}]}, "wrong-key")
135
+ {'status': 499, 'payload': {'error': 'Something went wrong'}}
136
+ """
137
+ return unwrap_or_failure(safe_whisp_request(input, api_key))
138
+
139
+ def whisp_request(input: AnyJSON, api_key: str) -> AnyJSON:
140
+ """
141
+ Sends a request to the Whisp API and extracts a list of feature properties from the response.
142
+
143
+ This function wraps the API request and extracts the 'properties' from each feature
144
+ inside the 'features' key of the response payload. On success, it returns a dictionary/JSON
145
+ containing a list of these properties. On failure, it returns the error message
146
+ from the response, if available.
147
+
148
+ Parameters
149
+ ----------
150
+ input : AnyJSON incl. GeoJSON
151
+ The JSON-serializable payload to send in the API request.
152
+ api_key : str
153
+ The authentication token included as the "X-API-KEY" header.
154
+
155
+ Returns
156
+ -------
157
+ AnyJSON
158
+ On success: Returns a dictionary/JSON with a single key 'properties', mapping to a list
159
+ of properties found in the API response's features.
160
+ On failure: Returns the error message wrapped in a dictionary/JSON as returned by the API.
161
+
162
+ Raises
163
+ ------
164
+ Exception
165
+ May raise generic exceptions in the event of I/O, network, or unexpected errors
166
+ at lower levels (for example, in safe_whisp_request or during result matching).
167
+
168
+ Examples
169
+ --------
170
+ >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <...>}]}, "secret-api-key-123")
171
+ {'properties': [{'plotId': '1', 'external_id': < ... > }
172
+ >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <invalid geometry or CRS>, <...>}]}, "wrong-key")
173
+ {'error': 'Something went wrong'}
174
+ """
175
+ response = safe_whisp_request(input, api_key)
176
+ match response:
177
+ case Success(value):
178
+ return {
179
+ 'properties': \
180
+ [feature.get('properties', {'error': 'Properties not available'}) \
181
+ for feature in value.get('payload', {}).get('data', {}).get(
182
+ 'features', {'error': 'Not any properties available'}
183
+ )]
184
+ }
185
+ case Failure(value):
186
+ return value.get('payload', {'error':'Error message not available'})
utils/main_processor.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import utils.api_client
4
+
5
+ # Import secrets
6
+ whisp_api_key = os.environ.get("WHISP_API_KEY")
7
+
8
+
9
+ # Define main function to fetch geojson, do API call and get statistics
10
+
11
+ def get_statistics(file):
12
+
13
+ # Open the geojson file uploaded
14
+ if file is None:
15
+ return "Geojson file empty"
16
+
17
+ try:
18
+ with open(file, 'r', encoding='utf-8') as f:
19
+ geojson_file = json.load(f)
20
+ except Exception as e:
21
+ return {"error": str(e)}
22
+
23
+ # Do the API call
24
+ whisp_response = utils.api_client.whisp_request(geojson_file, api_key=whisp_api_key)
25
+
26
+ return whisp_response
27
+
28
+
29
+
utils/utils.py DELETED
@@ -1,11 +0,0 @@
1
- import json
2
-
3
- def process_geojson(file):
4
- if file is None:
5
- return {}
6
-
7
- try:
8
- with open(file, 'r', encoding='utf-8') as f:
9
- return json.load(f)
10
- except Exception as e:
11
- return {"error": str(e)}