diff --git a/docs/index.md b/docs/index.md index add402b..d0250ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,14 +3,14 @@ !!! warning The OpenAQ python client is still under active development and may be unstable until - a v1.0.0 release. + a v1.0.0 release. - -The OpenAQ Python SDK provides a Python interface for interacting the with OpenAQ API. The library is compatible with Python versions 3.8, 3.9, 3.10, 3.11 and 3.12 +The OpenAQ Python SDK provides a Python interface for interacting the with +OpenAQ API. The library is compatible with Python versions 3.8, 3.9, 3.10, 3.11 +and 3.12 Features: -* Synchronous and Asynchronous client options. -* Deserialized response classes. -* Fully type annotated. - +- Synchronous and Asynchronous client options. +- Deserialized response classes. +- Comprehensive Type Annotations. diff --git a/openaq/_async/models/locations.py b/openaq/_async/models/locations.py index e241e57..066cff8 100644 --- a/openaq/_async/models/locations.py +++ b/openaq/_async/models/locations.py @@ -3,7 +3,7 @@ from typing import List, Tuple, Union from openaq.shared.models import build_query_params -from openaq.shared.responses import LocationsResponse +from openaq.shared.responses import LatestResponse, LocationsResponse, SensorsResponse from .base import AsyncResourceBase @@ -33,10 +33,32 @@ async def get(self, locations_id: int) -> LocationsResponse: location = await self._client._get(f"/locations/{locations_id}") return LocationsResponse.read_response(location) + async def latest(self, locations_id: int) -> LatestResponse: + """Retrieve latest measurements from a location. + + Args: + locations_id: The locations ID of the location to retrieve. + + Returns: + LatestResponse: An instance representing the retrieved latest results. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + latest = await self._client._get(f"/locations/{locations_id}/latest") + return LatestResponse.read_response(latest) + async def list( self, page: int = 1, - limit: int = 1000, + limit: int = 100, radius: Union[int, None] = None, coordinates: Union[Tuple[float, float], None] = None, bbox: Union[Tuple[float, float, float, float], None] = None, @@ -117,3 +139,25 @@ async def list( locations = await self._client._get("/locations", params=params) return LocationsResponse.read_response(locations) + + async def sensors(self, locations_id: int) -> SensorsResponse: + """Retrieve sensors from a location. + + Args: + locations_id: The locations ID of the location to retrieve. + + Returns: + SensorsResponse: An instance representing the retrieved latest results. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + sensors = await self._client._get(f"/locations/{locations_id}/sensors") + return SensorsResponse.read_response(sensors) diff --git a/openaq/_async/models/manufacturers.py b/openaq/_async/models/manufacturers.py index 56460ea..4951401 100644 --- a/openaq/_async/models/manufacturers.py +++ b/openaq/_async/models/manufacturers.py @@ -1,5 +1,5 @@ from openaq.shared.models import build_query_params -from openaq.shared.responses import ManufacturersResponse +from openaq.shared.responses import InstrumentsResponse, ManufacturersResponse from .base import AsyncResourceBase @@ -70,3 +70,27 @@ async def list( manufacturers = await self._client._get("/manufacturers", params=params) return ManufacturersResponse.read_response(manufacturers) + + async def instruments(self, manufacturers_id: int) -> InstrumentsResponse: + """Retrieve instruments of a manufacturer by ID. + + Args: + manufacturers_id: The manufacturers ID of the manufacturer to retrieve. + + Returns: + InstrumentsResponse: An instance representing the retrieved instruments. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + instruments_response = await self._client._get( + f"/manufacturers/{manufacturers_id}/instruments" + ) + return InstrumentsResponse.read_response(instruments_response) diff --git a/openaq/_async/models/parameters.py b/openaq/_async/models/parameters.py index 39171d1..c92feb5 100644 --- a/openaq/_async/models/parameters.py +++ b/openaq/_async/models/parameters.py @@ -1,5 +1,5 @@ from openaq.shared.models import build_query_params -from openaq.shared.responses import ParametersResponse +from openaq.shared.responses import LatestResponse, ParametersResponse from .base import AsyncResourceBase @@ -97,3 +97,25 @@ async def list( parameters = await self._client._get("/parameters", params=params) return ParametersResponse.read_response(parameters) + + async def latest(self, parameters_id: int) -> LatestResponse: + """Retrieve latest measurements from a location. + + Args: + parameters_id: The locations ID of the location to retrieve. + + Returns: + LatestResponse: An instance representing the retrieved latest results. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + latest = await self._client._get(f"/parameters/{parameters_id}/latest") + return LatestResponse.read_response(latest) diff --git a/openaq/_sync/models/licenses.py b/openaq/_sync/models/licenses.py index 55c690b..583148c 100644 --- a/openaq/_sync/models/licenses.py +++ b/openaq/_sync/models/licenses.py @@ -9,7 +9,7 @@ class Licenses(SyncResourceBase): - """This provides methods to retrieve air monitor locations resource from the OpenAQ API.""" + """This provides methods to retrieve air monitor latest resource from the OpenAQ API.""" def get(self, licenses_id: int) -> LicensesResponse: """Retrieve a specific license by its licenses ID. diff --git a/openaq/_sync/models/locations.py b/openaq/_sync/models/locations.py index 5c9a836..ce8dc25 100644 --- a/openaq/_sync/models/locations.py +++ b/openaq/_sync/models/locations.py @@ -3,7 +3,7 @@ from typing import List, Tuple, Union from openaq.shared.models import build_query_params -from openaq.shared.responses import LocationsResponse +from openaq.shared.responses import LatestResponse, LocationsResponse, SensorsResponse from .base import SyncResourceBase @@ -33,10 +33,32 @@ def get(self, locations_id: int) -> LocationsResponse: location_response = self._client._get(f"/locations/{locations_id}") return LocationsResponse.read_response(location_response) + def latest(self, locations_id: int) -> LatestResponse: + """Retrieve latest measurements from a location. + + Args: + locations_id: The locations ID of the location to retrieve. + + Returns: + LatestResponse: An instance representing the retrieved latest results. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + latest = self._client._get(f"/locations/{locations_id}/latest") + return LatestResponse.read_response(latest) + def list( self, page: int = 1, - limit: int = 1000, + limit: int = 100, radius: Union[int, None] = None, coordinates: Union[Tuple[float, float], None] = None, bbox: Union[Tuple[float, float, float, float], None] = None, @@ -117,3 +139,25 @@ def list( locations_response = self._client._get("/locations", params=params) return LocationsResponse.read_response(locations_response) + + def sensors(self, locations_id: int) -> SensorsResponse: + """Retrieve sensors from a location. + + Args: + locations_id: The locations ID of the location to retrieve. + + Returns: + SensorsResponse: An instance representing the retrieved latest results. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + sensors = self._client._get(f"/locations/{locations_id}/sensors") + return SensorsResponse.read_response(sensors) diff --git a/openaq/_sync/models/manufacturers.py b/openaq/_sync/models/manufacturers.py index ada70d5..396fe5c 100644 --- a/openaq/_sync/models/manufacturers.py +++ b/openaq/_sync/models/manufacturers.py @@ -1,5 +1,5 @@ from openaq.shared.models import build_query_params -from openaq.shared.responses import ManufacturersResponse +from openaq.shared.responses import InstrumentsResponse, ManufacturersResponse from .base import SyncResourceBase @@ -70,3 +70,27 @@ def list( manufacturer_response = self._client._get("/manufacturers", params=params) return ManufacturersResponse.read_response(manufacturer_response) + + def instruments(self, manufacturers_id: int) -> InstrumentsResponse: + """Retrieve instruments of a manufacturer by ID. + + Args: + manufacturers_id: The manufacturers ID of the manufacturer to retrieve. + + Returns: + InstrumentsResponse: An instance representing the retrieved instruments. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + instruments_response = self._client._get( + f"/manufacturers/{manufacturers_id}/instruments" + ) + return InstrumentsResponse.read_response(instruments_response) diff --git a/openaq/_sync/models/parameters.py b/openaq/_sync/models/parameters.py index 763d5ab..95822f2 100644 --- a/openaq/_sync/models/parameters.py +++ b/openaq/_sync/models/parameters.py @@ -1,5 +1,5 @@ from openaq.shared.models import build_query_params -from openaq.shared.responses import ParametersResponse +from openaq.shared.responses import LatestResponse, ParametersResponse from .base import SyncResourceBase @@ -29,6 +29,28 @@ def get(self, parameters_id: int) -> ParametersResponse: parameter_response = self._client._get(f"/parameters/{parameters_id}") return ParametersResponse.read_response(parameter_response) + def latest(self, parameters_id: int) -> LatestResponse: + """Retrieve latest measurements from a location. + + Args: + parameters_id: The locations ID of the location to retrieve. + + Returns: + LatestResponse: An instance representing the retrieved latest results. + + Raises: + BadRequestError: Raised for HTTP 400 error, indicating a client request error. + NotAuthorized: Raised for HTTP 401 error, indicating the client is not authorized. + Forbidden: Raised for HTTP 403 error, indicating the request is forbidden. + NotFoundError: Raised for HTTP 404 error, indicating a resource is not found. + ValidationError: Raised for HTTP 422 error, indicating invalid request parameters. + RateLimit: Raised for HTTP 429 error, indicating rate limit exceeded. + ServerError: Raised for HTTP 500 error, indicating an internal server error or unexpected server-side issue. + GatewayTimeoutError: Raised for HTTP 504 error, indicating a gateway timeout. + """ + latest = self._client._get(f"/parameters/{parameters_id}/latest") + return LatestResponse.read_response(latest) + def list( self, page: int = 1, diff --git a/openaq/shared/responses.py b/openaq/shared/responses.py index ed7acb0..6fb8b1e 100644 --- a/openaq/shared/responses.py +++ b/openaq/shared/responses.py @@ -880,7 +880,7 @@ def __post_init__(self): @dataclass -class Latest(_ResourceBase): +class LatestBase(_ResourceBase): """latest measurement. Attributes: @@ -896,11 +896,11 @@ class Latest(_ResourceBase): @dataclass class Sensor(_ResourceBase): - """Detailed information about an owner in OpenAQ. + """Detailed information about an sensor in OpenAQ. Attributes: - id: unique identifier for owner - name: owner name + id: unique identifier for sensor + name: sensor name """ id: int @@ -909,7 +909,7 @@ class Sensor(_ResourceBase): datetime_first: Datetime datetime_last: Datetime coverage: Coverage - latest: Latest + latest: LatestBase summary: Summary @@ -930,3 +930,41 @@ def __post_init__(self): self.meta = Meta.load(self.meta) if isinstance(self.results, list): self.results = [Sensor.load(x) for x in self.results] + + +@dataclass +class Latest(_ResourceBase): + """Latest measurement. + + Attributes: + datetime: datetime object + value: measured value + coordinates: coordinates object with latitude and longitude of measurement + sensors_id: unique identifier for sensor + locations_id: unique identifier for location + """ + + datetime: Datetime + value: float + coordinates: Coordinates + sensors_id: int + locations_id: int + + +@dataclass +class LatestResponse(_ResponseBase): + """Representation of the API response for latest resource. + + Attributes: + meta: a metadata object containing information about the results. + results: a list of latest records. + """ + + results: List[Latest] + + def __post_init__(self): + """Sets class attributes to correct type after checking input type.""" + if isinstance(self.meta, dict): + self.meta = Meta.load(self.meta) + if isinstance(self.results, list): + self.results = [Latest.load(x) for x in self.results] diff --git a/tests/integration/test_async_client.py b/tests/integration/test_async_client.py index 96e3db4..6b083da 100644 --- a/tests/integration/test_async_client.py +++ b/tests/integration/test_async_client.py @@ -28,11 +28,14 @@ class TestAsyncClient: def setup(self): self.client = AsyncOpenAQ(base_url=os.environ.get("TEST_BASE_URL")) + async def test_locations_list(self): + await self.client.locations.list() + async def test_locations_get(self): await self.client.locations.get(1) - async def test_locations_list(self): - await self.client.locations.list() + async def test_latest_get(self): + await self.client.locations.latest(1) async def test_countries_list(self): await self.client.countries.list() @@ -46,12 +49,21 @@ async def test_licenses_list(self): async def test_licenses_get(self): await self.client.licenses.get(1) - async def test_licenses_list(self): + async def test_owners_list(self): await self.client.owners.list() - async def test_licenses_get(self): + async def test_owners_get(self): await self.client.owners.get(1) + async def test_parameters_list(self): + await self.client.parameters.list() + + async def test_parameters_get(self): + await self.client.parameters.get(1) + + async def test_parameters_latest(self): + await self.client.parameters.latest(1) + async def test_providers_list(self): await self.client.providers.list() @@ -63,3 +75,15 @@ async def test_instruments_list(self): async def test_instruments_get(self): await self.client.instruments.get(1) + + async def test_manufacturers_list(self): + await self.client.manufacturers.list() + + async def test_manufacturers_get(self): + await self.client.manufacturers.get(1) + + async def test_manufacturers_instruments(self): + await self.client.manufacturers.instruments(1) + + async def test_sensors_get(self): + await self.client.sensors.get(1) diff --git a/tests/integration/test_sync_client.py b/tests/integration/test_sync_client.py index b067039..3ba08c2 100644 --- a/tests/integration/test_sync_client.py +++ b/tests/integration/test_sync_client.py @@ -27,12 +27,21 @@ def test_licenses_list(self): def test_licenses_get(self): self.client.licenses.get(1) - def test_licenses_list(self): + def test_owners_list(self): self.client.owners.list() - def test_licenses_get(self): + def test_owners_get(self): self.client.owners.get(1) + def test_parameters_list(self): + self.client.parameters.list() + + def test_parameters_get(self): + self.client.parameters.get(1) + + def test_parameters_latest(self): + self.client.parameters.latest(1) + def test_providers_list(self): self.client.providers.list() @@ -51,8 +60,8 @@ def test_manufacturers_list(self): def test_manufacturers_get(self): self.client.manufacturers.get(1) + def test_manufacturers_instruments(self): + self.client.manufacturers.instruments(1) + def test_sensors_get(self): self.client.sensors.get(1) - - def test_locations_sensors_list(self): - self.client.sensors.list(1)