Skip to content

Commit

Permalink
Merge main into branch
Browse files Browse the repository at this point in the history
  • Loading branch information
johtoblan committed Mar 26, 2024
2 parents 88dfa01 + aaecdcf commit 6b8018a
Show file tree
Hide file tree
Showing 50 changed files with 1,356 additions and 572 deletions.
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: CI

defaults:
run:
shell: bash

on:
push:
branches:
Expand All @@ -23,17 +27,14 @@ jobs:
python-version: "3.11"
- name: Install pre-commit
run: python -m pip install pre-commit
shell: bash
- name: Show pre-commit requirements
run: python -m pip freeze --local
shell: bash
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit for all files
run: pre-commit run --config './.pre-commit-config.yaml' --all-files --color=always --show-diff-on-failure
shell: bash

test-datastore:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -129,9 +130,7 @@ jobs:
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install pytest-timeout
pip install pytest-cov
pip install httpx
pip install pytest-timeout pytest-cov httpx deepdiff
pip install -r ./api/requirements.txt
- name: Copy Protobuf file to api directory and build
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
*/pytest-coverage*
cover/

# Translations
Expand Down
10 changes: 9 additions & 1 deletion api/formatters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from enum import Enum

from . import covjson
from . import geojson

logger = logging.getLogger(__name__)

Expand All @@ -10,4 +11,11 @@ class Formats(str, Enum):
covjson = "CoverageJSON" # According to EDR spec


formatters = {"CoverageJSON": covjson.convert_to_covjson}
class Metadata_Formats(str, Enum):
geojson = "GeoJSON"


formatters = {
"CoverageJSON": covjson.convert_to_covjson,
} # observations
metadata_formatters = {"GeoJSON": geojson.convert_to_geojson} # metadata
6 changes: 4 additions & 2 deletions api/formatters/covjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@

def make_parameter(ts_mdata):
return Parameter(
description={"en": ts_mdata.title},
description={
"en": f"{ts_mdata.standard_name} at {ts_mdata.level}m {ts_mdata.period} {ts_mdata.function}",
},
observedProperty=ObservedProperty(
id=f"https://vocab.nerc.ac.uk/standard_name/{ts_mdata.standard_name}",
label={"en": ts_mdata.instrument},
label={"en": ts_mdata.parameter_name},
),
unit=Unit(label={"en": ts_mdata.unit}),
)
Expand Down
43 changes: 43 additions & 0 deletions api/formatters/geojson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from fastapi import HTTPException
from geojson_pydantic import Feature
from geojson_pydantic import FeatureCollection
from geojson_pydantic import Point


def _make_properties(ts):
ts_metadata = {key.name: value for key, value in ts.ts_mdata.ListFields() if value}

ts_metadata["platform_vocabulary"] = (
"https://oscar.wmo.int/surface/#/search/station/stationReportDetails/" + ts.ts_mdata.platform
if not ts.ts_mdata.platform_vocabulary
else ts.ts_mdata.platform_vocabulary
)
# This should also be compatible with future when name is added to datastore
if "name" not in ts_metadata:
ts_metadata["name"] = ts.ts_mdata.platform # TODO: grab proper name when implemented in proto

return ts_metadata


def convert_to_geojson(response):
"""
Will only generate geoJSON for stationary timeseries
"""
features = [
Feature(
type="Feature",
id=ts.ts_mdata.timeseries_id,
properties=_make_properties(ts=ts),
geometry=Point(
type="Point",
coordinates=[
ts.obs_mdata[0].geo_point.lon,
ts.obs_mdata[0].geo_point.lat,
],
),
)
for ts in sorted(response.observations, key=lambda ts: ts.ts_mdata.timeseries_id)
]
if not features:
raise HTTPException(404, detail="Query did not return any time series.")
return FeatureCollection(features=features, type="FeatureCollection") if len(features) > 1 else features[0]
8 changes: 7 additions & 1 deletion api/grpc_getter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ async def get_obs_request(request):
return response


async def getTSAGRequest(request):
async def get_ts_ag_request(request):
grpc_stub = get_grpc_stub()
response = await grpc_stub.GetTSAttrGroups(request)
return response


async def get_extents_request(request):
grpc_stub = get_grpc_stub()
response = await grpc_stub.GetExtents(request)
return response
7 changes: 4 additions & 3 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

import metadata_endpoints
from brotli_asgi import BrotliMiddleware
from dependencies import create_url_from_request
from edr_pydantic.capabilities import LandingPageModel
from edr_pydantic.collections import Collection
from edr_pydantic.collections import Collections
from fastapi import FastAPI
from fastapi import Request
from routers import edr # , records
from routers import edr
from routers import feature
from utilities import create_url_from_request


def setup_logging():
Expand Down Expand Up @@ -64,4 +65,4 @@ async def get_collection_metadata(request: Request) -> Collection:

# Include all routes
app.include_router(edr.router)
# app.include(records.router)
app.include_router(feature.router)
54 changes: 48 additions & 6 deletions api/metadata_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import logging
from datetime import datetime
from datetime import timezone
from typing import Dict

import datastore_pb2 as dstore
Expand All @@ -11,20 +13,44 @@
from edr_pydantic.data_queries import EDRQuery
from edr_pydantic.extent import Extent
from edr_pydantic.extent import Spatial
from edr_pydantic.extent import Temporal
from edr_pydantic.link import EDRQueryLink
from edr_pydantic.link import Link
from edr_pydantic.observed_property import ObservedProperty
from edr_pydantic.parameter import Parameter
from edr_pydantic.unit import Unit
from edr_pydantic.variables import Variables
from fastapi import HTTPException
from grpc_getter import getTSAGRequest
from grpc_getter import get_extents_request
from grpc_getter import get_ts_ag_request


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


def datetime_to_iso_string(value: datetime) -> str:
"""Returns the datetime as ISO 8601 string.
Changes timezone +00:00 to the military time zone indicator (Z).
Keyword arguments:
value -- A datetime
Returns:
datetime string -- Returns the datetime as an ISO 8601 string with the military indicator.
"""
if value.tzinfo is None:
# This sort of replicates the functionality of Pydantic's AwareDatetime type
raise ValueError("Datetime object is not timezone aware")

iso_8601_str = value.isoformat()
tz_offset_utc = "+00:00"
if iso_8601_str.endswith(tz_offset_utc):
return f"{iso_8601_str[:-len(tz_offset_utc)]}Z"
else:
return iso_8601_str


def get_landing_page(request):
return LandingPageModel(
title="E-SOH EDR API",
Expand All @@ -43,8 +69,8 @@ def get_landing_page(request):


async def get_collection_metadata(base_url: str, is_self) -> Collection:
ts_request = dstore.GetTSAGRequest(attrs=["parameter_name", "title", "standard_name", "instrument", "unit"])
ts_response = await getTSAGRequest(ts_request)
ts_request = dstore.GetTSAGRequest(attrs=["parameter_name", "standard_name", "unit", "level", "period", "function"])
ts_response = await get_ts_ag_request(ts_request)
# logger.info(ts_response.ByteSize())
# logger.info(len(ts_response.groups))

Expand All @@ -55,10 +81,10 @@ async def get_collection_metadata(base_url: str, is_self) -> Collection:
for group in ts_response.groups:
ts = group.combo
parameter = Parameter(
description=ts.title,
description=f"{ts.standard_name} at {ts.level}m {ts.period} {ts.function}",
observedProperty=ObservedProperty(
id=f"https://vocab.nerc.ac.uk/standard_name/{ts.standard_name}",
label=ts.instrument,
label=ts.parameter_name,
),
unit=Unit(label=ts.unit),
)
Expand All @@ -73,12 +99,28 @@ async def get_collection_metadata(base_url: str, is_self) -> Collection:

all_parameters[ts.parameter_name] = parameter

extent_request = dstore.GetExtentsRequest()
extent_response = await get_extents_request(extent_request)
spatial_extent = extent_response.spatial_extent
interval_start = extent_response.temporal_extent.start.ToDatetime(tzinfo=timezone.utc)
interval_end = extent_response.temporal_extent.end.ToDatetime(tzinfo=timezone.utc)

collection = Collection(
id="observations",
links=[
Link(href=f"{base_url}/observations", rel="self" if is_self else "data"),
],
extent=Extent(spatial=Spatial(bbox=[[3.0, 50.0, 8.0, 55.0]], crs="WGS84")), # TODO: Get this from database
extent=Extent(
spatial=Spatial(
bbox=[[spatial_extent.left, spatial_extent.bottom, spatial_extent.right, spatial_extent.top]],
crs="EPSG:4326",
),
temporal=Temporal(
interval=[[interval_start, interval_end]],
values=[f"{datetime_to_iso_string(interval_start)}/{datetime_to_iso_string(interval_end)}"],
trs="datetime",
),
),
data_queries=DataQueries(
position=EDRQuery(
link=EDRQueryLink(
Expand Down
2 changes: 2 additions & 0 deletions api/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ covjson-pydantic~=0.2.0
edr-pydantic~=0.2.0
shapely~=2.0
geojson-pydantic~=1.0
aiocached~=0.3.0
jinja2~=3.1
38 changes: 23 additions & 15 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --no-emit-index-url
# pip-compile --no-emit-index-url --strip-extras requirements.in
#
aiocached==0.3
# via -r requirements.in
annotated-types==0.6.0
# via pydantic
anyio==4.2.0
anyio==4.3.0
# via
# starlette
# watchfiles
Expand All @@ -20,13 +22,15 @@ covjson-pydantic==0.2.1
# via -r requirements.in
edr-pydantic==0.2.1
# via -r requirements.in
exceptiongroup==1.2.0
# via anyio
fastapi==0.109.2
# via -r requirements.in
geojson-pydantic==1.0.2
# via -r requirements.in
grpcio==1.60.1
grpcio==1.62.1
# via grpcio-tools
grpcio-tools==1.60.1
grpcio-tools==1.62.1
# via -r requirements.in
gunicorn==21.2.0
# via -r requirements.in
Expand All @@ -36,41 +40,45 @@ httptools==0.6.1
# via uvicorn
idna==3.6
# via anyio
jinja2==3.1.3
# via -r requirements.in
markupsafe==2.1.5
# via jinja2
numpy==1.26.4
# via shapely
packaging==23.2
packaging==24.0
# via gunicorn
protobuf==4.25.2
protobuf==4.25.3
# via grpcio-tools
pydantic==2.6.1
pydantic==2.6.4
# via
# covjson-pydantic
# edr-pydantic
# fastapi
# geojson-pydantic
pydantic-core==2.16.2
pydantic-core==2.16.3
# via pydantic
python-dotenv==1.0.1
# via uvicorn
pyyaml==6.0.1
# via uvicorn
shapely==2.0.2
shapely==2.0.3
# via -r requirements.in
sniffio==1.3.0
sniffio==1.3.1
# via anyio
starlette==0.36.3
# via
# brotli-asgi
# fastapi
typing-extensions==4.9.0
typing-extensions==4.10.0
# via
# anyio
# fastapi
# pydantic
# pydantic-core
uvicorn[standard]==0.23.2
# via
# -r requirements.in
# uvicorn
uvicorn==0.23.2
# via -r requirements.in
uvloop==0.19.0
# via uvicorn
watchfiles==0.21.0
Expand Down
Loading

1 comment on commit 6b8018a

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API Unit Test Coverage Report
FileStmtsMissCoverMissing
\_\_init\_\_.py00100% 
datastore_pb2.py584621%24–69
datastore_pb2_grpc.py432347%37–52, 85–87, 92–94, 99–101, 106–108, 112–136, 174, 191, 208, 225
grpc_getter.py201145%15–16, 20–23, 27–29, 33–35
locustfile.py15150%1–31
main.py34585%41, 51–52, 62–63
metadata_endpoints.py552555%42–51, 55, 72–151, 155
utilities.py683154%33, 40, 62–65, 73–80, 85–92, 104–123
custom_geo_json
   edr_feature_collection.py60100% 
formatters
   \_\_init\_\_.py110100% 
   covjson.py50198%73
   geojson.py15193%42
routers
   \_\_init\_\_.py00100% 
   edr.py972673%59–134, 214–215, 259–260
   feature.py451958%70–103, 113–118, 124–146
TOTAL51720361% 

API Unit Test Coverage Summary

Tests Skipped Failures Errors Time
17 0 💤 0 ❌ 0 🔥 1.800s ⏱️

Please sign in to comment.