muriel 0.2.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.
muriel/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ """Muriel Python SDK - Official SDK for the Muriel® Water Quality API."""
2
+
3
+ from .client import MurielClient
4
+ from .agent import Agent, query
5
+ from .clients import RequirementsClient, SamplesClient, SystemsClient
6
+ from .endpoints.results import get_results
7
+ from .endpoints.facilities import (
8
+ get_state_facilities,
9
+ get_facility_geocoding,
10
+ )
11
+ from .endpoints.sampling_points import (
12
+ get_state_sampling_points,
13
+ get_sampling_point_geocoding,
14
+ )
15
+ from .endpoints.systems import get_systems
16
+ from .endpoints.requirements import get_requirements
17
+ from .endpoints.violations import get_violations
18
+ from .endpoints.contaminants import (
19
+ get_latest,
20
+ get_lead,
21
+ get_pfas,
22
+ get_coliform,
23
+ get_nitrate,
24
+ get_dbps,
25
+ get_arsenic,
26
+ get_manganese,
27
+ get_vocs,
28
+ )
29
+ from .exceptions import (
30
+ MurielAPIError,
31
+ MurielAuthenticationError,
32
+ MurielValidationError,
33
+ MurielNotFoundError,
34
+ MurielRateLimitError,
35
+ MurielServerError,
36
+ MurielCreditsError,
37
+ )
38
+
39
+ __version__ = "0.2.0"
40
+ __all__ = [
41
+ # Client
42
+ "MurielClient",
43
+ "Agent",
44
+ "query",
45
+ "RequirementsClient",
46
+ "SystemsClient",
47
+ "SamplesClient",
48
+ # Functions
49
+ "get_results",
50
+ "get_requirements",
51
+ "get_violations",
52
+ "get_systems",
53
+ "get_state_facilities",
54
+ "get_facility_geocoding",
55
+ "get_state_sampling_points",
56
+ "get_sampling_point_geocoding",
57
+ # Contaminant convenience functions
58
+ "get_latest",
59
+ "get_lead",
60
+ "get_pfas",
61
+ "get_coliform",
62
+ "get_nitrate",
63
+ "get_dbps",
64
+ "get_arsenic",
65
+ "get_manganese",
66
+ "get_vocs",
67
+ # Exceptions
68
+ "MurielAPIError",
69
+ "MurielAuthenticationError",
70
+ "MurielValidationError",
71
+ "MurielNotFoundError",
72
+ "MurielRateLimitError",
73
+ "MurielServerError",
74
+ "MurielCreditsError",
75
+ ]
muriel/agent.py ADDED
@@ -0,0 +1,355 @@
1
+ """
2
+ Agent Mode for natural language water quality queries.
3
+
4
+ Calls POST /api/v1/agent/query on the Muriel API. All LLM reasoning
5
+ happens server-side — users only need a Muriel API key.
6
+ """
7
+
8
+ import re
9
+ from datetime import date, datetime
10
+ from typing import Any, Dict, List, Optional, Sequence, Type, TypeVar, Union
11
+
12
+ import requests
13
+ from pydantic import BaseModel
14
+
15
+ from .config import DEFAULT_TIMEOUT, checked_base_url, get_api_key, get_base_url
16
+ from .exceptions import (
17
+ MurielAPIError,
18
+ MurielAuthenticationError,
19
+ MurielRateLimitError,
20
+ MurielServerError,
21
+ MurielValidationError,
22
+ )
23
+ from .models import AgentQueryResponse
24
+
25
+ T = TypeVar("T", bound=BaseModel)
26
+
27
+ # What `schema=` accepts: a Pydantic model, a list of field names, or a JSON
28
+ # Schema dict. The API only ever sees a JSON Schema dict.
29
+ SchemaArg = Union[Type[BaseModel], Sequence[str], Dict[str, Any]]
30
+
31
+ # Nine characters: a state code (or two digits for EPA-region tribal systems),
32
+ # then seven characters that are nearly always digits.
33
+ _PWSID_RE = re.compile(r"^(?:[A-Z]{2}|\d{2})(?=[A-Z0-9]*\d)[A-Z0-9]{7}$")
34
+
35
+
36
+ def _fields_schema(fields: Sequence[str]) -> Dict[str, Any]:
37
+ """JSON Schema for an object with exactly these fields, all required."""
38
+ names = [str(f).strip() for f in fields]
39
+ if not names or any(not n for n in names):
40
+ raise MurielValidationError("schema= needs at least one non-empty field name.")
41
+ if len(set(names)) != len(names):
42
+ raise MurielValidationError("schema= lists the same field twice.")
43
+ return {
44
+ "type": "object",
45
+ "properties": {n: {} for n in names},
46
+ "required": names,
47
+ "additionalProperties": False,
48
+ }
49
+
50
+
51
+ def _normalise_since(since: Union[str, date, datetime, None]) -> Optional[str]:
52
+ """ISO date string, or None. Accepts a date, a datetime, or 'YYYY-MM-DD'."""
53
+ if since is None:
54
+ return None
55
+ if isinstance(since, datetime):
56
+ return since.date().isoformat()
57
+ if isinstance(since, date):
58
+ return since.isoformat()
59
+ text = str(since).strip()
60
+ try:
61
+ return date.fromisoformat(text[:10]).isoformat()
62
+ except ValueError:
63
+ raise MurielValidationError(
64
+ f"since= must be a date or 'YYYY-MM-DD', got {since!r}."
65
+ ) from None
66
+
67
+
68
+ class Agent:
69
+ """
70
+ Natural language interface to the Muriel water quality database.
71
+
72
+ Sends queries to the Muriel API's server-side agent endpoint.
73
+ Only a Muriel API key is required — no separate LLM key needed.
74
+
75
+ Usage::
76
+
77
+ from muriel import Agent
78
+
79
+ agent = Agent() # reads MURIEL_API_KEY from environment
80
+ response = agent.query("Show me PFAS levels in Illinois")
81
+ print(response.summary)
82
+ print(response.results)
83
+
84
+ Pass a Pydantic model as ``schema`` to receive a typed, validated object::
85
+
86
+ from pydantic import BaseModel
87
+ from muriel import Agent
88
+
89
+ class PFASResult(BaseModel):
90
+ pwsid: str
91
+ analyte: str
92
+ concentration: float
93
+
94
+ agent = Agent()
95
+ result = agent.query("PFAS in IL", schema=PFASResult)
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ api_key: Optional[str] = None,
101
+ base_url: Optional[str] = None,
102
+ timeout: int = DEFAULT_TIMEOUT,
103
+ ):
104
+ """
105
+ Initialise the Agent.
106
+
107
+ Args:
108
+ api_key: Muriel API key. Defaults to the ``MURIEL_API_KEY``
109
+ environment variable.
110
+ base_url: Override the API base URL (default: https://api.murielai.io).
111
+ timeout: HTTP timeout in seconds (default: 30). Agent queries
112
+ can take several seconds — increase if you see timeouts.
113
+ """
114
+ self._api_key = api_key or get_api_key()
115
+ if not self._api_key:
116
+ raise MurielAuthenticationError(
117
+ "API key is required. Pass it as api_key= or set the "
118
+ "MURIEL_API_KEY environment variable."
119
+ )
120
+ self._base_url = checked_base_url(base_url or get_base_url())
121
+ self._timeout = timeout
122
+ self._session = requests.Session()
123
+ self._session.headers.update({
124
+ "X-API-Key": self._api_key,
125
+ "Content-Type": "application/json",
126
+ })
127
+
128
+ # ── Public interface ───────────────────────────────────────────────────────
129
+
130
+ def query(
131
+ self,
132
+ prompt: str,
133
+ *,
134
+ pwsid: Optional[str] = None,
135
+ since: Union[str, date, datetime, None] = None,
136
+ schema: Optional[SchemaArg] = None,
137
+ max_records: int = 1000,
138
+ ) -> Union[AgentQueryResponse, Dict[str, Any], T]:
139
+ """
140
+ Query water quality data using natural language.
141
+
142
+ Args:
143
+ prompt: Plain-English description of the data you want.
144
+ pwsid: Optional Public Water System ID (e.g. ``"IL0316000"``).
145
+ Pins the question to one system, so the agent does
146
+ not have to work out which system a name means.
147
+ since: Optional date (``date``, ``datetime`` or
148
+ ``"YYYY-MM-DD"``). Only results and events on or
149
+ after this date -- what an app polling for new
150
+ activity needs.
151
+ schema: Optional shape for the answer. One of:
152
+
153
+ * a list of field names, e.g.
154
+ ``["event_type", "num_events", "pwsid", "insight"]``
155
+ -- returns a plain ``dict`` with exactly those keys;
156
+ * a Pydantic model -- returns an instance of it;
157
+ * a JSON Schema ``dict`` -- returns a plain ``dict``.
158
+ max_records: Cap on the number of records returned (default 1000,
159
+ max 10000).
160
+
161
+ Returns:
162
+ ``AgentQueryResponse`` with ``results``, ``count``, ``summary``,
163
+ ``execution_log``, and an optional ``warning`` when no schema is
164
+ given. With a schema, the shaped answer: a ``dict`` for a field
165
+ list or JSON Schema, a model instance for a Pydantic model.
166
+
167
+ Example::
168
+
169
+ import muriel
170
+
171
+ muriel.query(
172
+ "Get the latest events for Chicago Water",
173
+ api_key="YOUR_MURIEL_API_KEY",
174
+ since="2026-09-29",
175
+ schema=["event_type", "num_events", "pwsid", "insight"],
176
+ )
177
+
178
+ Raises:
179
+ MurielAuthenticationError: Invalid or missing API key.
180
+ MurielRateLimitError: Credit balance exhausted.
181
+ MurielServerError: Server-side error.
182
+ MurielAPIError: Any other failure.
183
+ """
184
+ if not isinstance(prompt, str) or not prompt.strip():
185
+ raise MurielValidationError("prompt must be a non-empty string.")
186
+
187
+ payload: Dict[str, Any] = {
188
+ "prompt": prompt,
189
+ "max_records": max_records,
190
+ }
191
+
192
+ if pwsid is not None:
193
+ pwsid = str(pwsid).strip().upper()
194
+ if not _PWSID_RE.match(pwsid):
195
+ raise MurielValidationError(
196
+ f"pwsid= should look like 'IL0316000' (state code, then the "
197
+ f"system number), got {pwsid!r}."
198
+ )
199
+ payload["pwsid"] = pwsid
200
+ since_iso = _normalise_since(since)
201
+ if since_iso is not None:
202
+ payload["since"] = since_iso
203
+
204
+ model: Optional[Type[BaseModel]] = None
205
+ fields: Optional[List[str]] = None
206
+ if schema is not None:
207
+ if isinstance(schema, type) and issubclass(schema, BaseModel):
208
+ model = schema
209
+ payload["schema"] = schema.model_json_schema()
210
+ elif isinstance(schema, dict):
211
+ payload["schema"] = schema
212
+ elif isinstance(schema, (list, tuple)):
213
+ payload["schema"] = _fields_schema(schema)
214
+ fields = list(payload["schema"]["required"])
215
+ else:
216
+ raise MurielValidationError(
217
+ "schema= must be a list of field names, a Pydantic model, "
218
+ "or a JSON Schema dict."
219
+ )
220
+
221
+ data = self._post("/api/v1/agent/query", payload)
222
+
223
+ if schema is not None:
224
+ # The API has already shaped the results; take the first shaped
225
+ # object, as the Pydantic path always has.
226
+ results = data.get("results", [])
227
+ first = results[0] if isinstance(results, list) and results else results
228
+ if model is not None:
229
+ return model.model_validate(first)
230
+ first = dict(first or {})
231
+ if fields is not None:
232
+ missing = [f for f in fields if f not in first]
233
+ if missing:
234
+ raise MurielAPIError(
235
+ f"The answer is missing requested field(s): {', '.join(missing)}."
236
+ )
237
+ return {f: first[f] for f in fields}
238
+ return first
239
+
240
+ return AgentQueryResponse(
241
+ results=data.get("results", []),
242
+ count=data.get("count", 0),
243
+ summary=data.get("summary", ""),
244
+ execution_log=data.get("execution_log", []),
245
+ warning=data.get("warning"),
246
+ documents=data.get("documents") or [],
247
+ credits=data.get("credits"),
248
+ )
249
+
250
+ def close(self) -> None:
251
+ """Close the underlying HTTP session."""
252
+ self._session.close()
253
+
254
+ def __enter__(self) -> "Agent":
255
+ return self
256
+
257
+ def __exit__(self, *_: Any) -> None:
258
+ self.close()
259
+
260
+ # ── Internal ───────────────────────────────────────────────────────────────
261
+
262
+ def _post(self, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
263
+ url = f"{self._base_url}{endpoint}"
264
+ try:
265
+ # No redirects: the key travels in X-API-Key, which requests would
266
+ # forward to whatever host a redirect names.
267
+ response = self._session.post(url, json=payload, timeout=self._timeout,
268
+ allow_redirects=False)
269
+ except requests.exceptions.Timeout:
270
+ raise MurielAPIError(
271
+ f"Agent query timed out after {self._timeout}s. "
272
+ "Try increasing timeout= on the Agent constructor."
273
+ )
274
+ except requests.exceptions.ConnectionError:
275
+ raise MurielAPIError(f"Failed to connect to {self._base_url}")
276
+ except requests.exceptions.RequestException as e:
277
+ raise MurielAPIError(f"Request failed: {e}")
278
+
279
+ if 300 <= response.status_code < 400:
280
+ raise MurielAPIError(
281
+ f"Unexpected redirect ({response.status_code}); not followed, so your "
282
+ "API key was not sent elsewhere. Check MURIEL_BASE_URL.",
283
+ status_code=response.status_code,
284
+ )
285
+ if response.status_code == 200:
286
+ return response.json()
287
+ elif response.status_code == 401:
288
+ raise MurielAuthenticationError(
289
+ "Invalid API key.",
290
+ status_code=401,
291
+ response=response.json() if response.content else None,
292
+ )
293
+ elif response.status_code == 400:
294
+ error = response.json() if response.content else {}
295
+ raise MurielValidationError(
296
+ error.get("detail", "Validation error"),
297
+ status_code=400,
298
+ response=error,
299
+ )
300
+ elif response.status_code == 402:
301
+ raise MurielCreditsError(
302
+ detail_of(response, "Out of credits. Email info@murielai.io to add more."),
303
+ status_code=402,
304
+ response=response.json() if response.content else None,
305
+ )
306
+ elif response.status_code == 429:
307
+ raise MurielRateLimitError(
308
+ detail_of(response, "Rate limit exceeded. Wait and retry."),
309
+ status_code=429,
310
+ response=response.json() if response.content else None,
311
+ )
312
+ elif response.status_code >= 500:
313
+ raise MurielServerError(
314
+ detail_of(response, "Server error during agent query."),
315
+ status_code=response.status_code,
316
+ response=response.json() if response.content else None,
317
+ )
318
+ else:
319
+ raise MurielAPIError(
320
+ f"Unexpected status {response.status_code}",
321
+ status_code=response.status_code,
322
+ )
323
+
324
+
325
+ # ── Module-level convenience ───────────────────────────────────────────────────
326
+
327
+ # One Agent per API key, so repeated calls reuse a connection. `None` is the
328
+ # key read from MURIEL_API_KEY.
329
+ _agents: Dict[Optional[str], Agent] = {}
330
+
331
+
332
+ def query(
333
+ prompt: str,
334
+ *,
335
+ api_key: Optional[str] = None,
336
+ pwsid: Optional[str] = None,
337
+ since: Union[str, date, datetime, None] = None,
338
+ schema: Optional[SchemaArg] = None,
339
+ max_records: int = 1000,
340
+ ) -> Union[AgentQueryResponse, Dict[str, Any], BaseModel]:
341
+ """``muriel.query(...)``: ``Agent.query`` without building an Agent.
342
+
343
+ Args:
344
+ api_key: Muriel API key. Defaults to the ``MURIEL_API_KEY``
345
+ environment variable. All other arguments are as for
346
+ ``Agent.query``.
347
+
348
+ Build an ``Agent`` yourself to set a base URL or timeout.
349
+ """
350
+ agent = _agents.get(api_key)
351
+ if agent is None:
352
+ agent = _agents[api_key] = Agent(api_key=api_key)
353
+ return agent.query(
354
+ prompt, pwsid=pwsid, since=since, schema=schema, max_records=max_records
355
+ )
muriel/client.py ADDED
@@ -0,0 +1,164 @@
1
+ """Core client for the Muriel API."""
2
+
3
+ from typing import Any, Dict, Optional
4
+ import requests
5
+
6
+ from .config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, checked_base_url, get_api_key, get_base_url
7
+ from .exceptions import (
8
+ MurielAPIError,
9
+ MurielAuthenticationError,
10
+ MurielValidationError,
11
+ MurielNotFoundError,
12
+ MurielRateLimitError,
13
+ MurielServerError,
14
+ MurielCreditsError,
15
+ detail_of,
16
+ )
17
+
18
+
19
+ class MurielClient:
20
+ """Base client for making requests to the Muriel API."""
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: Optional[str] = None,
25
+ base_url: Optional[str] = None,
26
+ timeout: int = DEFAULT_TIMEOUT,
27
+ ):
28
+ """
29
+ Initialize the Muriel API client.
30
+
31
+ Args:
32
+ api_key: Your Muriel API key. If not provided, will look for MURIEL_API_KEY env var.
33
+ base_url: Base URL for the API (default: MURIEL_BASE_URL, else https://api.murielai.io)
34
+ timeout: Request timeout in seconds (default: 30)
35
+ """
36
+ self.api_key = api_key or get_api_key()
37
+ if not self.api_key:
38
+ raise MurielAuthenticationError(
39
+ "API key is required. Pass it as api_key parameter or set MURIEL_API_KEY environment variable."
40
+ )
41
+
42
+ self.base_url = checked_base_url(base_url or get_base_url())
43
+ self.timeout = timeout
44
+ self.session = requests.Session()
45
+ self.session.headers.update({"X-API-Key": self.api_key})
46
+
47
+ def _make_request(
48
+ self,
49
+ method: str,
50
+ endpoint: str,
51
+ params: Optional[Dict[str, Any]] = None,
52
+ json: Optional[Dict[str, Any]] = None,
53
+ ) -> Dict[str, Any]:
54
+ """
55
+ Make an HTTP request to the API.
56
+
57
+ Args:
58
+ method: HTTP method (GET, POST, etc.)
59
+ endpoint: API endpoint path
60
+ params: Query parameters
61
+ json: JSON body data
62
+
63
+ Returns:
64
+ Response data as dictionary
65
+
66
+ Raises:
67
+ MurielAPIError: For various API errors
68
+ """
69
+ url = f"{self.base_url}{endpoint}"
70
+
71
+ try:
72
+ response = self.session.request(
73
+ method=method,
74
+ url=url,
75
+ params=params,
76
+ json=json,
77
+ timeout=self.timeout,
78
+ # Never follow a redirect: requests strips Authorization on a
79
+ # cross-host redirect but NOT a custom X-API-Key header, so a
80
+ # redirect would hand the key to whatever host it points at.
81
+ allow_redirects=False,
82
+ )
83
+ if 300 <= response.status_code < 400:
84
+ raise MurielAPIError(
85
+ f"Unexpected redirect ({response.status_code}) to "
86
+ f"{response.headers.get('Location', '?')}; not followed, so your API "
87
+ "key was not sent there. Check MURIEL_BASE_URL.",
88
+ status_code=response.status_code,
89
+ )
90
+
91
+ # Handle different status codes
92
+ if response.status_code == 200:
93
+ return response.json()
94
+ elif response.status_code == 201:
95
+ return response.json()
96
+ elif response.status_code == 401:
97
+ raise MurielAuthenticationError(
98
+ "Invalid API key",
99
+ status_code=response.status_code,
100
+ response=response.json() if response.content else None,
101
+ )
102
+ elif response.status_code == 400:
103
+ error_data = response.json() if response.content else {}
104
+ raise MurielValidationError(
105
+ error_data.get("detail", "Validation error"),
106
+ status_code=response.status_code,
107
+ response=error_data,
108
+ )
109
+ elif response.status_code == 402:
110
+ raise MurielCreditsError(
111
+ detail_of(response, "Out of credits. Email info@murielai.io to add more."),
112
+ status_code=402,
113
+ response=response.json() if response.content else None,
114
+ )
115
+ elif response.status_code == 404:
116
+ raise MurielNotFoundError(
117
+ detail_of(response, "Resource not found"),
118
+ status_code=response.status_code,
119
+ response=response.json() if response.content else None,
120
+ )
121
+ elif response.status_code == 429:
122
+ raise MurielRateLimitError(
123
+ detail_of(response, "Rate limit exceeded"),
124
+ status_code=response.status_code,
125
+ response=response.json() if response.content else None,
126
+ )
127
+ elif response.status_code >= 500:
128
+ raise MurielServerError(
129
+ detail_of(response, "Server error"),
130
+ status_code=response.status_code,
131
+ response=response.json() if response.content else None,
132
+ )
133
+ else:
134
+ response.raise_for_status()
135
+ return response.json()
136
+
137
+ except requests.exceptions.Timeout:
138
+ raise MurielAPIError(f"Request timed out after {self.timeout} seconds")
139
+ except requests.exceptions.ConnectionError:
140
+ raise MurielAPIError(f"Failed to connect to {self.base_url}")
141
+ except requests.exceptions.RequestException as e:
142
+ if not isinstance(e, MurielAPIError):
143
+ raise MurielAPIError(f"Request failed: {str(e)}")
144
+ raise
145
+
146
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
147
+ """Make a GET request."""
148
+ return self._make_request("GET", endpoint, params=params)
149
+
150
+ def post(self, endpoint: str, json: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
151
+ """Make a POST request."""
152
+ return self._make_request("POST", endpoint, json=json)
153
+
154
+ def close(self):
155
+ """Close the HTTP session."""
156
+ self.session.close()
157
+
158
+ def __enter__(self):
159
+ """Context manager entry."""
160
+ return self
161
+
162
+ def __exit__(self, exc_type, exc_val, exc_tb):
163
+ """Context manager exit."""
164
+ self.close()
muriel/clients.py ADDED
@@ -0,0 +1,94 @@
1
+ """Object-oriented clients matching the API documentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from .config import get_api_key
8
+ from .endpoints.requirements import get_requirements as _get_requirements
9
+ from .endpoints.results import get_results as _get_results
10
+ from .endpoints.systems import get_systems as _get_systems
11
+
12
+
13
+ class _BaseClient:
14
+ def __init__(
15
+ self,
16
+ api_key: Optional[str] = None,
17
+ token: Optional[str] = None,
18
+ base_url: Optional[str] = None,
19
+ ):
20
+ resolved_key = api_key or token or get_api_key()
21
+ if not resolved_key:
22
+ raise ValueError("api_key or token is required, or set MURIEL_API_KEY")
23
+ self.api_key = resolved_key
24
+ self.base_url = base_url
25
+
26
+
27
+ class RequirementsClient(_BaseClient):
28
+ """Client for sampling requirements endpoints."""
29
+
30
+ def get_requirements(
31
+ self,
32
+ pwsid: Optional[str] = None,
33
+ state: Optional[str] = None,
34
+ limit: int = 100,
35
+ system_princ: Optional[str] = None,
36
+ ) -> Dict[str, Any]:
37
+ return _get_requirements(
38
+ pwsid=pwsid,
39
+ state=state,
40
+ api_key=self.api_key,
41
+ limit=limit,
42
+ system_princ=system_princ,
43
+ base_url=self.base_url,
44
+ )
45
+
46
+
47
+ class SystemsClient(_BaseClient):
48
+ """Client for public water system metadata."""
49
+
50
+ def get_systems(
51
+ self,
52
+ state: str,
53
+ limit: int = 100,
54
+ system_princ: Optional[str] = None,
55
+ system_statu: Optional[str] = None,
56
+ ) -> Dict[str, Any]:
57
+ return _get_systems(
58
+ state=state,
59
+ api_key=self.api_key,
60
+ limit=limit,
61
+ system_princ=system_princ,
62
+ system_statu=system_statu,
63
+ base_url=self.base_url,
64
+ )
65
+
66
+
67
+ class SamplesClient(_BaseClient):
68
+ """Client for sample results; wraps get_results() for messages-style parameters."""
69
+
70
+ def get_samples(
71
+ self,
72
+ pwsid: Optional[str] = None,
73
+ state: Optional[str] = None,
74
+ analyte: Optional[str] = None,
75
+ sample_date_start: Optional[str] = None,
76
+ sample_date_end: Optional[str] = None,
77
+ compliance_status: Optional[str] = None,
78
+ limit: int = 100,
79
+ **kwargs: Any,
80
+ ) -> List[Dict[str, Any]]:
81
+ if compliance_status is not None:
82
+ kwargs["compliance_status"] = compliance_status
83
+
84
+ return _get_results(
85
+ pwsid=pwsid,
86
+ state=state,
87
+ analyte=analyte,
88
+ start_date=sample_date_start,
89
+ end_date=sample_date_end,
90
+ api_key=self.api_key,
91
+ num=limit,
92
+ base_url=self.base_url,
93
+ **kwargs,
94
+ )