guidelinely 1.0.3__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.
- guidelinely/__init__.py +60 -0
- guidelinely/_version.py +34 -0
- guidelinely/auth.py +29 -0
- guidelinely/cache.py +46 -0
- guidelinely/client.py +469 -0
- guidelinely/exceptions.py +82 -0
- guidelinely/models.py +138 -0
- guidelinely-1.0.3.dist-info/METADATA +338 -0
- guidelinely-1.0.3.dist-info/RECORD +12 -0
- guidelinely-1.0.3.dist-info/WHEEL +5 -0
- guidelinely-1.0.3.dist-info/licenses/LICENSE +21 -0
- guidelinely-1.0.3.dist-info/top_level.txt +1 -0
guidelinely/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Guidelinely - Python client for the Guidelinely Environmental Guidelines API.
|
|
2
|
+
|
|
3
|
+
Provides programmatic access to environmental guideline calculations and searches
|
|
4
|
+
for chemical parameters in various media (water, soil, sediment).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from guidelinely.auth import get_api_key
|
|
8
|
+
from guidelinely.client import (
|
|
9
|
+
calculate_batch,
|
|
10
|
+
calculate_guidelines,
|
|
11
|
+
get_stats,
|
|
12
|
+
health_check,
|
|
13
|
+
list_media,
|
|
14
|
+
list_parameters,
|
|
15
|
+
list_sources,
|
|
16
|
+
readiness_check,
|
|
17
|
+
search_parameters,
|
|
18
|
+
)
|
|
19
|
+
from guidelinely.exceptions import (
|
|
20
|
+
GuidelinelyAPIError,
|
|
21
|
+
GuidelinelyError,
|
|
22
|
+
GuidelinelyTimeoutError,
|
|
23
|
+
)
|
|
24
|
+
from guidelinely.models import (
|
|
25
|
+
CalculationResponse,
|
|
26
|
+
GuidelineResponse,
|
|
27
|
+
SourceDocument,
|
|
28
|
+
SourceResponse,
|
|
29
|
+
StatsResponse,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from guidelinely._version import __version__
|
|
34
|
+
except ImportError:
|
|
35
|
+
__version__ = "0.0.0.dev0" # Fallback for editable installs without build
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Client functions
|
|
39
|
+
"health_check",
|
|
40
|
+
"readiness_check",
|
|
41
|
+
"list_parameters",
|
|
42
|
+
"search_parameters",
|
|
43
|
+
"list_media",
|
|
44
|
+
"list_sources",
|
|
45
|
+
"get_stats",
|
|
46
|
+
"calculate_guidelines",
|
|
47
|
+
"calculate_batch",
|
|
48
|
+
# Models
|
|
49
|
+
"GuidelineResponse",
|
|
50
|
+
"CalculationResponse",
|
|
51
|
+
"SourceResponse",
|
|
52
|
+
"SourceDocument",
|
|
53
|
+
"StatsResponse",
|
|
54
|
+
# Exceptions
|
|
55
|
+
"GuidelinelyError",
|
|
56
|
+
"GuidelinelyAPIError",
|
|
57
|
+
"GuidelinelyTimeoutError",
|
|
58
|
+
# Auth
|
|
59
|
+
"get_api_key",
|
|
60
|
+
]
|
guidelinely/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.0.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0, 3)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
guidelinely/auth.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Authentication helpers for the Guidelinely API.
|
|
2
|
+
|
|
3
|
+
The main API endpoints (metadata) do not require authentication.
|
|
4
|
+
Calculation endpoints optionally accept an API key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
__all__ = ["get_api_key"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
|
|
14
|
+
"""Get API key from argument or GUIDELINELY_API_KEY environment variable.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
api_key: Optional API key string. If not provided, will check environment.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
API key string or None if not available.
|
|
21
|
+
"""
|
|
22
|
+
if api_key is not None:
|
|
23
|
+
return api_key
|
|
24
|
+
|
|
25
|
+
env_key = os.getenv("GUIDELINELY_API_KEY")
|
|
26
|
+
if env_key:
|
|
27
|
+
return env_key
|
|
28
|
+
|
|
29
|
+
return None
|
guidelinely/cache.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Client-side caching for Guidelinely API calculation endpoints.
|
|
2
|
+
|
|
3
|
+
Uses diskcache for persistent, SQLite-backed caching that survives between runs.
|
|
4
|
+
|
|
5
|
+
The cache directory can be configured via the GUIDELINELY_CACHE_DIR environment
|
|
6
|
+
variable. If not set, defaults to ~/.guidelinely_cache.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional, cast
|
|
12
|
+
|
|
13
|
+
from diskcache import Cache # type: ignore[import-untyped]
|
|
14
|
+
|
|
15
|
+
__all__ = ["get_cached", "set_cached", "cache", "CACHE_DIR"]
|
|
16
|
+
|
|
17
|
+
# Cache directory: configurable via environment variable, defaults to user's home directory
|
|
18
|
+
_default_cache_dir = Path.home() / ".guidelinely_cache"
|
|
19
|
+
CACHE_DIR = Path(os.getenv("GUIDELINELY_CACHE_DIR", str(_default_cache_dir)))
|
|
20
|
+
cache = Cache(directory=str(CACHE_DIR))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_cached(key_data: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
24
|
+
"""Retrieve cached response for given request data.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
key_data: Cache key (typically dict of request parameters)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Cached response data if found, None otherwise
|
|
31
|
+
"""
|
|
32
|
+
result = cache.get(key_data)
|
|
33
|
+
if result is None:
|
|
34
|
+
return None
|
|
35
|
+
return cast(dict[str, Any], result)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_cached(key_data: dict[str, Any], value: dict[str, Any], ttl: int = 24 * 3600) -> None:
|
|
39
|
+
"""Store response in cache for given request data with TTL.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key_data: Cache key (typically dict of request parameters)
|
|
43
|
+
value: Response data to cache
|
|
44
|
+
ttl: Time to live in seconds (default: 24 hours)
|
|
45
|
+
"""
|
|
46
|
+
cache.set(key_data, value, expire=ttl)
|
guidelinely/client.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""Python client for the Guidelinely Environmental Guidelines API.
|
|
2
|
+
|
|
3
|
+
Provides programmatic access to environmental guideline calculations and searches
|
|
4
|
+
for chemical parameters in various media (water, soil, sediment).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Optional, Union, cast
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from guidelinely.auth import get_api_key
|
|
13
|
+
from guidelinely.cache import get_cached, set_cached
|
|
14
|
+
from guidelinely.exceptions import GuidelinelyAPIError, GuidelinelyTimeoutError
|
|
15
|
+
from guidelinely.models import (
|
|
16
|
+
CalculationResponse,
|
|
17
|
+
SourceResponse,
|
|
18
|
+
StatsResponse,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Module logger for debugging API calls
|
|
22
|
+
logger = logging.getLogger("guidelinely")
|
|
23
|
+
|
|
24
|
+
# API base URL
|
|
25
|
+
GUIDELINELY_API_BASE = "https://guidelines.1681248.com/api/v1"
|
|
26
|
+
|
|
27
|
+
# Default timeout for HTTP requests (in seconds)
|
|
28
|
+
DEFAULT_TIMEOUT = 30.0
|
|
29
|
+
|
|
30
|
+
# Package version for User-Agent header (duplicated from __init__.py to avoid circular import)
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
# User-Agent header for API request identification
|
|
34
|
+
USER_AGENT = f"guidelinely-python/{__version__}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _handle_error(response: httpx.Response) -> None:
|
|
38
|
+
"""Extract error message from API response and raise appropriate exception.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
response: HTTP response object with non-2xx status code.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
GuidelinelyAPIError: Always raised with extracted message and status code.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
error_body = response.json()
|
|
48
|
+
message = error_body.get("detail") or error_body.get("message") or "API request failed"
|
|
49
|
+
except Exception:
|
|
50
|
+
message = "API request failed"
|
|
51
|
+
raise GuidelinelyAPIError(message, response.status_code)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def health_check() -> dict[str, Any]:
|
|
55
|
+
"""Check if the API service is running.
|
|
56
|
+
|
|
57
|
+
Lightweight health check that returns 200 OK if the service is running.
|
|
58
|
+
Does not check dependencies.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary with health status.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
65
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
status = health_check()
|
|
69
|
+
print(status)
|
|
70
|
+
"""
|
|
71
|
+
logger.debug("Performing health check")
|
|
72
|
+
try:
|
|
73
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
74
|
+
response = client.get(f"{GUIDELINELY_API_BASE}/health")
|
|
75
|
+
logger.debug(f"Health check response: {response.status_code}")
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
_handle_error(response)
|
|
78
|
+
return cast(dict[str, Any], response.json())
|
|
79
|
+
except httpx.TimeoutException as e:
|
|
80
|
+
logger.warning(f"Health check timed out: {e}")
|
|
81
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def readiness_check() -> dict[str, Any]:
|
|
85
|
+
"""Check if the API is ready to handle requests.
|
|
86
|
+
|
|
87
|
+
Readiness check that verifies the service can handle requests
|
|
88
|
+
(database is accessible).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary with readiness status.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
95
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
status = readiness_check()
|
|
99
|
+
print(status)
|
|
100
|
+
"""
|
|
101
|
+
logger.debug("Performing readiness check")
|
|
102
|
+
try:
|
|
103
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
104
|
+
response = client.get(f"{GUIDELINELY_API_BASE}/ready")
|
|
105
|
+
logger.debug(f"Readiness check response: {response.status_code}")
|
|
106
|
+
if response.status_code != 200:
|
|
107
|
+
_handle_error(response)
|
|
108
|
+
return cast(dict[str, Any], response.json())
|
|
109
|
+
except httpx.TimeoutException as e:
|
|
110
|
+
logger.warning(f"Readiness check timed out: {e}")
|
|
111
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def list_parameters() -> list[str]:
|
|
115
|
+
"""List all available chemical parameters.
|
|
116
|
+
|
|
117
|
+
Get complete list of all available chemical parameters in the database.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of parameter names.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
124
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
params = list_parameters()
|
|
128
|
+
print(params[:5]) # ['Aluminum', 'Ammonia', 'Arsenic', 'Cadmium', 'Copper']
|
|
129
|
+
"""
|
|
130
|
+
logger.debug("Listing all parameters")
|
|
131
|
+
try:
|
|
132
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
133
|
+
response = client.get(f"{GUIDELINELY_API_BASE}/parameters")
|
|
134
|
+
logger.debug(f"List parameters response: {response.status_code}")
|
|
135
|
+
if response.status_code != 200:
|
|
136
|
+
_handle_error(response)
|
|
137
|
+
return cast(list[str], response.json())
|
|
138
|
+
except httpx.TimeoutException as e:
|
|
139
|
+
logger.warning(f"List parameters timed out: {e}")
|
|
140
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def search_parameters(q: str = "", media: Optional[list[str]] = None) -> list[str]:
|
|
144
|
+
"""Search for chemical parameters.
|
|
145
|
+
|
|
146
|
+
Search for chemical parameters using case-insensitive substring matching.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
q: Search query string. Empty string returns all parameters.
|
|
150
|
+
media: Optional list of media types to filter by (e.g., ["surface_water", "soil"]).
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of matching parameter names.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
157
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
# Find all ammonia-related parameters
|
|
161
|
+
ammonia = search_parameters("ammon")
|
|
162
|
+
print(ammonia)
|
|
163
|
+
|
|
164
|
+
# Find copper in surface water
|
|
165
|
+
copper_sw = search_parameters("copper", media=["surface_water"])
|
|
166
|
+
"""
|
|
167
|
+
logger.debug(f"Searching parameters with q={q!r}, media={media!r}")
|
|
168
|
+
try:
|
|
169
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
170
|
+
params: dict[str, Any] = {"q": q}
|
|
171
|
+
if media:
|
|
172
|
+
params["media"] = media
|
|
173
|
+
|
|
174
|
+
response = client.get(
|
|
175
|
+
f"{GUIDELINELY_API_BASE}/parameters/search",
|
|
176
|
+
params=params,
|
|
177
|
+
)
|
|
178
|
+
logger.debug(f"Search parameters response: {response.status_code}")
|
|
179
|
+
if response.status_code != 200:
|
|
180
|
+
_handle_error(response)
|
|
181
|
+
return cast(list[str], response.json())
|
|
182
|
+
except httpx.TimeoutException as e:
|
|
183
|
+
logger.warning(f"Search parameters timed out: {e}")
|
|
184
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def list_media() -> dict[str, str]:
|
|
188
|
+
"""List all environmental media types.
|
|
189
|
+
|
|
190
|
+
Get list of all available environmental media types (water, soil, sediment, etc.).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dictionary mapping enum names to display names.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
197
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
media = list_media()
|
|
201
|
+
print(media)
|
|
202
|
+
"""
|
|
203
|
+
logger.debug("Listing all media types")
|
|
204
|
+
try:
|
|
205
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
206
|
+
response = client.get(f"{GUIDELINELY_API_BASE}/media")
|
|
207
|
+
logger.debug(f"List media response: {response.status_code}")
|
|
208
|
+
if response.status_code != 200:
|
|
209
|
+
_handle_error(response)
|
|
210
|
+
return cast(dict[str, str], response.json())
|
|
211
|
+
except httpx.TimeoutException as e:
|
|
212
|
+
logger.warning(f"List media timed out: {e}")
|
|
213
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def list_sources() -> list[SourceResponse]:
|
|
217
|
+
"""List all guideline sources and documents.
|
|
218
|
+
|
|
219
|
+
Get list of all guideline sources and their associated documents.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of SourceResponse objects with nested document information.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
226
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
sources = list_sources()
|
|
230
|
+
print(sources[0].name) # e.g., 'CCME'
|
|
231
|
+
print(sources[0].documents[0].title)
|
|
232
|
+
"""
|
|
233
|
+
logger.debug("Listing all sources")
|
|
234
|
+
try:
|
|
235
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
236
|
+
response = client.get(f"{GUIDELINELY_API_BASE}/sources")
|
|
237
|
+
logger.debug(f"List sources response: {response.status_code}")
|
|
238
|
+
if response.status_code != 200:
|
|
239
|
+
_handle_error(response)
|
|
240
|
+
return [SourceResponse(**source) for source in response.json()]
|
|
241
|
+
except httpx.TimeoutException as e:
|
|
242
|
+
logger.warning(f"List sources timed out: {e}")
|
|
243
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_stats() -> StatsResponse:
|
|
247
|
+
"""Get database statistics.
|
|
248
|
+
|
|
249
|
+
Get statistics about the guideline database (counts of sources, documents, etc.).
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
StatsResponse with total_parameters, total_guidelines, total_sources, total_documents.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
256
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
stats = get_stats()
|
|
260
|
+
print(f"Total parameters: {stats.total_parameters}")
|
|
261
|
+
print(f"Total guidelines: {stats.total_guidelines}")
|
|
262
|
+
"""
|
|
263
|
+
logger.debug("Getting database statistics")
|
|
264
|
+
try:
|
|
265
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
266
|
+
response = client.get(f"{GUIDELINELY_API_BASE}/stats")
|
|
267
|
+
logger.debug(f"Get stats response: {response.status_code}")
|
|
268
|
+
if response.status_code != 200:
|
|
269
|
+
_handle_error(response)
|
|
270
|
+
return StatsResponse(**response.json())
|
|
271
|
+
except httpx.TimeoutException as e:
|
|
272
|
+
logger.warning(f"Get stats timed out: {e}")
|
|
273
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def calculate_guidelines(
|
|
277
|
+
parameter: str,
|
|
278
|
+
media: str,
|
|
279
|
+
context: Optional[dict[str, str]] = None,
|
|
280
|
+
target_unit: Optional[str] = None,
|
|
281
|
+
api_key: Optional[str] = None,
|
|
282
|
+
) -> CalculationResponse:
|
|
283
|
+
"""Calculate guidelines for a parameter.
|
|
284
|
+
|
|
285
|
+
Calculate guideline values for a specific parameter in a given media type
|
|
286
|
+
with environmental context.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
parameter: Chemical parameter name (e.g., "Aluminum", "Copper").
|
|
290
|
+
media: Media type (e.g., "surface_water", "soil", "sediment").
|
|
291
|
+
context: Dictionary of environmental parameters as strings with units.
|
|
292
|
+
For water: pH ("7.0 1"), hardness ("100 mg/L"), temperature ("20 °C"),
|
|
293
|
+
chloride ("50 mg/L")
|
|
294
|
+
For soil: pH ("6.5 1"), organic_matter ("3.5 %"),
|
|
295
|
+
cation_exchange_capacity ("15 meq/100g")
|
|
296
|
+
target_unit: Optional unit to convert result to (e.g., "mg/L", "μg/L").
|
|
297
|
+
api_key: Optional API key. If None, will use GUIDELINELY_API_KEY environment variable.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
CalculationResponse with results, context, and total_count.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
304
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> import os
|
|
308
|
+
>>> os.environ["GUIDELINELY_API_KEY"] = "your_key"
|
|
309
|
+
>>>
|
|
310
|
+
>>> result = calculate_guidelines(
|
|
311
|
+
... parameter="Aluminum",
|
|
312
|
+
... media="surface_water",
|
|
313
|
+
... context={"pH": "7.0 1", "hardness": "100 mg/L"}
|
|
314
|
+
... )
|
|
315
|
+
>>>
|
|
316
|
+
>>> print(f"Total: {result.total_count}")
|
|
317
|
+
>>> for guideline in result.results:
|
|
318
|
+
... print(f"{guideline.parameter}: {guideline.value} ({guideline.source})")
|
|
319
|
+
"""
|
|
320
|
+
key = get_api_key(api_key)
|
|
321
|
+
logger.debug(f"Calculating guidelines for {parameter} in {media}")
|
|
322
|
+
|
|
323
|
+
# Prepare cache key (excludes api_key for security - keys should not be stored on disk)
|
|
324
|
+
cache_key = {
|
|
325
|
+
"endpoint": "calculate",
|
|
326
|
+
"parameter": parameter,
|
|
327
|
+
"media": media,
|
|
328
|
+
"context": context,
|
|
329
|
+
"target_unit": target_unit,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Check cache first
|
|
333
|
+
cached_response = get_cached(cache_key)
|
|
334
|
+
if cached_response:
|
|
335
|
+
logger.debug("Returning cached response")
|
|
336
|
+
return CalculationResponse(**cached_response)
|
|
337
|
+
|
|
338
|
+
body: dict[str, Any] = {
|
|
339
|
+
"parameter": parameter,
|
|
340
|
+
"media": media,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if context:
|
|
344
|
+
body["context"] = context
|
|
345
|
+
if target_unit:
|
|
346
|
+
body["target_unit"] = target_unit
|
|
347
|
+
|
|
348
|
+
headers = {"User-Agent": USER_AGENT}
|
|
349
|
+
if key:
|
|
350
|
+
headers["X-API-KEY"] = key
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
354
|
+
response = client.post(
|
|
355
|
+
f"{GUIDELINELY_API_BASE}/calculate",
|
|
356
|
+
json=body,
|
|
357
|
+
headers=headers,
|
|
358
|
+
)
|
|
359
|
+
logger.debug(f"Calculate response: {response.status_code}")
|
|
360
|
+
if response.status_code != 200:
|
|
361
|
+
_handle_error(response)
|
|
362
|
+
|
|
363
|
+
data = response.json()
|
|
364
|
+
# Cache the response
|
|
365
|
+
set_cached(cache_key, data)
|
|
366
|
+
return CalculationResponse(**data)
|
|
367
|
+
except httpx.TimeoutException as e:
|
|
368
|
+
logger.warning(f"Calculate guidelines timed out: {e}")
|
|
369
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def calculate_batch(
|
|
373
|
+
parameters: list[Union[str, dict[str, Any]]],
|
|
374
|
+
media: str,
|
|
375
|
+
context: Optional[dict[str, str]] = None,
|
|
376
|
+
api_key: Optional[str] = None,
|
|
377
|
+
) -> CalculationResponse:
|
|
378
|
+
"""Batch calculate guidelines for multiple parameters.
|
|
379
|
+
|
|
380
|
+
Calculate guideline values for multiple parameters in a given media type
|
|
381
|
+
with shared environmental context. More efficient than multiple individual calls.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
parameters: List of parameter names (strings), or list mixing strings and dicts
|
|
385
|
+
with 'name' and 'target_unit' fields. Maximum 50 parameters.
|
|
386
|
+
media: Media type (e.g., "surface_water", "soil").
|
|
387
|
+
context: Dictionary of environmental parameters as strings with units.
|
|
388
|
+
api_key: Optional API key. If None, will use GUIDELINELY_API_KEY environment variable.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
CalculationResponse with results, context, and total_count.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
ValueError: If more than 50 parameters provided.
|
|
395
|
+
GuidelinelyAPIError: If the API returns an error response.
|
|
396
|
+
GuidelinelyTimeoutError: If the request times out.
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
>>> import os
|
|
400
|
+
>>> os.environ["GUIDELINELY_API_KEY"] = "your_key"
|
|
401
|
+
>>>
|
|
402
|
+
>>> # Calculate multiple metals in surface water
|
|
403
|
+
>>> results = calculate_batch(
|
|
404
|
+
... parameters=["Aluminum", "Copper", "Lead"],
|
|
405
|
+
... media="surface_water",
|
|
406
|
+
... context={"pH": "7.0 1", "hardness": "100 mg/L"}
|
|
407
|
+
... )
|
|
408
|
+
>>>
|
|
409
|
+
>>> # With per-parameter unit conversion
|
|
410
|
+
>>> results = calculate_batch(
|
|
411
|
+
... parameters=[
|
|
412
|
+
... "Aluminum",
|
|
413
|
+
... {"name": "Copper", "target_unit": "μg/L"},
|
|
414
|
+
... {"name": "Lead", "target_unit": "mg/L"}
|
|
415
|
+
... ],
|
|
416
|
+
... media="surface_water",
|
|
417
|
+
... context={"pH": "7.5 1", "hardness": "150 mg/L", "temperature": "15 °C"}
|
|
418
|
+
... )
|
|
419
|
+
"""
|
|
420
|
+
if len(parameters) > 50:
|
|
421
|
+
raise ValueError("Maximum 50 parameters per batch request")
|
|
422
|
+
|
|
423
|
+
key = get_api_key(api_key)
|
|
424
|
+
logger.debug(f"Batch calculating {len(parameters)} parameters in {media}")
|
|
425
|
+
|
|
426
|
+
# Prepare cache key (excludes api_key for security - keys should not be stored on disk)
|
|
427
|
+
cache_key = {
|
|
428
|
+
"endpoint": "calculate/batch",
|
|
429
|
+
"parameters": parameters,
|
|
430
|
+
"media": media,
|
|
431
|
+
"context": context,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# Check cache first
|
|
435
|
+
cached_response = get_cached(cache_key)
|
|
436
|
+
if cached_response:
|
|
437
|
+
logger.debug("Returning cached response")
|
|
438
|
+
return CalculationResponse(**cached_response)
|
|
439
|
+
|
|
440
|
+
body: dict[str, Any] = {
|
|
441
|
+
"parameters": parameters,
|
|
442
|
+
"media": media,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if context:
|
|
446
|
+
body["context"] = context
|
|
447
|
+
|
|
448
|
+
headers = {"User-Agent": USER_AGENT}
|
|
449
|
+
if key:
|
|
450
|
+
headers["X-API-KEY"] = key
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
with httpx.Client(timeout=DEFAULT_TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
|
|
454
|
+
response = client.post(
|
|
455
|
+
f"{GUIDELINELY_API_BASE}/calculate/batch",
|
|
456
|
+
json=body,
|
|
457
|
+
headers=headers,
|
|
458
|
+
)
|
|
459
|
+
logger.debug(f"Batch calculate response: {response.status_code}")
|
|
460
|
+
if response.status_code != 200:
|
|
461
|
+
_handle_error(response)
|
|
462
|
+
|
|
463
|
+
data = response.json()
|
|
464
|
+
# Cache the response
|
|
465
|
+
set_cached(cache_key, data)
|
|
466
|
+
return CalculationResponse(**data)
|
|
467
|
+
except httpx.TimeoutException as e:
|
|
468
|
+
logger.warning(f"Batch calculate timed out: {e}")
|
|
469
|
+
raise GuidelinelyTimeoutError(f"Request timed out: {e}") from e
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Custom exceptions for the Guidelinely API client.
|
|
2
|
+
|
|
3
|
+
Provides structured error handling for API communication failures,
|
|
4
|
+
allowing callers to catch specific exception types and access
|
|
5
|
+
error details like HTTP status codes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ["GuidelinelyError", "GuidelinelyAPIError", "GuidelinelyTimeoutError"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GuidelinelyError(Exception):
|
|
12
|
+
"""Base exception for all Guidelinely client errors.
|
|
13
|
+
|
|
14
|
+
All exceptions raised by the guidelinely package inherit from this class,
|
|
15
|
+
making it easy to catch all library-specific errors.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
try:
|
|
19
|
+
result = calculate_guidelines(...)
|
|
20
|
+
except GuidelinelyError as e:
|
|
21
|
+
print(f"Guidelinely error: {e}")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GuidelinelyAPIError(GuidelinelyError):
|
|
28
|
+
"""Raised when the API returns an error response.
|
|
29
|
+
|
|
30
|
+
Provides access to both the error message and HTTP status code
|
|
31
|
+
for detailed error handling.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
message: Human-readable error description from the API.
|
|
35
|
+
status_code: HTTP status code returned by the API.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
try:
|
|
39
|
+
result = calculate_guidelines(...)
|
|
40
|
+
except GuidelinelyAPIError as e:
|
|
41
|
+
if e.status_code == 404:
|
|
42
|
+
print("Parameter not found")
|
|
43
|
+
elif e.status_code == 401:
|
|
44
|
+
print("Invalid API key")
|
|
45
|
+
else:
|
|
46
|
+
print(f"API error {e.status_code}: {e.message}")
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, status_code: int) -> None:
|
|
50
|
+
"""Initialize the API error.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: Error message from the API response.
|
|
54
|
+
status_code: HTTP status code from the response.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.message = message
|
|
58
|
+
self.status_code = status_code
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
"""Return formatted error string with status code."""
|
|
62
|
+
return f"[{self.status_code}] {self.message}"
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
"""Return detailed representation for debugging."""
|
|
66
|
+
return f"GuidelinelyAPIError(message={self.message!r}, status_code={self.status_code})"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GuidelinelyTimeoutError(GuidelinelyError):
|
|
70
|
+
"""Raised when an API request times out.
|
|
71
|
+
|
|
72
|
+
This exception wraps httpx.TimeoutException to provide a consistent
|
|
73
|
+
exception hierarchy for the library.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
try:
|
|
77
|
+
result = calculate_guidelines(...)
|
|
78
|
+
except GuidelinelyTimeoutError:
|
|
79
|
+
print("Request timed out, please try again")
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
pass
|
guidelinely/models.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Pydantic models for Guidelinely API requests and responses."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, model_validator
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
# Response models
|
|
9
|
+
"GuidelineResponse",
|
|
10
|
+
"CalculationResponse",
|
|
11
|
+
"SourceResponse",
|
|
12
|
+
"SourceDocument",
|
|
13
|
+
"StatsResponse",
|
|
14
|
+
"MediaResponse",
|
|
15
|
+
# Request models
|
|
16
|
+
"CalculateRequest",
|
|
17
|
+
"BatchCalculateRequest",
|
|
18
|
+
"ParameterWithUnit",
|
|
19
|
+
"SearchParametersRequest",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GuidelineResponse(BaseModel):
|
|
24
|
+
"""Single guideline result with calculated or static value.
|
|
25
|
+
|
|
26
|
+
Represents a guideline value in PostgreSQL unitrange format (e.g., '[10 μg/L,100 μg/L]').
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
id: int
|
|
30
|
+
parameter: str
|
|
31
|
+
parameter_specification: str
|
|
32
|
+
media: str
|
|
33
|
+
value: str # PostgreSQL unitrange format: '[10 μg/L,100 μg/L]', '(,87.0 μg/L]', '[5.0 mg/L,)'
|
|
34
|
+
lower: Optional[float] = None # Parsed lower bound or None if unbounded
|
|
35
|
+
upper: Optional[float] = None # Parsed upper bound or None if unbounded
|
|
36
|
+
unit: str
|
|
37
|
+
is_calculated: bool
|
|
38
|
+
source: str
|
|
39
|
+
basis: Optional[str] = None
|
|
40
|
+
receptor: str
|
|
41
|
+
exposure_duration: str
|
|
42
|
+
guideline_type: Optional[str] = None
|
|
43
|
+
notes: Optional[str] = None
|
|
44
|
+
reference_id: Optional[int] = None
|
|
45
|
+
document_id: Optional[int] = None
|
|
46
|
+
created_at: Optional[str] = None
|
|
47
|
+
updated_at: Optional[str] = None
|
|
48
|
+
document_abbreviation: str
|
|
49
|
+
source_abbreviation: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CalculationResponse(BaseModel):
|
|
53
|
+
"""Response from calculation endpoints (/calculate and /calculate/batch)."""
|
|
54
|
+
|
|
55
|
+
results: list[GuidelineResponse]
|
|
56
|
+
context: dict[str, Any] # Environmental context used for calculation
|
|
57
|
+
total_count: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CalculateRequest(BaseModel):
|
|
61
|
+
"""Request body for single parameter calculation."""
|
|
62
|
+
|
|
63
|
+
parameter: str
|
|
64
|
+
media: str
|
|
65
|
+
context: Optional[dict[str, str]] = None # Environmental parameters as strings with units
|
|
66
|
+
target_unit: Optional[str] = None # Optional unit conversion
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ParameterWithUnit(BaseModel):
|
|
70
|
+
"""Parameter specification with optional target unit for batch requests."""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
target_unit: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BatchCalculateRequest(BaseModel):
|
|
77
|
+
"""Request body for batch parameter calculation."""
|
|
78
|
+
|
|
79
|
+
parameters: list[Union[str, ParameterWithUnit]] # Mix of strings and objects
|
|
80
|
+
media: str
|
|
81
|
+
context: Optional[dict[str, str]] = None
|
|
82
|
+
|
|
83
|
+
@model_validator(mode="after")
|
|
84
|
+
def validate_parameter_count(self) -> "BatchCalculateRequest":
|
|
85
|
+
"""Ensure batch doesn't exceed 50 parameters.
|
|
86
|
+
|
|
87
|
+
This validation runs automatically when the model is instantiated,
|
|
88
|
+
raising ValueError if more than 50 parameters are provided.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The validated model instance.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If parameters list contains more than 50 items.
|
|
95
|
+
"""
|
|
96
|
+
if len(self.parameters) > 50:
|
|
97
|
+
raise ValueError("Maximum 50 parameters per batch request")
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SearchParametersRequest(BaseModel):
|
|
102
|
+
"""Request body for parameter search (optional media filter)."""
|
|
103
|
+
|
|
104
|
+
media: Optional[list[str]] = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class MediaResponse(BaseModel):
|
|
108
|
+
"""Response from /media endpoint mapping enum names to display names."""
|
|
109
|
+
|
|
110
|
+
# Dynamic keys, so use dict directly in practice
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SourceDocument(BaseModel):
|
|
115
|
+
"""Nested document information within a source."""
|
|
116
|
+
|
|
117
|
+
id: int
|
|
118
|
+
title: str
|
|
119
|
+
url: Optional[str] = None
|
|
120
|
+
year: Optional[int] = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SourceResponse(BaseModel):
|
|
124
|
+
"""Guideline source with nested documents."""
|
|
125
|
+
|
|
126
|
+
id: int
|
|
127
|
+
name: str
|
|
128
|
+
abbreviation: str
|
|
129
|
+
documents: list[SourceDocument]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class StatsResponse(BaseModel):
|
|
133
|
+
"""Database statistics response."""
|
|
134
|
+
|
|
135
|
+
total_parameters: int
|
|
136
|
+
total_guidelines: int
|
|
137
|
+
total_sources: int
|
|
138
|
+
total_documents: int
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: guidelinely
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Python client for the Guidelinely Environmental Guidelines API
|
|
5
|
+
Author-email: Michael Davison <michael.davison@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mpdavison/guidelinely-python
|
|
8
|
+
Project-URL: Documentation, https://guidelines.1681248.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/mpdavison/guidelinely-python
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/mpdavison/guidelinely-python/issues
|
|
11
|
+
Keywords: environmental,guidelines,api,water quality,soil,sediment
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: diskcache>=5.6.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
|
|
31
|
+
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Guidelinely Python Client
|
|
39
|
+
|
|
40
|
+
[](https://www.python.org/downloads/)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
43
|
+
Python client library for the [Guidelinely API](https://guidelines.1681248.com/docs) - an environmental guideline calculation and search API.
|
|
44
|
+
|
|
45
|
+
Calculate context-dependent environmental guideline values for chemical parameters (Aluminum, Copper, Lead, etc.) in various media (water, soil, sediment) based on environmental conditions (pH, hardness, temperature, etc.).
|
|
46
|
+
|
|
47
|
+
This Python client mirrors the functionality of the [R client](https://github.com/mpdavison/envguidelines).
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
- ✅ **Metadata Queries**: List parameters, search, explore media types and sources
|
|
53
|
+
- ✅ **Single Calculations**: Calculate guidelines for individual parameters
|
|
54
|
+
- ✅ **Batch Calculations**: Efficiently calculate multiple parameters (up to 50)
|
|
55
|
+
- ✅ **Context-Aware**: Support for pH, hardness, temperature, and other environmental factors
|
|
56
|
+
- ✅ **Unit Conversion**: Optional target unit specification
|
|
57
|
+
- ✅ **Type Safety**: Full Pydantic model support for request/response validation
|
|
58
|
+
- ✅ **Comprehensive Tests**: Mock-based test suite with responses library
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Clone the repository
|
|
64
|
+
git clone https://github.com/mpdavison/guidelinely-python.git
|
|
65
|
+
cd guidelinely-python
|
|
66
|
+
|
|
67
|
+
# Install in development mode
|
|
68
|
+
pip install -e .
|
|
69
|
+
|
|
70
|
+
# Or install with dev dependencies
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from guidelinely import calculate_guidelines
|
|
78
|
+
|
|
79
|
+
# Calculate aluminum guidelines in surface water
|
|
80
|
+
result = calculate_guidelines(
|
|
81
|
+
parameter="Aluminum",
|
|
82
|
+
media="surface_water",
|
|
83
|
+
context={
|
|
84
|
+
"pH": "7.0 1", # pH 7.0 (dimensionless)
|
|
85
|
+
"hardness": "100 mg/L" # Water hardness as CaCO3
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(f"Found {result.total_count} guidelines")
|
|
90
|
+
for guideline in result.results:
|
|
91
|
+
print(f"{guideline.parameter}: {guideline.value} ({guideline.source})")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Key
|
|
95
|
+
|
|
96
|
+
Calculation endpoints optionally accept an API key. Set it as an environment variable:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export GUIDELINELY_API_KEY="your_api_key_here"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Metadata endpoints (list parameters, search, etc.) work without authentication.
|
|
103
|
+
|
|
104
|
+
## Usage Examples
|
|
105
|
+
|
|
106
|
+
### Basic Metadata Queries
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from guidelinely import (
|
|
110
|
+
health_check,
|
|
111
|
+
list_parameters,
|
|
112
|
+
search_parameters,
|
|
113
|
+
list_media,
|
|
114
|
+
list_sources,
|
|
115
|
+
get_stats,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Check API health
|
|
119
|
+
status = health_check()
|
|
120
|
+
|
|
121
|
+
# List all chemical parameters
|
|
122
|
+
params = list_parameters()
|
|
123
|
+
print(f"Available parameters: {len(params)}")
|
|
124
|
+
|
|
125
|
+
# Search for ammonia-related parameters
|
|
126
|
+
ammonia = search_parameters("ammon")
|
|
127
|
+
print(ammonia) # ['Ammonia', 'Ammonium', ...]
|
|
128
|
+
|
|
129
|
+
# Get available media types
|
|
130
|
+
media = list_media()
|
|
131
|
+
# {'surface_water': 'Surface Water', 'soil': 'Soil', ...}
|
|
132
|
+
|
|
133
|
+
# View guideline sources
|
|
134
|
+
sources = list_sources()
|
|
135
|
+
|
|
136
|
+
# Database statistics
|
|
137
|
+
stats = get_stats()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Single Parameter Calculation
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from guidelinely import calculate_guidelines
|
|
144
|
+
import os
|
|
145
|
+
|
|
146
|
+
os.environ["GUIDELINELY_API_KEY"] = "your_key"
|
|
147
|
+
|
|
148
|
+
result = calculate_guidelines(
|
|
149
|
+
parameter="Copper",
|
|
150
|
+
media="surface_water",
|
|
151
|
+
context={
|
|
152
|
+
"pH": "7.5 1",
|
|
153
|
+
"hardness": "150 mg/L",
|
|
154
|
+
"temperature": "15 °C",
|
|
155
|
+
"chloride": "75 mg/L"
|
|
156
|
+
},
|
|
157
|
+
target_unit="μg/L" # Optional unit conversion
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Filter results
|
|
161
|
+
chronic_aquatic = [
|
|
162
|
+
g for g in result.results
|
|
163
|
+
if g.receptor == "Aquatic Life" and g.exposure_duration == "chronic"
|
|
164
|
+
]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Batch Calculations
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from guidelinely import calculate_batch
|
|
171
|
+
|
|
172
|
+
# Calculate multiple parameters at once (more efficient)
|
|
173
|
+
result = calculate_batch(
|
|
174
|
+
parameters=["Aluminum", "Copper", "Lead", "Zinc", "Cadmium"],
|
|
175
|
+
media="surface_water",
|
|
176
|
+
context={
|
|
177
|
+
"pH": "7.5 1",
|
|
178
|
+
"hardness": "150 mg/L",
|
|
179
|
+
"temperature": "15 °C"
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
print(f"Total: {result.total_count} guidelines")
|
|
184
|
+
|
|
185
|
+
# With per-parameter unit conversion
|
|
186
|
+
result = calculate_batch(
|
|
187
|
+
parameters=[
|
|
188
|
+
"Aluminum",
|
|
189
|
+
{"name": "Copper", "target_unit": "μg/L"},
|
|
190
|
+
{"name": "Lead", "target_unit": "mg/L"}
|
|
191
|
+
],
|
|
192
|
+
media="surface_water",
|
|
193
|
+
context={"pH": "7.0 1", "hardness": "100 mg/L"}
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Soil Calculations
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
result = calculate_guidelines(
|
|
201
|
+
parameter="Lead",
|
|
202
|
+
media="soil",
|
|
203
|
+
context={
|
|
204
|
+
"pH": "6.5 1",
|
|
205
|
+
"organic_matter": "3.5 %",
|
|
206
|
+
"cation_exchange_capacity": "15 meq/100g"
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Environmental Context Parameters
|
|
212
|
+
|
|
213
|
+
All context parameters **must be strings with units** (Pint format):
|
|
214
|
+
|
|
215
|
+
### Water (surface_water, groundwater)
|
|
216
|
+
```python
|
|
217
|
+
context = {
|
|
218
|
+
"pH": "7.0 1", # Dimensionless - use "1" as unit
|
|
219
|
+
"hardness": "100 mg/L", # mg/L as CaCO3
|
|
220
|
+
"temperature": "20 °C",
|
|
221
|
+
"chloride": "50 mg/L"
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Soil
|
|
226
|
+
```python
|
|
227
|
+
context = {
|
|
228
|
+
"pH": "6.5 1",
|
|
229
|
+
"organic_matter": "3.5 %",
|
|
230
|
+
"cation_exchange_capacity": "15 meq/100g"
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Sediment
|
|
235
|
+
```python
|
|
236
|
+
context = {
|
|
237
|
+
"pH": "7.0 1",
|
|
238
|
+
"organic_matter": "2.5 %",
|
|
239
|
+
"grain_size": "0.5 mm"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Data Models
|
|
244
|
+
|
|
245
|
+
The library uses Pydantic for type-safe data handling:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from guidelinely.models import GuidelineResponse, CalculationResponse
|
|
249
|
+
|
|
250
|
+
# All API responses are strongly typed
|
|
251
|
+
result: CalculationResponse = calculate_guidelines(...)
|
|
252
|
+
|
|
253
|
+
# Access typed fields
|
|
254
|
+
for guideline in result.results:
|
|
255
|
+
guideline.parameter # str
|
|
256
|
+
guideline.value # str (PostgreSQL unitrange format)
|
|
257
|
+
guideline.lower # Optional[float]
|
|
258
|
+
guideline.upper # Optional[float]
|
|
259
|
+
guideline.unit # str
|
|
260
|
+
guideline.is_calculated # bool
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Guideline Value Format
|
|
264
|
+
|
|
265
|
+
Guidelines use PostgreSQL `unitrange` format:
|
|
266
|
+
- `[10 μg/L,100 μg/L]` - Range from 10 to 100 μg/L
|
|
267
|
+
- `(,87.0 μg/L]` - Upper limit only (≤87.0 μg/L)
|
|
268
|
+
- `[5.0 mg/L,)` - Lower limit only (≥5.0 mg/L)
|
|
269
|
+
|
|
270
|
+
Parsed into `lower`, `upper`, and `unit` fields.
|
|
271
|
+
|
|
272
|
+
## Development
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Install development dependencies
|
|
276
|
+
pip install -e ".[dev]"
|
|
277
|
+
|
|
278
|
+
# Run tests
|
|
279
|
+
pytest
|
|
280
|
+
|
|
281
|
+
# Run tests with coverage
|
|
282
|
+
pytest --cov=guidelinely --cov-report=html
|
|
283
|
+
|
|
284
|
+
# Format code
|
|
285
|
+
black guidelinely/ tests/ examples/
|
|
286
|
+
|
|
287
|
+
# Type checking
|
|
288
|
+
mypy guidelinely/
|
|
289
|
+
|
|
290
|
+
# Linting
|
|
291
|
+
ruff check guidelinely/ tests/
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## API Reference
|
|
295
|
+
|
|
296
|
+
### Client Functions
|
|
297
|
+
|
|
298
|
+
- `health_check()` - Service health check
|
|
299
|
+
- `readiness_check()` - Database readiness check
|
|
300
|
+
- `list_parameters()` - List all chemical parameters
|
|
301
|
+
- `search_parameters(q, media)` - Search parameters
|
|
302
|
+
- `list_media()` - List media types
|
|
303
|
+
- `list_sources()` - List guideline sources
|
|
304
|
+
- `get_stats()` - Database statistics
|
|
305
|
+
- `calculate_guidelines(parameter, media, context, target_unit, api_key)` - Calculate single parameter
|
|
306
|
+
- `calculate_batch(parameters, media, context, api_key)` - Batch calculate (max 50 parameters)
|
|
307
|
+
|
|
308
|
+
### Models
|
|
309
|
+
|
|
310
|
+
- `GuidelineResponse` - Single guideline result
|
|
311
|
+
- `CalculationResponse` - Calculation endpoint response
|
|
312
|
+
- `CalculateRequest` - Single calculation request body
|
|
313
|
+
- `BatchCalculateRequest` - Batch calculation request body
|
|
314
|
+
|
|
315
|
+
## Examples
|
|
316
|
+
|
|
317
|
+
See the `examples/` directory for complete working examples:
|
|
318
|
+
|
|
319
|
+
1. `01_basic_metadata.py` - Basic metadata queries
|
|
320
|
+
2. `02_calculate_single_parameter.py` - Single parameter calculations
|
|
321
|
+
3. `03_batch_calculations.py` - Batch calculations
|
|
322
|
+
4. `04_soil_calculations.py` - Soil-specific calculations
|
|
323
|
+
5. `05_advanced_workflow.py` - Advanced filtering and analysis
|
|
324
|
+
|
|
325
|
+
## Resources
|
|
326
|
+
|
|
327
|
+
- **API Documentation**: https://guidelines.1681248.com/docs
|
|
328
|
+
- **OpenAPI Spec**: https://guidelines.1681248.com/openapi.json
|
|
329
|
+
- **R Client**: https://github.com/mpdavison/envguidelines
|
|
330
|
+
- **Issue Tracker**: https://github.com/mpdavison/guidelinely-python/issues
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
335
|
+
|
|
336
|
+
## Contributing
|
|
337
|
+
|
|
338
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
guidelinely/__init__.py,sha256=IjusaUQolmVzQl5pajYzz7WTJrxpha2leVwM2NbLqgU,1395
|
|
2
|
+
guidelinely/_version.py,sha256=l8k828IdTfzXAlmx4oT8GsiIf2eeMAlFDALjoYk-jrU,704
|
|
3
|
+
guidelinely/auth.py,sha256=hGrmMGQ9Ye9555yOJglrHOGzS3clU7z9QTg-gRYNOVo,704
|
|
4
|
+
guidelinely/cache.py,sha256=V9bXeyT9_FR1ijRFXu_7R7rcZwmtBjE8TJx-ZWxniDM,1537
|
|
5
|
+
guidelinely/client.py,sha256=nBXvIKHD0xYskbtjun5w7wYO5vHQK8gkxtJZx2XclkE,16808
|
|
6
|
+
guidelinely/exceptions.py,sha256=d_HaNw7nqzyv-w2hRNMj2YXx2qJoOj_NkHft7fZED8k,2508
|
|
7
|
+
guidelinely/models.py,sha256=Z8q1fptPa3SNuSRtwtQV8CIiIEz8cmTKRi99m_4w4A4,3817
|
|
8
|
+
guidelinely-1.0.3.dist-info/licenses/LICENSE,sha256=0zH1WeuuqGNzsoBXBmOfaW_hqqVAUefUNoWYm88CdwM,1081
|
|
9
|
+
guidelinely-1.0.3.dist-info/METADATA,sha256=QB6DnTRf5OGQrCdngZn0aAS3yHKE1tvCHo-5nhIXErY,9397
|
|
10
|
+
guidelinely-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
guidelinely-1.0.3.dist-info/top_level.txt,sha256=zrK-evc6akaqjoD3VabEnbc1xmz_RSDDKofUnQ7COlU,12
|
|
12
|
+
guidelinely-1.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Guidelinely Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
guidelinely
|