Skip to content

Commit

Permalink
Release 20230920 (#274)
Browse files Browse the repository at this point in the history
* included order_by in v1 and v2 measurements (#263)

Co-authored-by: Gabriel Fosse <[email protected]>

* manufacturers resource (#273)

* manufacturers resource resolves #252
---------

Co-authored-by: Gabriel Fosse <[email protected]>

* Adding `v3/instruments` resource (#271)

* instruments resource resolves #270 

---------

Co-authored-by: Gabriel Fosse <[email protected]>

---------

Co-authored-by: Gabriel Fosse <[email protected]>
Co-authored-by: Gabriel Fosse <[email protected]>
  • Loading branch information
3 people authored Sep 20, 2023
1 parent 25e6ebf commit 4b88bf9
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 19 deletions.
9 changes: 4 additions & 5 deletions openaq_api/openaq_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
UnprocessableEntityLog,
WarnLog,
)

# v2 routers
from openaq_api.routers.auth import router as auth_router
from openaq_api.routers.averages import router as averages_router
from openaq_api.routers.cities import router as cities_router
Expand All @@ -49,9 +47,10 @@
# V3 routers
from openaq_api.v3.routers import (
countries,
instruments,
locations,
manufacturers,
measurements,
owners,
parameters,
providers,
sensors,
Expand Down Expand Up @@ -246,17 +245,17 @@ def favico():
return RedirectResponse("https://openaq.org/assets/graphics/meta/favicon.png")

# v3
app.include_router(instruments.router)
app.include_router(locations.router)
app.include_router(parameters.router)
app.include_router(tiles.router)
app.include_router(countries.router)
app.include_router(manufacturers.router)
app.include_router(measurements.router)
app.include_router(owners.router)
app.include_router(trends.router)
app.include_router(providers.router)
app.include_router(sensors.router)

# v2
app.include_router(auth_router)
app.include_router(averages_router)
app.include_router(cities_router)
Expand Down
4 changes: 4 additions & 0 deletions openaq_api/openaq_api/routers/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async def measurements_get(
):
where = m.where()
params = m.params()
order_clause = f"ORDER BY {m.order_by} {m.sort}"
includes = m.include_fields

sql = f"""
Expand Down Expand Up @@ -226,6 +227,7 @@ async def measurements_get(
JOIN locations_view_cached sn ON (sy.sensor_nodes_id = sn.id)
JOIN measurands m ON (m.measurands_id = h.measurands_id)
WHERE {where}
{order_clause}
OFFSET :offset
LIMIT :limit;
"""
Expand Down Expand Up @@ -256,6 +258,7 @@ async def measurements_get_v1(
m.entity = "government"
params = m.params()
where = m.where()
order_clause = f"ORDER BY {m.order_by} {m.sort}"

sql = f"""
SELECT sn.id as "locationId"
Expand All @@ -277,6 +280,7 @@ async def measurements_get_v1(
JOIN measurands m ON (m.measurands_id = h.measurands_id)
JOIN countries c ON (c.countries_id = sn.countries_id)
WHERE {where}
{order_clause}
OFFSET :offset
LIMIT :limit
"""
Expand Down
24 changes: 17 additions & 7 deletions openaq_api/openaq_api/v3/models/responses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Any
from typing import Any, List

from humps import camelize
from pydantic import BaseModel, ConfigDict, Field
Expand Down Expand Up @@ -86,20 +86,25 @@ class EntityBase(JsonBase):
id: int
name: str


class OwnerBase(JsonBase):
id: int
name: str
locations_count: int = Field(alias='locationsCount')


class ProviderBase(JsonBase):
id: int
name: str


class InstrumentBase(JsonBase):
id: int
name: str


class ManufacturerBase(JsonBase):
id: int
name: str
entity: EntityBase


class Latest(JsonBase):
Expand All @@ -112,7 +117,6 @@ class InstrumentBase(JsonBase):
id: int
name: str


class ParameterBase(JsonBase):
id: int
name: str
Expand Down Expand Up @@ -167,15 +171,19 @@ class Provider(ProviderBase):


class Owner(OwnerBase):
...
entity: EntityBase


class Instrument(InstrumentBase):
locations_count: int = Field(alias='locationsCount')
is_monitor: bool = Field(alias='isMonitor')
manufacturer: ManufacturerBase


class Manufacturer(ManufacturerBase):
...
instruments: List[InstrumentBase]
locations_count: int = Field(alias="locationsCount")



class Sensor(SensorBase):
Expand Down Expand Up @@ -227,6 +235,8 @@ class Trend(JsonBase):

# response classes

class InstrumentsResponse(OpenAQResult):
results: list[Instrument]

class LocationsResponse(OpenAQResult):
results: list[Location]
Expand Down
155 changes: 155 additions & 0 deletions openaq_api/openaq_api/v3/routers/instruments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import logging
from typing import Annotated

from fastapi import APIRouter, Depends, Path

from openaq_api.db import DB
from openaq_api.v3.models.queries import (
Paging,
QueryBaseModel,
QueryBuilder,

)
from openaq_api.v3.models.responses import InstrumentsResponse

logger = logging.getLogger("instruments")

router = APIRouter(
prefix="/v3",
tags=["v3-alpha"],
include_in_schema=True,
)

class ManufacturerInstrumentsQuery(QueryBaseModel):
"""
Path query to filter results by manufacturers ID
Inherits from QueryBaseModel
Attributes:
manufacturers_id: manufacturers ID value
"""

manufacturers_id: int = Path(
..., description="Limit results to a specific manufacturer id", ge=1
)

def where(self) -> str:
return "i.manufacturer_entities_id = :manufacturers_id"

class InstrumentPathQuery(QueryBaseModel):
"""Path query to filter results by instruments ID
Inherits from QueryBaseModel
Attributes:
instruments_id: instruments ID value
"""

instruments_id: int = Path(
..., description="Limit the results to a specific instruments id", ge=1
)

def where(self) -> str:
"""Generates SQL condition for filtering to a single instruments_id
Overrides the base QueryBaseModel `where` method
Returns:
string of WHERE clause
"""
return "i.instruments_id = :instruments_id"


class InstrumentsQueries(
Paging,
):
...


@router.get(
"/instruments/{instruments_id}",
response_model=InstrumentsResponse,
summary="Get an instrument by ID",
description="Provides a instrument by instrument ID",
)
async def instrument_get(
instruments: Annotated[
InstrumentPathQuery, Depends(InstrumentPathQuery.depends())
],
db: DB = Depends(),
):
response = await fetch_instruments(instruments, db)
return response


@router.get(
"/instruments",
response_model=InstrumentsResponse,
summary="Get instruments",
description="Provides a list of instruments",
)
async def instruments_get(
instruments: Annotated[
InstrumentsQueries, Depends(InstrumentsQueries.depends())
],
db: DB = Depends(),
):
response = await fetch_instruments(instruments, db)
return response

@router.get(
"/manufacturers/{manufacturers_id}/instruments",
response_model=InstrumentsResponse,
summary="Get instruments by manufacturer ID",
description="Provides a list of instruments for a specific manufacturer",
)
async def get_instruments_by_manufacturer(
manufacturer: Annotated[
ManufacturerInstrumentsQuery, Depends(ManufacturerInstrumentsQuery.depends())
],
db: DB = Depends(),
):
response = await fetch_instruments(manufacturer, db)
return response

async def fetch_instruments(query, db):
query_builder = QueryBuilder(query)
sql = f"""
WITH locations_summary AS (
SELECT
i.instruments_id
, COUNT(sn.sensor_nodes_id) AS locations_count
FROM
sensor_nodes sn
JOIN
sensor_systems ss ON sn.sensor_nodes_id = ss.sensor_nodes_id
JOIN
instruments i ON i.instruments_id = ss.instruments_id
GROUP BY i.instruments_id
)
SELECT
instruments_id AS id
, label AS name
, locations_count
, is_monitor
, json_build_object('id', e.entities_id, 'name', e.full_name) AS manufacturer
FROM
instruments i
JOIN
locations_summary USING (instruments_id)
JOIN
entities e
ON
i.manufacturer_entities_id = e.entities_id
{query_builder.where()}
ORDER BY
instruments_id
{query_builder.pagination()};
"""


response = await db.fetchPage(sql, query_builder.params())
return response
40 changes: 33 additions & 7 deletions openaq_api/openaq_api/v3/routers/manufacturers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
from fastapi import APIRouter, Depends, Path

from openaq_api.db import DB
from openaq_api.v3.models.queries import Paging, QueryBaseModel, QueryBuilder
from openaq_api.v3.models.queries import (
Paging,
QueryBaseModel,
QueryBuilder,
)
from openaq_api.v3.models.responses import ManufacturersResponse

logger = logging.getLogger("manufacturers")
Expand All @@ -15,7 +19,6 @@
include_in_schema=True,
)


class ManufacturerPathQuery(QueryBaseModel):
"""Path query to filter results by manufacturers ID
Expand All @@ -37,10 +40,12 @@ def where(self) -> str:
Returns:
string of WHERE clause
"""
return "id = :manufacturers_id"
return "e.entities_id = :manufacturers_id"


class ManufacturersQueries(Paging):
class ManufacturersQueries(
Paging
):
...


Expand Down Expand Up @@ -78,7 +83,28 @@ async def manufacturers_get(

async def fetch_manufacturers(query, db):
query_builder = QueryBuilder(query)
sql = f"""
"""
sql = f"""
SELECT
e.entities_id AS id
, e.full_name AS name
, ARRAY_AGG(DISTINCT (jsonb_build_object('id', i.instruments_id, 'name', i.label))) AS instruments
, COUNT(sn.sensor_nodes_id) AS locations_count
, COUNT(1) OVER() AS found
FROM
sensor_nodes sn
JOIN
sensor_systems ss ON sn.sensor_nodes_id = ss.sensor_nodes_id
JOIN
instruments i ON i.instruments_id = ss.instruments_id
JOIN
entities e ON e.entities_id = i.manufacturer_entities_id
{query_builder.where()}
GROUP BY id, name
{query_builder.pagination()};
"""


response = await db.fetchPage(sql, query_builder.params())
return response
return response

0 comments on commit 4b88bf9

Please sign in to comment.