Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| # imports | |
| from collections.abc import Callable | |
| from requests import post | |
| from requests.exceptions import HTTPError | |
| from returns.context import ReaderIOResult | |
| from returns.io import IOResult | |
| from returns.methods import unwrap_or_failure | |
| from returns.result import Failure, Result, Success | |
| from returns.unsafe import unsafe_perform_io | |
| from typing import NamedTuple, Protocol, TypeAlias, Union | |
| # Define URL | |
| API_URL: str = "https://whisp.openforis.org/api/submit/geojson" | |
| # type aliases | |
| AnyJSON: TypeAlias = Union[ | |
| str, | |
| int, | |
| float, | |
| bool, | |
| None, | |
| dict[str, 'AnyJSON'], # Recursive dict key-value pairs | |
| list['AnyJSON'] # Recursive list of JSON values | |
| ] # covers all JSON incl. GeoJSON | |
| ApiResponse: TypeAlias = dict[str, Union[int, dict, AnyJSON]] | |
| # settings interface | |
| class _Options(Protocol): | |
| URL: str | |
| HEADER: dict | |
| class _Settings(NamedTuple): | |
| # implements the _Options interface | |
| URL: str | |
| HEADER: dict | |
| # internal helper functions | |
| def _get_api_response(input: AnyJSON) -> ReaderIOResult[ApiResponse, ApiResponse, _Settings]: | |
| def _post_call(settings: _Options) -> IOResult[ApiResponse, ApiResponse]: | |
| try: | |
| response = post(settings.URL, headers=settings.HEADER, json=input) | |
| status = response.status_code | |
| payload = response.json() | |
| response.raise_for_status() | |
| return IOResult.from_value({'status': status, 'payload': payload}) | |
| except HTTPError: | |
| return IOResult.from_failure({'status': status, 'payload': payload}) | |
| except Exception as e: | |
| return IOResult.from_failure({'status': 499, 'payload': str(e)}) | |
| return ReaderIOResult(_post_call) | |
| # whisp request functions | |
| def safe_whisp_request(input: AnyJSON, api_key: str) -> Result[AnyJSON, AnyJSON]: | |
| """ | |
| Safely sends a request to the Whisp API and returns the parsed JSON response or an error. | |
| This function wraps the API interaction inside result containers for safe error and side effect handling. | |
| It prepares the required headers (including the API key), performs the request using | |
| internal helper functions, and safely returns either the successful response payload | |
| or an error message encapsulated in a `Result` container. | |
| Parameters | |
| ---------- | |
| input : AnyJSON incl. GeoJSON | |
| The data payload (JSON-serializable) to send in the API request. | |
| api_key : str | |
| The authentication token to be included as the "X-API-KEY" header. | |
| Returns | |
| ------- | |
| Result[AnyJSON, AnyJSON] | |
| On success: Returns a Success container containing the JSON response from the API. | |
| On failure: Returns a Failure container containing a dictionary with the error message. | |
| Raises | |
| ------ | |
| Exception | |
| May raise network, serialization/deserialization, or system-level exceptions | |
| if low-level I/O or unexpected errors occur outside of safe handling. | |
| Examples | |
| -------- | |
| >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <...>}]}, "secret-api-key-123") | |
| Success< {'status': 200, 'payload': {"type": "FeatureCollection", "features": [{"type": Feature", "geometry": < ... > }]} > | |
| >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <invalid geometry or CRS>, < ... >}]}, "wrong-key") | |
| Failure< {'status': 499, 'payload': {'error': 'Something went wrong'}} > | |
| """ | |
| header = { | |
| "accept": "application/json", | |
| "X-API-KEY": api_key, | |
| "Content-Type": "application/json" | |
| } | |
| _settings = _Settings(API_URL, header) | |
| response = _get_api_response(input)(_settings) | |
| return unsafe_perform_io(response) | |
| def raw_whisp_request(input: AnyJSON, api_key: str) -> AnyJSON: | |
| """ | |
| Sends a request to the Whisp API and returns the result as raw JSON. Raised errors are wrapped in JSON and returned. | |
| This function calls the safe_whisp_request wrapper, but instead of returning | |
| 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. | |
| Use this function when you need a simpler interface which returns JSON as a result rather than explicit error handling. | |
| Parameters | |
| ---------- | |
| input : AnyJSON incl. GeoJSON | |
| The JSON-serializable payload to be sent in the API request. | |
| api_key : str | |
| The authentication token inserted as the "X-API-KEY" header. | |
| Returns | |
| ------- | |
| AnyJSON | |
| The JSON-decoded response payload from the API if the request was successful. | |
| If the request fails, the exception is returned wrapped in JSON. | |
| Raises | |
| ------ | |
| Exception | |
| Raises the contained error or a generic exception if the API request fails | |
| or if low-level issues occur (for example, network or deserialization errors). | |
| Examples | |
| -------- | |
| >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": < ... >}]}, "secret-api-key-123") | |
| {'status': 200, 'payload': {"type": "FeatureCollection", "features": [{"type": Feature", "geometry": < ... > }]} | |
| >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <invalid geometry or CRS>, <...>}]}, "wrong-key") | |
| {'status': 499, 'payload': {'error': 'Something went wrong'}} | |
| """ | |
| return unwrap_or_failure(safe_whisp_request(input, api_key)) | |
| def whisp_request(input: AnyJSON, api_key: str) -> AnyJSON: | |
| """ | |
| Sends a request to the Whisp API and extracts a list of feature properties from the response. | |
| This function wraps the API request and extracts the 'properties' from each feature | |
| inside the 'features' key of the response payload. On success, it returns a dictionary/JSON | |
| containing a list of these properties. On failure, it returns the error message | |
| from the response, if available. | |
| Parameters | |
| ---------- | |
| input : AnyJSON incl. GeoJSON | |
| The JSON-serializable payload to send in the API request. | |
| api_key : str | |
| The authentication token included as the "X-API-KEY" header. | |
| Returns | |
| ------- | |
| AnyJSON | |
| On success: Returns a dictionary/JSON with a single key 'properties', mapping to a list | |
| of properties found in the API response's features. | |
| On failure: Returns the error message wrapped in a dictionary/JSON as returned by the API. | |
| Raises | |
| ------ | |
| Exception | |
| May raise generic exceptions in the event of I/O, network, or unexpected errors | |
| at lower levels (for example, in safe_whisp_request or during result matching). | |
| Examples | |
| -------- | |
| >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <...>}]}, "secret-api-key-123") | |
| {'properties': [{'plotId': '1', 'external_id': < ... > } | |
| >>> safe_whisp_request({"type": "FeatureCollection", "features": [{"type": Feature", "geometry": <invalid geometry or CRS>, <...>}]}, "wrong-key") | |
| {'error': 'Something went wrong'} | |
| """ | |
| response = safe_whisp_request(input, api_key) | |
| match response: | |
| case Success(value): | |
| return { | |
| 'properties': \ | |
| [feature.get('properties', {'error': 'Properties not available'}) \ | |
| for feature in value.get('payload', {}).get('data', {}).get( | |
| 'features', {'error': 'Not any properties available'} | |
| )] | |
| } | |
| case Failure(value): | |
| return value.get('payload', {'error':'Error message not available'}) |