Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 20230920 (#274) #277

Merged
merged 1 commit into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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