sgu-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sgu_client/__init__.py +22 -0
- sgu_client/client/__init__.py +5 -0
- sgu_client/client/base.py +200 -0
- sgu_client/client/levels/__init__.py +30 -0
- sgu_client/client/levels/modeled.py +275 -0
- sgu_client/client/levels/observed.py +431 -0
- sgu_client/config.py +32 -0
- sgu_client/exceptions.py +39 -0
- sgu_client/models/__init__.py +36 -0
- sgu_client/models/base.py +41 -0
- sgu_client/models/modeled.py +216 -0
- sgu_client/models/observed.py +281 -0
- sgu_client/models/shared.py +46 -0
- sgu_client/py.typed +0 -0
- sgu_client/sgu_client.py +51 -0
- sgu_client/utils/__init__.py +5 -0
- sgu_client/utils/pandas_helpers.py +68 -0
- sgu_client-0.1.0.dist-info/METADATA +277 -0
- sgu_client-0.1.0.dist-info/RECORD +20 -0
- sgu_client-0.1.0.dist-info/WHEEL +4 -0
sgu_client/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""SGU Client - Python library for accessing SGU groundwater data."""
|
|
2
|
+
|
|
3
|
+
from .config import SGUConfig
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
SGUAPIError,
|
|
6
|
+
SGUClientError,
|
|
7
|
+
SGUConnectionError,
|
|
8
|
+
SGUTimeoutError,
|
|
9
|
+
SGUValidationError,
|
|
10
|
+
)
|
|
11
|
+
from .sgu_client import SGUClient
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SGUAPIError",
|
|
16
|
+
"SGUClient",
|
|
17
|
+
"SGUClientError",
|
|
18
|
+
"SGUConfig",
|
|
19
|
+
"SGUConnectionError",
|
|
20
|
+
"SGUTimeoutError",
|
|
21
|
+
"SGUValidationError",
|
|
22
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Base HTTP client for SGU API."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from requests.adapters import HTTPAdapter
|
|
9
|
+
from urllib3.util.retry import Retry
|
|
10
|
+
|
|
11
|
+
from sgu_client.config import SGUConfig
|
|
12
|
+
from sgu_client.exceptions import SGUAPIError, SGUConnectionError, SGUTimeoutError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseClient:
|
|
18
|
+
"""Base HTTP client with common functionality for SGU API."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: SGUConfig | None = None):
|
|
21
|
+
"""Initialize the base client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: Configuration object. If None, uses default config.
|
|
25
|
+
"""
|
|
26
|
+
self.config = config or SGUConfig()
|
|
27
|
+
self._session = self._create_session()
|
|
28
|
+
|
|
29
|
+
if self.config.debug:
|
|
30
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
31
|
+
|
|
32
|
+
def _create_session(self) -> requests.Session:
|
|
33
|
+
"""Create and configure a requests session."""
|
|
34
|
+
session = requests.Session()
|
|
35
|
+
|
|
36
|
+
# Set headers
|
|
37
|
+
session.headers.update(
|
|
38
|
+
{
|
|
39
|
+
"User-Agent": self.config.user_agent,
|
|
40
|
+
"Accept": "application/json",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Configure retry strategy
|
|
45
|
+
retry_strategy = Retry(
|
|
46
|
+
total=self.config.max_retries,
|
|
47
|
+
backoff_factor=1,
|
|
48
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
52
|
+
session.mount("http://", adapter)
|
|
53
|
+
session.mount("https://", adapter)
|
|
54
|
+
|
|
55
|
+
return session
|
|
56
|
+
|
|
57
|
+
def _make_request(
|
|
58
|
+
self,
|
|
59
|
+
method: str,
|
|
60
|
+
endpoint: str,
|
|
61
|
+
params: dict[str, Any] | None = None,
|
|
62
|
+
data: dict[str, Any] | None = None,
|
|
63
|
+
base_url: str | None = None,
|
|
64
|
+
**kwargs,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""Make an HTTP request to the SGU API.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
method: HTTP method (GET, POST, etc.)
|
|
70
|
+
endpoint: API endpoint path
|
|
71
|
+
params: Query parameters
|
|
72
|
+
data: Request body data
|
|
73
|
+
base_url: Optional override for base URL (for different API endpoints)
|
|
74
|
+
**kwargs: Additional arguments passed to requests
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
JSON response data
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
SGUConnectionError: If connection fails
|
|
81
|
+
SGUTimeoutError: If request times out
|
|
82
|
+
SGUAPIError: If API returns an error
|
|
83
|
+
"""
|
|
84
|
+
url = urljoin(base_url or self.config.base_url, endpoint)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
if self.config.debug:
|
|
88
|
+
logger.debug(f"Making {method} request to {url}")
|
|
89
|
+
if params:
|
|
90
|
+
logger.debug(f"Query params: {params}")
|
|
91
|
+
if data:
|
|
92
|
+
logger.debug(f"Request data: {data}")
|
|
93
|
+
|
|
94
|
+
response = self._session.request(
|
|
95
|
+
method=method,
|
|
96
|
+
url=url,
|
|
97
|
+
params=params,
|
|
98
|
+
json=data,
|
|
99
|
+
timeout=self.config.timeout,
|
|
100
|
+
**kwargs,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if self.config.debug:
|
|
104
|
+
logger.debug(f"Response status: {response.status_code}")
|
|
105
|
+
|
|
106
|
+
# Check for HTTP errors
|
|
107
|
+
if not response.ok:
|
|
108
|
+
try:
|
|
109
|
+
error_data = response.json()
|
|
110
|
+
except ValueError:
|
|
111
|
+
error_data = {"error": response.text}
|
|
112
|
+
|
|
113
|
+
raise SGUAPIError(
|
|
114
|
+
f"API request failed with status {response.status_code}",
|
|
115
|
+
status_code=response.status_code,
|
|
116
|
+
response_data=error_data,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return response.json()
|
|
120
|
+
|
|
121
|
+
except requests.exceptions.ReadTimeout as e:
|
|
122
|
+
raise SGUTimeoutError(f"Read timeout after {self.config.timeout}s") from e
|
|
123
|
+
except requests.exceptions.ConnectTimeout as e:
|
|
124
|
+
raise SGUTimeoutError(
|
|
125
|
+
f"Connection timeout after {self.config.timeout}s"
|
|
126
|
+
) from e
|
|
127
|
+
except requests.exceptions.ConnectionError as e:
|
|
128
|
+
# Check if this is a timeout wrapped in MaxRetryError
|
|
129
|
+
if "Read timed out" in str(e) or "ReadTimeoutError" in str(e):
|
|
130
|
+
raise SGUTimeoutError(
|
|
131
|
+
f"Read timeout after {self.config.timeout}s"
|
|
132
|
+
) from e
|
|
133
|
+
raise SGUConnectionError(f"Connection failed: {e}") from e
|
|
134
|
+
except requests.exceptions.RequestException as e:
|
|
135
|
+
raise SGUAPIError(f"Request failed: {e}") from e
|
|
136
|
+
|
|
137
|
+
def get(
|
|
138
|
+
self,
|
|
139
|
+
endpoint: str,
|
|
140
|
+
params: dict[str, Any] | None = None,
|
|
141
|
+
base_url: str | None = None,
|
|
142
|
+
**kwargs,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Make a GET request.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
endpoint: API endpoint path
|
|
148
|
+
params: Query parameters
|
|
149
|
+
base_url: Optional override for base URL
|
|
150
|
+
**kwargs: Additional arguments passed to requests
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
JSON response data
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
SGUConnectionError: If connection fails
|
|
157
|
+
SGUTimeoutError: If request times out
|
|
158
|
+
SGUAPIError: If API returns an error
|
|
159
|
+
"""
|
|
160
|
+
return self._make_request(
|
|
161
|
+
"GET", endpoint, params=params, base_url=base_url, **kwargs
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def post(
|
|
165
|
+
self, endpoint: str, data: dict[str, Any] | None = None, **kwargs
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Make a POST request.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
endpoint: API endpoint path
|
|
171
|
+
data: Request body data
|
|
172
|
+
**kwargs: Additional arguments passed to requests
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
JSON response data
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
SGUConnectionError: If connection fails
|
|
179
|
+
SGUTimeoutError: If request times out
|
|
180
|
+
SGUAPIError: If API returns an error
|
|
181
|
+
"""
|
|
182
|
+
return self._make_request("POST", endpoint, data=data, **kwargs)
|
|
183
|
+
|
|
184
|
+
def __enter__(self):
|
|
185
|
+
"""Context manager entry.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The client instance for use in with statements
|
|
189
|
+
"""
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
193
|
+
"""Context manager exit - clean up resources.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
exc_type: Exception type (if any)
|
|
197
|
+
exc_val: Exception value (if any)
|
|
198
|
+
exc_tb: Exception traceback (if any)
|
|
199
|
+
"""
|
|
200
|
+
self._session.close()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Levels client module."""
|
|
2
|
+
|
|
3
|
+
from .modeled import ModeledGroundwaterLevelClient
|
|
4
|
+
from .observed import ObservedGroundwaterLevelClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LevelsClient:
|
|
8
|
+
"""Client for accessing groundwater level data.
|
|
9
|
+
|
|
10
|
+
Provides access to both observed and modeled groundwater levels
|
|
11
|
+
through a hierarchical API structure.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> client = SGUClient()
|
|
15
|
+
>>> stations = client.levels.observed.get_stations()
|
|
16
|
+
>>> areas = client.levels.modeled.get_areas()
|
|
17
|
+
>>> levels = client.levels.modeled.get_levels()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, base_client):
|
|
21
|
+
"""Initialize the levels client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_client: The base HTTP client instance.
|
|
25
|
+
"""
|
|
26
|
+
self.observed = ObservedGroundwaterLevelClient(base_client)
|
|
27
|
+
self.modeled = ModeledGroundwaterLevelClient(base_client)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["LevelsClient"]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Modeled groundwater level client endpoints."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sgu_client.client.base import BaseClient
|
|
7
|
+
from sgu_client.models.modeled import (
|
|
8
|
+
ModeledArea,
|
|
9
|
+
ModeledAreaCollection,
|
|
10
|
+
ModeledGroundwaterLevel,
|
|
11
|
+
ModeledGroundwaterLevelCollection,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ModeledGroundwaterLevelClient:
|
|
16
|
+
"""Client for modeled groundwater level-related SGU API endpoints."""
|
|
17
|
+
|
|
18
|
+
BASE_PATH = "collections"
|
|
19
|
+
MODELED_BASE_URL = "https://api.sgu.se/oppnadata/grundvattennivaer-sgu-hype-omraden/ogc/features/v1/"
|
|
20
|
+
|
|
21
|
+
def __init__(self, base_client: BaseClient):
|
|
22
|
+
"""Initialize modeled groundwater level client.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
base_client: Base HTTP client instance
|
|
26
|
+
"""
|
|
27
|
+
self._client = base_client
|
|
28
|
+
|
|
29
|
+
def get_areas(
|
|
30
|
+
self,
|
|
31
|
+
bbox: list[float] | None = None,
|
|
32
|
+
limit: int | None = None,
|
|
33
|
+
filter_expr: str | None = None,
|
|
34
|
+
sortby: list[str] | None = None,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> ModeledAreaCollection:
|
|
37
|
+
"""Get modeled groundwater areas.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
bbox: Bounding box as [min_lon, min_lat, max_lon, max_lat]
|
|
41
|
+
limit: Maximum number of features to return (1-50000, default 50000)
|
|
42
|
+
filter_expr: CQL filter expression
|
|
43
|
+
sortby: List of sort expressions (e.g., ['+omrade_id'])
|
|
44
|
+
**kwargs: Additional query parameters
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Typed collection of modeled groundwater areas
|
|
48
|
+
"""
|
|
49
|
+
endpoint = f"{self.BASE_PATH}/omraden/items"
|
|
50
|
+
params = self._build_query_params(
|
|
51
|
+
bbox=bbox,
|
|
52
|
+
limit=limit,
|
|
53
|
+
filter=filter_expr,
|
|
54
|
+
sortby=sortby,
|
|
55
|
+
**kwargs,
|
|
56
|
+
)
|
|
57
|
+
response = self._make_request(endpoint, params)
|
|
58
|
+
return ModeledAreaCollection(**response)
|
|
59
|
+
|
|
60
|
+
def get_area(self, area_id: str) -> ModeledArea:
|
|
61
|
+
"""Get a specific modeled groundwater area by ID.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
area_id: Area identifier
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Typed modeled groundwater area
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If area not found or multiple areas returned
|
|
71
|
+
"""
|
|
72
|
+
endpoint = f"{self.BASE_PATH}/omraden/items/{area_id}"
|
|
73
|
+
response = self._make_request(endpoint, {})
|
|
74
|
+
|
|
75
|
+
# SGU API returns a FeatureCollection even for single items
|
|
76
|
+
collection = ModeledAreaCollection(**response)
|
|
77
|
+
if not collection.features:
|
|
78
|
+
raise ValueError(f"Area {area_id} not found")
|
|
79
|
+
if len(collection.features) > 1:
|
|
80
|
+
raise ValueError(f"Multiple areas returned for ID {area_id}")
|
|
81
|
+
|
|
82
|
+
return collection.features[0]
|
|
83
|
+
|
|
84
|
+
def get_levels(
|
|
85
|
+
self,
|
|
86
|
+
bbox: list[float] | None = None,
|
|
87
|
+
datetime: str | None = None,
|
|
88
|
+
limit: int | None = None,
|
|
89
|
+
filter_expr: str | None = None,
|
|
90
|
+
sortby: list[str] | None = None,
|
|
91
|
+
**kwargs: Any,
|
|
92
|
+
) -> ModeledGroundwaterLevelCollection:
|
|
93
|
+
"""Get modeled groundwater levels.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
bbox: Bounding box as [min_lon, min_lat, max_lon, max_lat]
|
|
97
|
+
datetime: Date/time filter (RFC 3339 format or interval)
|
|
98
|
+
limit: Maximum number of features to return (1-50000, default 50000)
|
|
99
|
+
filter_expr: CQL filter expression
|
|
100
|
+
sortby: List of sort expressions (e.g., ['+datum', '-omrade_id'])
|
|
101
|
+
**kwargs: Additional query parameters
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Typed collection of modeled groundwater levels
|
|
105
|
+
"""
|
|
106
|
+
endpoint = f"{self.BASE_PATH}/grundvattennivaer-tidigare/items"
|
|
107
|
+
params = self._build_query_params(
|
|
108
|
+
bbox=bbox,
|
|
109
|
+
datetime=datetime,
|
|
110
|
+
limit=limit,
|
|
111
|
+
filter=filter_expr,
|
|
112
|
+
sortby=sortby,
|
|
113
|
+
**kwargs,
|
|
114
|
+
)
|
|
115
|
+
response = self._make_request(endpoint, params)
|
|
116
|
+
return ModeledGroundwaterLevelCollection(**response)
|
|
117
|
+
|
|
118
|
+
def get_level(self, level_id: str) -> ModeledGroundwaterLevel:
|
|
119
|
+
"""Get a specific modeled groundwater level by ID.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
level_id: Level identifier
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Typed modeled groundwater level
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If level not found or multiple levels returned
|
|
129
|
+
"""
|
|
130
|
+
endpoint = f"{self.BASE_PATH}/grundvattennivaer-tidigare/items/{level_id}"
|
|
131
|
+
response = self._make_request(endpoint, {})
|
|
132
|
+
|
|
133
|
+
# SGU API returns a FeatureCollection even for single items
|
|
134
|
+
collection = ModeledGroundwaterLevelCollection(**response)
|
|
135
|
+
if not collection.features:
|
|
136
|
+
raise ValueError(f"Level {level_id} not found")
|
|
137
|
+
if len(collection.features) > 1:
|
|
138
|
+
raise ValueError(f"Multiple levels returned for ID {level_id}")
|
|
139
|
+
|
|
140
|
+
return collection.features[0]
|
|
141
|
+
|
|
142
|
+
def get_levels_by_area(
|
|
143
|
+
self, area_id: int, **kwargs: Any
|
|
144
|
+
) -> ModeledGroundwaterLevelCollection:
|
|
145
|
+
"""Get modeled groundwater levels for a specific area.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
area_id: Area ID to filter by
|
|
149
|
+
**kwargs: Additional query parameters (limit, datetime, bbox, etc.)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Typed collection of modeled groundwater levels for the specified area
|
|
153
|
+
"""
|
|
154
|
+
filter_expr = f"omrade_id = {area_id}"
|
|
155
|
+
return self.get_levels(filter_expr=filter_expr, **kwargs)
|
|
156
|
+
|
|
157
|
+
def get_levels_by_areas(
|
|
158
|
+
self, area_ids: list[int], **kwargs: Any
|
|
159
|
+
) -> ModeledGroundwaterLevelCollection:
|
|
160
|
+
"""Get modeled groundwater levels for multiple areas.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
area_ids: List of area IDs to filter by
|
|
164
|
+
**kwargs: Additional query parameters (limit, datetime, bbox, etc.)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Typed collection of modeled groundwater levels for the specified areas
|
|
168
|
+
"""
|
|
169
|
+
if not area_ids:
|
|
170
|
+
raise ValueError("At least one area ID must be provided")
|
|
171
|
+
|
|
172
|
+
# Create CQL filter expression for multiple area IDs
|
|
173
|
+
if len(area_ids) == 1:
|
|
174
|
+
filter_expr = f"omrade_id = {area_ids[0]}"
|
|
175
|
+
else:
|
|
176
|
+
area_ids_str = ", ".join(str(area_id) for area_id in area_ids)
|
|
177
|
+
filter_expr = f"omrade_id IN ({area_ids_str})"
|
|
178
|
+
|
|
179
|
+
return self.get_levels(filter_expr=filter_expr, **kwargs)
|
|
180
|
+
|
|
181
|
+
def get_levels_by_coords(
|
|
182
|
+
self,
|
|
183
|
+
lat: float,
|
|
184
|
+
lon: float,
|
|
185
|
+
buffer: float = 0.01,
|
|
186
|
+
**kwargs: Any,
|
|
187
|
+
) -> ModeledGroundwaterLevelCollection:
|
|
188
|
+
"""Get modeled groundwater levels for a specific coordinate.
|
|
189
|
+
|
|
190
|
+
This convenience function first finds relevant areas containing or near
|
|
191
|
+
the specified coordinates, then retrieves modeled levels for those areas.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
lat: Latitude coordinate (WGS84)
|
|
195
|
+
lon: Longitude coordinate (WGS84)
|
|
196
|
+
buffer: Buffer distance in degrees around the point (default 0.01 ≈ 1km)
|
|
197
|
+
**kwargs: Additional query parameters (limit, datetime, etc.)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Typed collection of modeled groundwater levels for areas near the coordinates
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ValueError: If no areas found near the specified coordinates
|
|
204
|
+
"""
|
|
205
|
+
logger = logging.getLogger(__name__)
|
|
206
|
+
|
|
207
|
+
# Create bounding box around the point
|
|
208
|
+
bbox = [
|
|
209
|
+
lon - buffer, # min_lon
|
|
210
|
+
lat - buffer, # min_lat
|
|
211
|
+
lon + buffer, # max_lon
|
|
212
|
+
lat + buffer, # max_lat
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
# Find areas within the bounding box
|
|
216
|
+
areas = self.get_areas(bbox=bbox, limit=1000)
|
|
217
|
+
|
|
218
|
+
if not areas.features:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"No modeled groundwater areas found near coordinates "
|
|
221
|
+
f"({lat}, {lon}) within {buffer}° buffer"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Extract area IDs
|
|
225
|
+
area_ids = [int(area.properties.omrade_id) for area in areas.features]
|
|
226
|
+
|
|
227
|
+
# Log warning if multiple areas found (near boundary)
|
|
228
|
+
if len(area_ids) > 1:
|
|
229
|
+
logger.warning(
|
|
230
|
+
f"Found {len(area_ids)} modeled areas near coordinates "
|
|
231
|
+
f"({lat}, {lon}). This suggests the point is close to an area boundary. "
|
|
232
|
+
f"Area IDs: {area_ids}. All areas will be included in the results."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Get levels for all found areas
|
|
236
|
+
return self.get_levels_by_areas(area_ids, **kwargs)
|
|
237
|
+
|
|
238
|
+
def _build_query_params(self, **params: Any) -> dict[str, Any]:
|
|
239
|
+
"""Build query parameters for API requests.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
**params: Raw parameter values
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Cleaned dictionary of query parameters
|
|
246
|
+
"""
|
|
247
|
+
query_params = {}
|
|
248
|
+
|
|
249
|
+
for key, value in params.items():
|
|
250
|
+
if value is None:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
if key == "bbox" and isinstance(value, list):
|
|
254
|
+
query_params[key] = ",".join(map(str, value))
|
|
255
|
+
elif key == "sortby" and isinstance(value, list):
|
|
256
|
+
query_params[key] = ",".join(value)
|
|
257
|
+
else:
|
|
258
|
+
query_params[key] = value
|
|
259
|
+
|
|
260
|
+
return query_params
|
|
261
|
+
|
|
262
|
+
def _make_request(self, endpoint: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
+
"""Make HTTP request to SGU API.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
endpoint: API endpoint path
|
|
267
|
+
params: Query parameters
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
JSON response data
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
Various HTTP and API exceptions via base client
|
|
274
|
+
"""
|
|
275
|
+
return self._client.get(endpoint, params=params, base_url=self.MODELED_BASE_URL)
|