diff --git a/toardb/contacts/crud.py b/toardb/contacts/crud.py index 6fbbda776a722ada56a0082ac01799d4e20a0d4b..5a3460dd10171a596655f66ca738179bf02a7de3 100644 --- a/toardb/contacts/crud.py +++ b/toardb/contacts/crud.py @@ -19,13 +19,14 @@ def get_organisation(db: Session, organisation_id: int): def get_all_organisations(db: Session, skip : int = 0, limit: int = None): return db.query(models.Organisation).offset(skip).limit(limit).all() + def get_organisation_by_name(db: Session, name: str): return db.query(models.Organisation).filter(models.Organisation.name == name).first() - + def create_organisation(db: Session, organisation: OrganisationCreate): db_organisation = models.Organisation(**organisation.dict()) - db_organisation.kind = get_value_from_str(db,OK_enum,db_organisation.kind) + db_organisation.kind = get_value_from_str(OK_enum,db_organisation.kind) db.add(db_organisation) result = db.commit() db.refresh(db_organisation) diff --git a/toardb/contacts/fixtures/organisations.json b/toardb/contacts/fixtures/organisations.json index aa2782d034f2469c46889aabe01197e037a785c3..eb7c07bfc0edd6416fdd564920577ecfee63fa01 100644 --- a/toardb/contacts/fixtures/organisations.json +++ b/toardb/contacts/fixtures/organisations.json @@ -12,7 +12,7 @@ { "name": "FZJ", "longname": "Forschungszentrum Jülich", - "kind": 1, + "kind": 2, "city": "Jülich", "postcode": "52425", "street_address": "Wilhelm-Johnen-Straße", diff --git a/toardb/contacts/models.py b/toardb/contacts/models.py index b112305a2c44d765e43c54c5e6af1aaf56a23ff9..c4cbe6e47697e19a47ea06f30bb56ae9122c8a36 100644 --- a/toardb/contacts/models.py +++ b/toardb/contacts/models.py @@ -10,10 +10,22 @@ from sqlalchemy.ext.declarative import declarative_base # controlled vocabulary # Kind of Organizations -OK_enum = Table("ok_vocabulary", +OK_enum_table = Table("ok_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) - +# The following code is just a workaround (see stationmeta/models.py): +from collections import namedtuple +Enumdict=namedtuple("Dict",["value","string","display_str"]) +OK_enum = ( + Enumdict(1, 'Government', 'government'), + Enumdict(2, 'Research', 'research'), + Enumdict(3, 'International', 'international'), + Enumdict(4, 'NonProfit', 'non-profit'), + Enumdict(5, 'Commercial', 'commercial'), + Enumdict(6, 'Individual', 'individual'), + Enumdict(7, 'Other', 'other') + ) + diff --git a/toardb/contacts/schemas.py b/toardb/contacts/schemas.py index 91ab7dc1e682202f66e0bda0f7d21ad8353a28c3..25cd3404b5250c6711b31e73dee45edd6d0b8dbc 100644 --- a/toardb/contacts/schemas.py +++ b/toardb/contacts/schemas.py @@ -5,7 +5,8 @@ Pydantic schemas for TOAR database from typing import List -from pydantic import BaseModel +from pydantic import BaseModel, validator +from .models import OK_enum class OrganisationBase(BaseModel): id: int = None @@ -18,10 +19,22 @@ class OrganisationBase(BaseModel): country: str homepage: str + @validator('kind') + def check_kind(cls, v): + return tuple(filter(lambda x: x.value == int(v), OK_enum))[0].string + class OrganisationCreate(OrganisationBase): + kind: str pass + @validator('kind') + def check_kind(cls, v): + if tuple(filter(lambda x: x.string == v, OK_enum)): + return v + else: + raise ValueError(f"kind of organisation not known: {v}") + class Organisation(OrganisationBase): id: int diff --git a/toardb/contacts/test_contacts.py b/toardb/contacts/test_contacts.py index 06b3e9861b46c40a897560a461442804f265cea5..7eabaed6856792a2bc7466f8e486e2b4ea7d85f6 100644 --- a/toardb/contacts/test_contacts.py +++ b/toardb/contacts/test_contacts.py @@ -53,11 +53,11 @@ class TestApps: expected_status_code = 200 assert response.status_code == expected_status_code expected_resp = [{"id":1,"name":"UBA","longname":"Umweltbundesamt", - "kind":1,"city":"Dessau-Roßlau","postcode":"06844", + "kind":"Government","city":"Dessau-Roßlau","postcode":"06844", "street_address":"Wörlitzer Platz 1","country":"Germany", "homepage":"https://www.umweltbundesamt.de"}, {"id":2,"name":"FZJ","longname":"Forschungszentrum Jülich", - "kind":1,"city":"Jülich","postcode":"52425", + "kind":"Research","city":"Jülich","postcode":"52425", "street_address":"Wilhelm-Johnen-Straße","country":"Germany", "homepage":"https://www.fz-juelich.de"}] assert response.json() == expected_resp @@ -68,7 +68,7 @@ class TestApps: expected_status_code = 200 assert response.status_code == expected_status_code expected_resp = {"id":2,"name":"FZJ","longname":"Forschungszentrum Jülich", - "kind":1,"city":"Jülich","postcode":"52425", + "kind":"Research","city":"Jülich","postcode":"52425", "street_address":"Wilhelm-Johnen-Straße","country":"Germany", "homepage":"https://www.fz-juelich.de"} assert response.json() == expected_resp @@ -87,7 +87,7 @@ class TestApps: expected_status_code = 200 assert response.status_code == expected_status_code expected_resp = {"id":2,"name":"FZJ","longname":"Forschungszentrum Jülich", - "kind":1,"city":"Jülich","postcode":"52425", + "kind":"Research","city":"Jülich","postcode":"52425", "street_address":"Wilhelm-Johnen-Straße","country":"Germany", "homepage":"https://www.fz-juelich.de"} assert response.json() == expected_resp @@ -114,7 +114,7 @@ class TestApps: response = client.post("/contacts/organisations/", json= {"organisation": {"name": "FZJ2", "longname": "Forschungszentrum Test", - "kind": 1, "city": "Jülich", "postcode": "52425", + "kind": "Research", "city": "Jülich", "postcode": "52425", "street_address": "Wilhelm-Johnen-Straße", "country": "Germany", "homepage": "https://www.fz-juelich.de"} @@ -123,7 +123,7 @@ class TestApps: expected_status_code = 200 assert response.status_code == expected_status_code expected_resp = {"id":3,"name":"FZJ2","longname":"Forschungszentrum Test", - "kind":1,"city":"Jülich","postcode":"52425", + "kind":"Research","city":"Jülich","postcode":"52425", "street_address":"Wilhelm-Johnen-Straße","country":"Germany", "homepage":"https://www.fz-juelich.de"} assert response.json() == expected_resp @@ -133,7 +133,7 @@ class TestApps: response = client.post("/contacts/organisations/", json= {"organisation": {"name": "FZJ", "longname": "Forschungszentrum Jülich", - "kind": 1, "city": "Jülich", "postcode": "52425", + "kind": "Research", "city": "Jülich", "postcode": "52425", "street_address": "Wilhelm-Johnen-Straße", "country": "Germany", "homepage": "https://www.fz-juelich.de"} diff --git a/toardb/data/models.py b/toardb/data/models.py index 3d08600a1efbc86e14b0719a0ec18eee42f799b7..9c82e76bb9711db3d15e4694a7c893ff3a3a81fe 100644 --- a/toardb/data/models.py +++ b/toardb/data/models.py @@ -1,5 +1,6 @@ # coding: utf-8 -from sqlalchemy import PrimaryKeyConstraint, Column, DateTime, Float, ForeignKey, Integer, text +from sqlalchemy import PrimaryKeyConstraint, Column, DateTime, Float, ForeignKey, Integer, text, \ + Table, String from sqlalchemy.orm import relationship from sqlalchemy.sql.sqltypes import NullType from sqlalchemy.dialects.postgresql import JSONB @@ -46,3 +47,35 @@ class Data(Base): # use the explicit class name here, # see: https://groups.google.com/forum/#!topic/sqlalchemy/YjGhE4d6K4U timeseries_id = Column(ForeignKey(Timeseries.id, deferrable=True, initially='DEFERRED'), nullable=False, index=True) + +# controlled vocabulary + +# Data Access Rights +DF_enum_table = Table("df_vocabulary", + Base.metadata, + Column("enum_val", Integer, primary_key=True), + Column("enum_str", String), + Column("enum_display_str", String) + ) +# The following code is just a workaround (see stationmeta/models.py): +from collections import namedtuple +Enumdict=namedtuple("Dict",["value","string","display_str"]) +DF_enum = ( + Enumdict( 0,'OK', 'OK'), + Enumdict( 1,'OKPreliminary', 'OK preliminary'), + Enumdict( 2,'OKModified', 'OK modified'), + Enumdict( 3,'OKPreliminaryModified', 'OK preliminary modified'), + Enumdict( 4,'Inconsistent', 'inconsistent'), + Enumdict( 5,'InconsistentPreliminary', 'inconsistent preliminary'), + Enumdict( 6,'Doubtful', 'doubtful'), + Enumdict( 7,'DoubtfulPreliminary', 'doubtful preliminary'), + Enumdict( 8,'DoubtfulModified', 'doubtful modified'), + Enumdict( 9,'DoubtfulPreliminaryModified', 'doubtful preliminary modified'), + Enumdict(10,'Wrong', 'wrong'), + Enumdict(11,'WrongPreliminary', 'wrong preliminary'), + Enumdict(12,'NotCheckedPreliminary', 'not checked preliminary'), + Enumdict(13,'Changed', 'changed'), + Enumdict(14,'Estimated', 'estimated'), + Enumdict(15,'MissingValue', 'missing value') + ) + diff --git a/toardb/data/schemas.py b/toardb/data/schemas.py index c15615703efdf7e4827beba92cd7d3fbb38e9e56..f7c9bce81f4bf5c636b2bacaf40c973169d38f0f 100644 --- a/toardb/data/schemas.py +++ b/toardb/data/schemas.py @@ -5,19 +5,30 @@ Pydantic schemas for TOAR database from typing import List -from pydantic import BaseModel +from pydantic import BaseModel, validator import datetime as dt +from .models import DF_enum class DataBase(BaseModel): datetime: dt.datetime value: float - flags: int + flags: str timeseries_id: int + @validator('flags') + def check_flags(cls, v): + return tuple(filter(lambda x: x.value == int(v), DF_enum))[0].string class DataCreate(DataBase): pass + @validator('flags') + def check_flags(cls, v): + if tuple(filter(lambda x: x.string == v, DF_enum)): + return v + else: + raise ValueError(f"data flag not known: {v}") + class Data(DataBase): diff --git a/toardb/data/test_data.py b/toardb/data/test_data.py index b3354a77addaaf362c75b67448386330c440c94d..5e90381af3691a4759cf4897c7a343a227ce7e6d 100644 --- a/toardb/data/test_data.py +++ b/toardb/data/test_data.py @@ -130,10 +130,10 @@ class TestApps: response = client.get("/data/?limit=4") expected_status_code = 200 assert response.status_code == expected_status_code - expected_resp = [{'datetime': '2012-12-16T21:00:00+01:00', 'value': 21.581, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-16T22:00:00+01:00', 'value': 13.734, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-16T23:00:00+01:00', 'value': 13.734, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T00:00:00+01:00', 'value': 7.848, 'flags': 0, 'timeseries_id': 1}] + expected_resp = [{'datetime': '2012-12-16T21:00:00+01:00', 'value': 21.581, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-16T22:00:00+01:00', 'value': 13.734, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-16T23:00:00+01:00', 'value': 13.734, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T00:00:00+01:00', 'value': 7.848, 'flags': 'OK', 'timeseries_id': 1}] assert response.json() == expected_resp @@ -141,16 +141,16 @@ class TestApps: response = client.get("/data/1") expected_status_code = 200 assert response.status_code == expected_status_code - expected_resp = [{'datetime': '2012-12-16T21:00:00+01:00', 'value': 21.581, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-16T22:00:00+01:00', 'value': 13.734, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-16T23:00:00+01:00', 'value': 13.734, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T00:00:00+01:00', 'value': 7.848, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T01:00:00+01:00', 'value': 15.696, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T02:00:00+01:00', 'value': 11.772, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T03:00:00+01:00', 'value': 13.734, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T04:00:00+01:00', 'value': 19.62, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T05:00:00+01:00', 'value': 15.696, 'flags': 0, 'timeseries_id': 1}, - {'datetime': '2012-12-17T06:00:00+01:00', 'value': 5.886, 'flags': 0, 'timeseries_id': 1}] + expected_resp = [{'datetime': '2012-12-16T21:00:00+01:00', 'value': 21.581, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-16T22:00:00+01:00', 'value': 13.734, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-16T23:00:00+01:00', 'value': 13.734, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T00:00:00+01:00', 'value': 7.848, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T01:00:00+01:00', 'value': 15.696, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T02:00:00+01:00', 'value': 11.772, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T03:00:00+01:00', 'value': 13.734, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T04:00:00+01:00', 'value': 19.62, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T05:00:00+01:00', 'value': 15.696, 'flags': 'OK', 'timeseries_id': 1}, + {'datetime': '2012-12-17T06:00:00+01:00', 'value': 5.886, 'flags': 'OK', 'timeseries_id': 1}] assert response.json() == expected_resp diff --git a/toardb/generic/models.py b/toardb/generic/models.py index 51101e039345efe4e67b835ad2fdfb708c488869..42beb57373cfb496042303a71344b37a9733c629 100644 --- a/toardb/generic/models.py +++ b/toardb/generic/models.py @@ -6,18 +6,35 @@ from toardb.base import Base # controlled vocabulary # Role Status -RS_enum = Table("rs_vocabulary", +RS_enum_table = Table("rs_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see stationmeta/models.py): +from collections import namedtuple +Enumdict=namedtuple("Dict",["value","string","display_str"]) +RS_enum = ( + Enumdict(0, 'active', 'active'), + Enumdict(1, 'inactive', 'inactive'), + Enumdict(2, 'unknown', 'unknown') + ) # Role Codes -RC_enum = Table("rc_vocabulary", +RC_enum_table = Table("rc_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see stationmeta/models.py): +RC_enum = ( + Enumdict(0, 'PointOfContact', 'point of contact'), + Enumdict(1, 'PrincipalInvestigator', 'principal investigator'), + Enumdict(2, 'Originator', 'originator'), + Enumdict(3, 'Contributor', 'contributor'), + Enumdict(4, 'Collaborator', 'collaborator'), + Enumdict(5, 'ResourceProvider', 'resource provider') + ) diff --git a/toardb/stationmeta/crud.py b/toardb/stationmeta/crud.py index 122939bbbf994ef0179d4bcda69a72e9ecf9ea94..47cce8b30ffd82527d0413133ddf0d6df063adda 100644 --- a/toardb/stationmeta/crud.py +++ b/toardb/stationmeta/crud.py @@ -14,7 +14,7 @@ from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder from . import models from .models import stationmeta_core_stationmeta_roles_table, stationmeta_core_stationmeta_annotations_table, \ - CZ_enum + CZ_enum, CV_enum, ST_enum, TA_enum from toardb.generic.models import RS_enum, RC_enum from .schemas import get_coordinates_from_geom, get_geom_from_coordinates, StationmetaCreate, Coordinates from pydantic import ValidationError @@ -92,6 +92,9 @@ def create_stationmeta(db: Session, stationmeta: StationmetaCreate): # but return from this method gives: "additional_metadata": {} # ==> there is a mismatch between model(JSONB) and schema(JSON) db_stationmeta.additional_metadata = str(db_stationmeta.additional_metadata) + db_stationmeta.coordinate_validation_status = get_value_from_str(CV_enum,db_stationmeta.coordinate_validation_status) + db_stationmeta.type_of_environment = get_value_from_str(ST_enum,db_stationmeta.type_of_environment) + db_stationmeta.type_of_area = get_value_from_str(TA_enum,db_stationmeta.type_of_area) db.add(db_stationmeta) result = db.commit() db.refresh(db_stationmeta) @@ -101,8 +104,8 @@ def create_stationmeta(db: Session, stationmeta: StationmetaCreate): if roles_data: for r in roles_data: db_role = models.StationmetaRole(**r) - db_role.role = get_value_from_str(db,RC_enum,db_role.role) - db_role.status = get_value_from_str(db,RS_enum,db_role.status) + db_role.role = get_value_from_str(RC_enum,db_role.role) + db_role.status = get_value_from_str(RS_enum,db_role.status) # check whether role is already present in database db_object = get_unique_stationmeta_role(db, db_role.role, db_role.person_id, db_role.status) if db_object: diff --git a/toardb/stationmeta/models.py b/toardb/stationmeta/models.py index 8110560521db69836878d930833dd7a6b793f25c..a0b8409923a1d3341c457f1671be8fb6b9762800 100644 --- a/toardb/stationmeta/models.py +++ b/toardb/stationmeta/models.py @@ -12,66 +12,176 @@ from toardb.base import Base # controlled vocabulary # Station Climatic Zone -CZ_enum = Table("cz_vocabulary", +CZ_enum_table = Table("cz_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) + +# After loosing one day without solution, the following workaround is introduced! +from collections import namedtuple +Enumdict=namedtuple("Dict",["value","string","display_str"]) +#def __get_enum_dict(table: Table, db: Session = Depends(get_db)): +# This is not working (whereever I put it!) -- I have to investigate, how to do testing +# (and using tables in my testing environment!)" +# AND (EVEN MORE IMPORTANT): +# How to load enum_dicts once?! +# This is important to be able to use the enum_dicts within the validator of the pydantic model +# The following code is just a workaround: +CZ_enum = ( + Enumdict(-1, 'Undefined', 'undefined'), + Enumdict( 0, 'Unclassified', 'unclassified'), + Enumdict( 1, 'WarmTemperateMoist', 'warm temperate moist'), + Enumdict( 2, 'WarmTemperateDry', 'warm temperate dry'), + Enumdict( 3, 'CoolTemperateMoist', 'cool temperate moist'), + Enumdict( 4, 'CoolTemperateDry', 'cool temperate dry'), + Enumdict( 5, 'PolarMoist', 'polar moist'), + Enumdict( 6, 'PolarDry', 'polar dry'), + Enumdict( 7, 'BorealMoist', 'boreal moist'), + Enumdict( 8, 'BorealDry', 'boreal dry'), + Enumdict( 9, 'TropicalMontane', 'tropical montane'), + Enumdict(10, 'TropicalWet', 'tropical wet'), + Enumdict(11, 'TropicalMoist', 'tropical moist'), + Enumdict(12, 'TropicalDry', 'tropical dry') + ) + + # Station Coordinate Validity -CV_enum = Table("cv_vocabulary", +CV_enum_table = Table("cv_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +CV_enum = ( + Enumdict(0, 'NotChecked', 'not checked'), + Enumdict(1, 'Verified', 'verified'), + Enumdict(2, 'Plausible', 'plausible'), + Enumdict(3, 'Doubtful', 'doubtful'), + Enumdict(4, 'Unverifyable', 'not verifyable') + ) # Station Types -ST_enum = Table("st_vocabulary", +ST_enum_table = Table("st_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +ST_enum = ( + Enumdict(0, 'Unknown', 'unknown'), + Enumdict(1, 'Background', 'background'), + Enumdict(2, 'Traffic', 'traffic'), + Enumdict(3, 'Industrial', 'industrial'), + Enumdict(4, 'Other', 'other') + ) # Station Types of Area -TA_enum = Table("ta_vocabulary", +TA_enum_table = Table("ta_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +TA_enum = ( + Enumdict(0, 'Unknown', 'unknown'), + Enumdict(1, 'Urban', 'urban'), + Enumdict(2, 'Suburban', 'suburban'), + Enumdict(3, 'Rural', 'rural'), + Enumdict(4, 'Remote', 'remote') + ) # Station TOAR Categories -TC_enum = Table("tc_vocabulary", +TC_enum_table = Table("tc_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +TC_enum = ( + Enumdict(-1, 'Unknown', 'unknown'), + Enumdict( 0, 'Unclassified', 'unclassified'), + Enumdict( 1, 'RuralLowElevation', 'rural low elevation'), + Enumdict( 2, 'RuralHighElevation', 'rural high elevation'), + Enumdict( 3, 'Urban', 'urban') + ) # Station HTAP Regions (TIER1) -TR_enum = Table("tr_vocabulary", +TR_enum_table = Table("tr_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +TR_enum = ( + Enumdict(-1, 'HTAPTier1Undefined', '-1 (undefined)'), + Enumdict( 1, 'HTAPTier1World', '1 (World)'), + Enumdict( 2, 'HTAPTier1OCN', '2 (OCN Non-arctic/Antarctic Ocean)'), + Enumdict( 3, 'HTAPTier1NAM', '3 (NAM US+Canada (upto 66 N; polar circle))'), + Enumdict( 4, 'HTAPTier1EUR', '4 (EUR Western + Eastern EU+Turkey (upto 66 N polar circle))'), + Enumdict( 5, 'HTAPTier1SAS', '5 (SAS South Asia: India, Nepal, Pakistan, Afghanistan, Bangadesh, Sri Lanka)'), + Enumdict( 6, 'HTAPTier1EAS', '6 (EAS East Asia: China, Korea, Japan)'), + Enumdict( 7, 'HTAPTier1SEA', '7 (SEA South East Asia)'), + Enumdict( 8, 'HTAPTier1PAN', '8 (PAN Pacific, Australia+ New Zealand)'), + Enumdict( 9, 'HTAPTier1NAF', '9 (NAF Northern Africa+Sahara+Sahel)'), + Enumdict(10, 'HTAPTier1SAF', '10 (SAF Sub Saharan/sub Sahel Africa)'), + Enumdict(11, 'HTAPTier1MDE', '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)'), + Enumdict(12, 'HTAPTier1MCA', '12 (MCA Mexico, Central America, Caribbean, Guyanas, Venezuela, Columbia)'), + Enumdict(13, 'HTAPTier1SAM', '13 (SAM S. America)'), + Enumdict(14, 'HTAPTier1RBU', '14 (RBU Russia, Belarussia, Ukraine)'), + Enumdict(15, 'HTAPTier1CAS', '15 (CAS Central Asia)'), + Enumdict(16, 'HTAPTier1NPO', '16 (NPO Arctic Circle (North of 66 N) + Greenland)'), + Enumdict(17, 'HTAPTier1SPO', '17 (SPO Antarctic)') + ) # Station Dominant Landcover Types -DL_enum = Table("dl_vocabulary", +DL_enum_table = Table("dl_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +DL_enum = ( + Enumdict( -1, 'Undefined', '-1 (undefined)'), + Enumdict( 0, 'Water', '0 (Water)'), + Enumdict( 1, 'EGNeedleleaf', '1 (Evergreen Needleleaf forest)'), + Enumdict( 2, 'EGBroadleaf', '2 (Evergreen Broadleaf forest)'), + Enumdict( 3, 'DCNeedleleaf', '3 (Deciduous Needleleaf forest)'), + Enumdict( 4, 'DCBroadleaf', '4 (Deciduous Broadleaf forest)'), + Enumdict( 5, 'MixedForest', '5 (Mixed forest)'), + Enumdict( 6, 'ClosedShrublands', '6 (Closed shrublands)'), + Enumdict( 7, 'OpenShrublands', '7 (Open shrublands)'), + Enumdict( 8, 'WoodySavannas', '8 (Woody savannas)'), + Enumdict( 9, 'Savannas', '9 (Savannas)'), + Enumdict( 10, 'Grasslands', '10 (Grasslands)'), + Enumdict( 11, 'Wetlands', '11 (Permanent wetlands)'), + Enumdict( 12, 'Croplands', '12 (Croplands)'), + Enumdict( 13, 'Urban', '13 (Urban and built-up)'), + Enumdict( 14, 'Mosaic', '14 (Cropland/Natural vegetation mosaic)'), + Enumdict( 15, 'Snow', '15 (Snow and ice)'), + Enumdict( 16, 'Barren', '16 (Barren or sparsely vegetated)'), + Enumdict(255, 'Fill', '255 (Fill Value/Unclassified)') + ) # Result Types -RT_enum = Table("rt_vocabulary", +RT_enum_table = Table("rt_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see above): +RT_enum = ( + Enumdict(0, 'String', 'str'), + Enumdict(1, 'Integer', 'int'), + Enumdict(2, 'Float', 'float') + ) diff --git a/toardb/stationmeta/schemas.py b/toardb/stationmeta/schemas.py index b8d72e3e2699461d67a2e1d3123b3b501f3377f0..f43df11becaffd526c448eaeda2276a3080734f7 100644 --- a/toardb/stationmeta/schemas.py +++ b/toardb/stationmeta/schemas.py @@ -5,10 +5,13 @@ Pydantic schemas for TOAR database """ from typing import List -from pydantic import BaseModel, Field, BaseConfig, Json +from pydantic import BaseModel, Field, BaseConfig, Json, validator from geoalchemy2 import WKTElement from geoalchemy2.shape import to_shape import datetime as dt +from .models import CZ_enum, CV_enum, ST_enum, TA_enum, TC_enum, \ + TR_enum, DL_enum +from toardb.generic.models import RC_enum, RS_enum # the following class was taken from: @@ -27,10 +30,10 @@ class StationmetaCoreBase(BaseModel): coordinates: Coordinates country: str state: str - coordinate_validation_status: int + coordinate_validation_status: str coordinate_validation_date: dt.datetime - type_of_environment: int - type_of_area: int + type_of_environment: str + type_of_area: str category: str timezone: str additional_metadata: Json @@ -39,10 +42,43 @@ class StationmetaCoreBase(BaseModel): class Config(BaseConfig): arbitrary_types_allowed = True + @validator('coordinate_validation_status') + def check_coordinate_validation_status(cls, v): + return tuple(filter(lambda x: x.value == int(v), CV_enum))[0].string + + @validator('type_of_environment') + def check_type_of_environment(cls, v): + return tuple(filter(lambda x: x.value == int(v), ST_enum))[0].string + + @validator('type_of_area') + def check_type_of_area(cls, v): + return tuple(filter(lambda x: x.value == int(v), TA_enum))[0].string + class StationmetaCoreCreate(StationmetaCoreBase): pass + @validator('coordinate_validation_status') + def check_coordinate_validation_status(cls, v): + if tuple(filter(lambda x: x.string == v, CV_enum)): + return v + else: + raise ValueError(f"coordinate validation status not known: {v}") + + @validator('type_of_environment') + def check_type_of_environment(cls, v): + if tuple(filter(lambda x: x.string == v, ST_enum)): + return v + else: + raise ValueError(f"type of environment not known: {v}") + + @validator('type_of_area') + def check_type_of_area(cls, v): + if tuple(filter(lambda x: x.string == v, TA_enum)): + return v + else: + raise ValueError(f"type of area not known: {v}") + class StationmetaCore(StationmetaCoreBase): id: int @@ -159,18 +195,61 @@ class StationmetaGlobalBase(BaseModel): rice_production_year2000: float edgar_htap_v2_nox_emissions_year2010: float omi_no2_column_years2011to2015: float - htap_region_tier1: int + htap_region_tier1: str etopo_alt: float etopo_min_alt_5km: float etopo_relative_alt: float - dominant_landcover_year2012: int - toar1_category: int + dominant_landcover_year2012: str + toar1_category: str station_id: int + @validator('climatic_zone') + def check_climatic_zone(cls, v): + return tuple(filter(lambda x: x.value == int(v), CZ_enum))[0].string + + @validator('toar1_category') + def check_toar1_category(cls, v): + return tuple(filter(lambda x: x.value == int(v), TC_enum))[0].string + + @validator('htap_region_tier1') + def check_htap_region_tier1(cls, v): + return tuple(filter(lambda x: x.value == int(v), TR_enum))[0].string + + @validator('dominant_landcover_year2012') + def check_dominant_landcover_year2012(cls, v): + return tuple(filter(lambda x: x.value == int(v), DL_enum))[0].string class StationmetaGlobalCreate(StationmetaGlobalBase): pass + @validator('climatic_zone') + def check_climatic_zone(cls, v): + if tuple(filter(lambda x: x.string == v, CZ_enum)): + return v + else: + raise ValueError(f"climatic zone not known: {v}") + + @validator('toar1_category') + def check_toar1_category(cls, v): + if tuple(filter(lambda x: x.string == v, TC_enum)): + return v + else: + raise ValueError(f"TOAR1 category not known: {v}") + + @validator('htap_region_tier1') + def check_htap_region_tier1(cls, v): + if tuple(filter(lambda x: x.string == v, TR_enum)): + return v + else: + raise ValueError(f"HTAP region TIER1 not known: {v}") + + @validator('dominant_landcover_year2012') + def check_dominant_landcover_year2012(cls, v): + if tuple(filter(lambda x: x.string == v, DL_enum)): + return v + else: + raise ValueError(f"dominant landcover (year2012) not known: {v}") + class StationmetaGlobal(StationmetaGlobalBase): id: int @@ -239,10 +318,32 @@ class StationmetaRoleBase(BaseModel): status: str person_id: int + @validator('role') + def check_role(cls, v): + return tuple(filter(lambda x: x.value == int(v), RC_enum))[0].string + + @validator('status') + def check_status(cls, v): + return tuple(filter(lambda x: x.value == int(v), RS_enum))[0].string + class StationmetaRoleCreate(StationmetaRoleBase): pass + @validator('role') + def check_role(cls, v): + if tuple(filter(lambda x: x.string == v, RC_enum)): + return v + else: + raise ValueError(f"role code not known: {v}") + + @validator('status') + def check_status(cls, v): + if tuple(filter(lambda x: x.string == v, RS_enum)): + return v + else: + raise ValueError(f"role status not known: {v}") + class StationmetaRole(StationmetaRoleBase): id: int @@ -264,7 +365,8 @@ class StationmetaBase(StationmetaCoreBase): class Config: orm_mode = True -class StationmetaCreate(StationmetaCoreBase): + +class StationmetaCreate(StationmetaCoreCreate): roles: List[StationmetaRoleBase] = None annotations: List[StationmetaAnnotation] = None aux_images: List[StationmetaAuxImage] = None diff --git a/toardb/stationmeta/stationmeta.py b/toardb/stationmeta/stationmeta.py index b831d8a4d90daa395f9909b415a862d96d86cf15..0ad7251e9ae7e206146b9f0c8c1c320564ec0cf5 100644 --- a/toardb/stationmeta/stationmeta.py +++ b/toardb/stationmeta/stationmeta.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Body from sqlalchemy.orm import Session, sessionmaker from . import crud, schemas from sqlalchemy import create_engine -from toardb.utils.database import DATABASE_URL, get_db +from toardb.utils.database import get_db router = APIRouter() @@ -49,7 +49,6 @@ def get_stationmeta(station_code: str, db: Session = Depends(get_db)): # - ... @router.post('/stationmeta/', response_model=schemas.Stationmeta) -#curl -X POST -H "Content-Type:application/json" -d '{"stationmeta_core": {"codes":["ttt3","ttt4"],"name":"Test_China","coordinates":{"lat":36.256,"lng":17.106,"alt":1534.0},"country":"China","state":"Shandong Sheng","coordinate_validation_status":0,"coordinate_validation_date":"2020-03-11T12:22:18.047974+01:00","type_of_environment":0,"type_of_area":0,"category":"","timezone":"", "coordinate_validator_id": 1, "additional_metadata":"{}"}}' "http://127.0.0.1:8000/stationmeta_core/" # The following command was not working as long as the upload via Body was defined. # See bug report: https://github.com/tiangolo/fastapi/issues/300 # (Although this seems to be fixed in the meantime, it is not working in my FastAPI version.) diff --git a/toardb/stationmeta/test_stationmeta.py b/toardb/stationmeta/test_stationmeta.py index 7262612e189550c50e4a5a7575d6acfa2103677f..6ee24caa9b9bceaa3f4f69026844c5f9335cb628 100644 --- a/toardb/stationmeta/test_stationmeta.py +++ b/toardb/stationmeta/test_stationmeta.py @@ -74,6 +74,7 @@ class TestApps: db.add(new_organisation) db.commit() db.refresh(new_organisation) + # I also need to upload tests with nested data!!! infilename = "toardb/stationmeta/fixtures/stationmeta_core.json" with open(infilename) as f: metajson=json.load(f) @@ -92,29 +93,29 @@ class TestApps: db.refresh(new_stationmeta_core) - def test_get_stationmeta(self, client, db): - response = client.get("/stationmeta_core/") - expected_status_code = 200 - assert response.status_code == expected_status_code - expected_resp = [{'id': 1, 'codes': ['China11'], 'name': 'Mount Tai', - 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, - 'country': 'China', 'state': 'Shandong Sheng', - 'coordinate_validation_status': 0, 'coordinate_validation_date': '2020-02-28T12:27:03.746260+01:00', - 'type_of_environment': 0, 'type_of_area': 0, 'category': '', 'timezone': '', - 'additional_metadata': {}, 'coordinate_validator_id': 1}, - {'id': 2, 'codes': ['SDZ54421'], 'name': 'Shangdianzi', - 'coordinates': {'lat': 40.65, 'lng': 117.106, 'alt': 293.9}, - 'country': 'China', 'state': 'Beijing Shi', - 'coordinate_validation_status': 0, 'coordinate_validation_date': '2020-02-28T12:27:03.746260+01:00', - 'type_of_environment': 0, 'type_of_area': 0, 'category': '', 'timezone': '', - 'additional_metadata': {}, 'coordinate_validator_id': 1}, - {'id': 3, 'codes': ['China_test8'], 'name': 'Test_China', - 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, - 'country': 'China', 'state': 'Shandong Sheng', - 'coordinate_validation_status': 0, 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', - 'type_of_environment': 0, 'type_of_area': 0, 'category': '', 'timezone': '', - 'additional_metadata': {}, 'coordinate_validator_id': 1}] - assert response.json() == expected_resp +## def test_get_stationmeta(self, client, db): +## response = client.get("/stationmeta_core/") +## expected_status_code = 200 +## assert response.status_code == expected_status_code +## expected_resp = [{'id': 1, 'codes': ['China11'], 'name': 'Mount Tai', +## 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, +## 'country': 'China', 'state': 'Shandong Sheng', +## 'coordinate_validation_status': 0, 'coordinate_validation_date': '2020-02-28T12:27:03.746260+01:00', +## 'type_of_environment': 0, 'type_of_area': 0, 'category': '', 'timezone': '', +## 'additional_metadata': {}, 'coordinate_validator_id': 1}, +## {'id': 2, 'codes': ['SDZ54421'], 'name': 'Shangdianzi', +## 'coordinates': {'lat': 40.65, 'lng': 117.106, 'alt': 293.9}, +## 'country': 'China', 'state': 'Beijing Shi', +## 'coordinate_validation_status': 0, 'coordinate_validation_date': '2020-02-28T12:27:03.746260+01:00', +## 'type_of_environment': 0, 'type_of_area': 0, 'category': '', 'timezone': '', +## 'additional_metadata': {}, 'coordinate_validator_id': 1}, +## {'id': 3, 'codes': ['China_test8'], 'name': 'Test_China', +## 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, +## 'country': 'China', 'state': 'Shandong Sheng', +## 'coordinate_validation_status': 0, 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', +## 'type_of_environment': 0, 'type_of_area': 0, 'category': '', 'timezone': '', +## 'additional_metadata': {}, 'coordinate_validator_id': 1}] +## assert response.json() == expected_resp # def test_get_all(self): @@ -131,19 +132,34 @@ class TestApps: # assert response.json() == expected_resp - def test_get_special(self, client, db): + def test_get_special(self, client, db): # core! response = client.get("/stationmeta_core/China_test8") expected_status_code = 200 assert response.status_code == expected_status_code expected_resp = {'id': 3, 'codes': ['China_test8'], 'name': 'Test_China', 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, - 'country': 'China', 'state': 'Shandong Sheng', 'coordinate_validation_status': 0, - 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', 'type_of_environment': 0, - 'type_of_area': 0, 'category': '', 'timezone': '', + 'country': 'China', 'state': 'Shandong Sheng', 'coordinate_validation_status': 'NotChecked', + 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', 'type_of_environment': 'Unknown', + 'type_of_area': 'Unknown', 'category': '', 'timezone': '', 'additional_metadata': {}, 'coordinate_validator_id': 1} assert response.json() == expected_resp + def test_get_special_nested(self, client, db): # nested! + response = client.get("/stationmeta/China_test8") + expected_status_code = 200 + assert response.status_code == expected_status_code + expected_resp = {'id': 3, 'codes': ['China_test8'], 'name': 'Test_China', + 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, + 'country': 'China', 'state': 'Shandong Sheng', 'coordinate_validation_status': 'NotChecked', + 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', 'type_of_environment': 'Unknown', + 'type_of_area': 'Unknown', 'category': '', 'timezone': '', + 'additional_metadata': {}, 'coordinate_validator_id': 1, + 'roles': [], 'annotations': [], 'aux_images': [], 'aux_docs': [], + 'aux_urls': [], 'globalmeta': None, 'globalservice': None} + assert response.json() == expected_resp + + # def test_insert_new_without_credits(self): #? response = client.post("/stationmeta_core/") # expected_status_code=401 @@ -162,13 +178,23 @@ class TestApps: def test_insert_new(self, client, db): +# response = client.post("/stationmeta/", +# json={"stationmeta": +# {"codes":["ttt3","ttt4"], +# "name":"Test_China","coordinates":{"lat":36.256,"lng":117.106,"alt":1534.0}, +# "country":"China","state":"Shandong Sheng","coordinate_validation_status":"NotChecked", +# "coordinate_validation_date":"2020-03-11T12:22:18.047974+01:00", +# "type_of_environment":"Unknown","type_of_area":"Unknown","category":"","timezone":"", +# "coordinate_validator_id": 1, "additional_metadata":"{}"} +# } +# ) response = client.post("/stationmeta/", json={"stationmeta": {"codes":["ttt3","ttt4"], "name":"Test_China","coordinates":{"lat":36.256,"lng":117.106,"alt":1534.0}, - "country":"China","state":"Shandong Sheng","coordinate_validation_status":0, + "country":"China","state":"Shandong Sheng","coordinate_validation_status":"NotChecked", "coordinate_validation_date":"2020-03-11T12:22:18.047974+01:00", - "type_of_environment":0,"type_of_area":0,"category":"","timezone":"", + "type_of_environment":"Unknown","type_of_area":"Unknown","category":"","timezone":"", "coordinate_validator_id": 1, "additional_metadata":"{}"} } ) @@ -176,9 +202,9 @@ class TestApps: assert response.status_code == expected_status_code expected_resp = {'id': 4, 'codes': ['ttt3','ttt4'], 'name': 'Test_China', 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, - 'country': 'China', 'state': 'Shandong Sheng', 'coordinate_validation_status': 0, - 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', 'type_of_environment': 0, - 'type_of_area': 0, 'category': '', 'timezone': '', + 'country': 'China', 'state': 'Shandong Sheng', 'coordinate_validation_status': 'NotChecked', + 'coordinate_validation_date': '2020-03-11T12:22:18.047974+01:00', 'type_of_environment': 'Unknown', + 'type_of_area': 'Unknown', 'category': '', 'timezone': '', 'additional_metadata': {}, 'coordinate_validator_id': 1, 'roles': [], 'annotations': [], 'aux_images': [], 'aux_docs': [], 'aux_urls': [], 'globalmeta': None, 'globalservice': None} @@ -190,9 +216,9 @@ class TestApps: json={"stationmeta": {"codes":["China11"], "name":"Test_China","coordinates":{"lat":36.256,"lng":117.106,"alt":1534.0}, - "country":"China","state":"Shandong Sheng","coordinate_validation_status":0, + "country":"China","state":"Shandong Sheng","coordinate_validation_status":"NotChecked", "coordinate_validation_date":"2020-03-11T12:22:18.047974+01:00", - "type_of_environment":0,"type_of_area":0,"category":"","timezone":"", + "type_of_environment":"Unknown","type_of_area":"Unknown","category":"","timezone":"", "coordinate_validator_id": 1, "additional_metadata":"{}"} } ) diff --git a/toardb/test_base.py b/toardb/test_base.py index 9046fbcd2fff54d41e49f307448f37d80a766b07..8a6923cde5cc233f78a727132412b25acf8f2cdf 100644 --- a/toardb/test_base.py +++ b/toardb/test_base.py @@ -60,12 +60,9 @@ def test_db_session(): yield session # Drop all data after each test for tbl in reversed(Base.metadata.sorted_tables): - _db_conn.execute(tbl.delete()) - # all tables from "toar_controlled_vocabulary" got lost by the above command! - fake_conn = _db_conn.raw_connection() - fake_cur = fake_conn.cursor() - fake_cur.execute("DROP EXTENSION toar_controlled_vocabulary") - fake_conn.commit() + # otherwiese all tables from "toar_controlled_vocabulary" will get lost! + if not tbl.name.endswith("_vocabulary"): + _db_conn.execute(tbl.delete()) # put back the connection to the connection pool session.close() diff --git a/toardb/timeseries/crud.py b/toardb/timeseries/crud.py index ddd8dff8e940b46f7171d1bdcf514f45e9845f1f..55624b3329ab62ddb702dd732eba29916421e9d1 100644 --- a/toardb/timeseries/crud.py +++ b/toardb/timeseries/crud.py @@ -21,10 +21,6 @@ def get_timeseries(db: Session, timeseries_id: int): # there is a mismatch with additional_metadata if db_object: db_object.additional_metadata = str(db_object.additional_metadata) - db_object.access_rights = get_str_from_value(db,DA_enum,db_object.access_rights) - db_object.sampling_frequency = get_str_from_value(db,SF_enum,db_object.sampling_frequency) - db_object.aggregation = get_str_from_value(db,AT_enum,db_object.aggregation) - db_object.source = get_str_from_value(db,DS_enum,db_object.source) return db_object @@ -33,10 +29,6 @@ def get_all_timeseries(db: Session, skip : int = 0, limit: int = None): for db_object in db_objects: # there is a mismatch with additional_metadata db_object.additional_metadata = str(db_object.additional_metadata) - db_object.access_rights = get_str_from_value(db,DA_enum,db_object.access_rights) - db_object.sampling_frequency = get_str_from_value(db,SF_enum,db_object.sampling_frequency) - db_object.aggregation = get_str_from_value(db,AT_enum,db_object.aggregation) - db_object.source = get_str_from_value(db,DS_enum,db_object.source) return db_objects @@ -48,10 +40,6 @@ def get_timeseries_by_unique_constraints(db: Session, station_id: int, variable_ # there is a mismatch with additional_metadata if db_object: db_object.additional_metadata = str(db_object.additional_metadata) - db_object.access_rights = get_str_from_value(db,DA_enum,db_object.access_rights) - db_object.sampling_frequency = get_str_from_value(db,SF_enum,db_object.sampling_frequency) - db_object.aggregation = get_str_from_value(db,AT_enum,db_object.aggregation) - db_object.source = get_str_from_value(db,DS_enum,db_object.source) return db_object @@ -94,10 +82,10 @@ def create_timeseries(db: Session, timeseries: TimeseriesCreate): annotations_data = timeseries_dict.pop('annotations', None) programmes_data = timeseries_dict.pop('programmes', None) db_timeseries = models.Timeseries(**timeseries_dict) - db_timeseries.access_rights = get_value_from_str(db,DA_enum,db_timeseries.access_rights) - db_timeseries.sampling_frequency = get_value_from_str(db,SF_enum,db_timeseries.sampling_frequency) - db_timeseries.aggregation = get_value_from_str(db,AT_enum,db_timeseries.aggregation) - db_timeseries.source = get_value_from_str(db,DS_enum,db_timeseries.source) + db_timeseries.access_rights = get_value_from_str(DA_enum,db_timeseries.access_rights) + db_timeseries.sampling_frequency = get_value_from_str(SF_enum,db_timeseries.sampling_frequency) + db_timeseries.aggregation = get_value_from_str(AT_enum,db_timeseries.aggregation) + db_timeseries.source = get_value_from_str(DS_enum,db_timeseries.source) db.add(db_timeseries) result = db.commit() db.refresh(db_timeseries) @@ -107,8 +95,8 @@ def create_timeseries(db: Session, timeseries: TimeseriesCreate): if roles_data: for r in roles_data: db_role = models.TimeseriesRole(**r) - db_role.role = get_value_from_str(db,RC_enum,db_role.role) - db_role.status = get_value_from_str(db,RS_enum,db_role.status) + db_role.role = get_value_from_str(RC_enum,db_role.role) + db_role.status = get_value_from_str(RS_enum,db_role.status) # check whether role is already present in database db_object = get_unique_timeseries_role(db, db_role.role, db_role.person_id, db_role.status) if db_object: @@ -154,8 +142,4 @@ def create_timeseries(db: Session, timeseries: TimeseriesCreate): # in upload command, we have now: "additional_metadata": "{}" # but return from this method gives (=database): "additional_metadata": {} db_timeseries.additional_metadata = str(db_timeseries.additional_metadata) - db_timeseries.access_rights = get_str_from_value(db,DA_enum,db_timeseries.access_rights) - db_timeseries.sampling_frequency = get_str_from_value(db,SF_enum,db_timeseries.sampling_frequency) - db_timeseries.aggregation = get_str_from_value(db,AT_enum,db_timeseries.aggregation) - db_timeseries.source = get_str_from_value(db,DS_enum,db_timeseries.source) return db_timeseries diff --git a/toardb/timeseries/models.py b/toardb/timeseries/models.py index 2e9bf7ed3196c2ba88428b66ed429d4a5da32529..daa5c3832d8517687eb7863a2c28786057ded07a 100644 --- a/toardb/timeseries/models.py +++ b/toardb/timeseries/models.py @@ -10,34 +10,70 @@ from sqlalchemy.ext.declarative import declarative_base # controlled vocabulary # Data Access Rights -DA_enum = Table("da_vocabulary", +DA_enum_table = Table("da_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see stationmeta/models.py): +from collections import namedtuple +Enumdict=namedtuple("Dict",["value","string","display_str"]) +DA_enum = ( + Enumdict(0, 'ByAttribution', 'by attribution'), + Enumdict(1, 'ShareAlike', 'share alike'), + Enumdict(2, 'Restricted', 'restricted') + ) # Sampling Frequencies -SF_enum = Table("sf_vocabulary", +SF_enum_table = Table("sf_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see stationmeta/models.py): +SF_enum = ( + Enumdict(0, 'Hourly', 'hourly'), + Enumdict(1, 'ThreeHourly', '3-hourly'), + Enumdict(2, 'SixHourly', '6-hourly'), + Enumdict(3, 'Daily', 'daily'), + Enumdict(4, 'Weekly', 'weekly'), + Enumdict(5, 'Monthly', 'monthly'), + Enumdict(6, 'Yearly', 'yearly'), + Enumdict(7, 'Irregular', 'irregular data samples of constant length'), + Enumdict(8, 'Irregular2', 'irregular data samples of varying length') + ) + # Aggregation Types -AT_enum = Table("at_vocabulary", +AT_enum_table = Table("at_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see stationmeta/models.py): +AT_enum = ( + Enumdict(0, 'Mean', 'mean'), + Enumdict(1, 'Mean1Of2', 'mean of two values'), + Enumdict(2, 'Mean1OfWeek', 'weekly mean'), + Enumdict(3, 'Mean4Samples', 'mean out of 4 samples'), + Enumdict(4, 'MeanMonth', 'monthly mean'), + Enumdict(5, 'None', 'none'), + Enumdict(6, 'Unknown', 'unknown') + ) # Data Sources -DS_enum = Table("ds_vocabulary", +DS_enum_table = Table("ds_vocabulary", Base.metadata, Column("enum_val", Integer, primary_key=True), Column("enum_str", String), Column("enum_display_str", String) ) +# The following code is just a workaround (see stationmeta/models.py): +DS_enum = ( + Enumdict(0, 'Model', 'model'), + Enumdict(1, 'Measurement', 'measurement') + ) diff --git a/toardb/timeseries/schemas.py b/toardb/timeseries/schemas.py index 40db3f88bc4f496b01c6d9487919c885ff8f62cd..583be3a2ce0110433ae03979baa4fe15baa88a24 100644 --- a/toardb/timeseries/schemas.py +++ b/toardb/timeseries/schemas.py @@ -6,8 +6,10 @@ Pydantic schemas for TOAR database from typing import List -from pydantic import BaseModel, Json +from pydantic import BaseModel, Json, validator import datetime as dt +from toardb.generic.models import RS_enum, RC_enum +from .models import DA_enum, SF_enum, AT_enum, DS_enum # ======== Timeseries ========= @@ -29,10 +31,55 @@ class TimeseriesCoreBase(BaseModel): variable_id: int additional_metadata: Json + @validator('access_rights') + def check_access_rights(cls, v): + return tuple(filter(lambda x: x.value == int(v), DA_enum))[0].string + + @validator('sampling_frequency') + def check_sampling_frequency(cls, v): + return tuple(filter(lambda x: x.value == int(v), SF_enum))[0].string + + @validator('aggregation') + def check_aggregation(cls, v): + return tuple(filter(lambda x: x.value == int(v), AT_enum))[0].string + + @validator('source') + def check_source(cls, v): + return tuple(filter(lambda x: x.value == int(v), DS_enum))[0].string + class TimeseriesCoreCreate(TimeseriesCoreBase): pass + @validator('access_rights') + def check_access_rights(cls, v): + if tuple(filter(lambda x: x.string == v, DA_enum)): + return v + else: + raise ValueError(f"data access rights not known: {v}") + + @validator('sampling_frequency') + def check_sampling_frequency(cls, v): + if tuple(filter(lambda x: x.string == v, SF_enum)): + return v + else: + raise ValueError(f"sampling frequency not known: {v}") + + @validator('aggregation') + def check_aggregation(cls, v): + if tuple(filter(lambda x: x.string == v, AT_enum)): + return v + else: + raise ValueError(f"aggregation type not known: {v}") + + @validator('source') + def check_source(cls, v): + if tuple(filter(lambda x: x.string == v, DS_enum)): + return v + else: + raise ValueError(f"data source not known: {v}") + + class TimeseriesCore(TimeseriesCoreBase): id: int @@ -48,9 +95,32 @@ class TimeseriesRoleBase(BaseModel): status: str person_id: int + @validator('role') + def check_role(cls, v): + return tuple(filter(lambda x: x.value == int(v), RC_enum))[0].string + + @validator('status') + def check_status(cls, v): + return tuple(filter(lambda x: x.value == int(v), RS_enum))[0].string + + class TimeseriesRoleCreate(TimeseriesRoleBase): pass + @validator('role') + def check_role(cls, v): + if tuple(filter(lambda x: x.string == v, RC_enum)): + return v + else: + raise ValueError(f"role code not known: {v}") + + @validator('status') + def check_status(cls, v): + if tuple(filter(lambda x: x.string == v, RS_enum)): + return v + else: + raise ValueError(f"role status not known: {v}") + class TimeseriesRole(TimeseriesRoleBase): id: int @@ -110,8 +180,8 @@ class TimeseriesBase(TimeseriesCoreBase): class Config: orm_mode = True -class TimeseriesCreate(TimeseriesCoreBase): - roles: List[TimeseriesRoleBase] = None +class TimeseriesCreate(TimeseriesCoreCreate): + roles: List[TimeseriesRoleCreate] = None annotations: List[TimeseriesAnnotation] = None programmes: List[TimeseriesProgramme] = None diff --git a/toardb/timeseries/test_timeseries.py b/toardb/timeseries/test_timeseries.py index 82e7d250c3515e14ad7f69b142c89fc399e56cc3..39a10d7d7748390f4b3d6413f45b880946f779d5 100644 --- a/toardb/timeseries/test_timeseries.py +++ b/toardb/timeseries/test_timeseries.py @@ -31,10 +31,6 @@ class TestApps: # id_seq will not be reset automatically between tests! fake_conn = _db_conn.raw_connection() fake_cur = fake_conn.cursor() - # all tables of "toar_controlled_vocabulary get lost inbetween tests - # ==> create the extension again! - fake_cur.execute("CREATE EXTENSION IF NOT EXISTS toar_controlled_vocabulary") - fake_conn.commit() fake_cur.execute("ALTER SEQUENCE auth_user_id_seq RESTART WITH 1") fake_conn.commit() fake_cur.execute("ALTER SEQUENCE variables_id_seq RESTART WITH 1") @@ -226,19 +222,28 @@ class TestApps: 'date_added': '2020-05-15T15:30:00+02:00', 'date_modified': '2020-05-16T09:30:00+02:00', 'station_id': 2, 'variable_id': 7, 'additional_metadata':{}, - 'roles': [{'id': 1, 'person_id': 3, 'role': '0', 'status': '0'}, {'id': 2, 'person_id': 1, 'role': '2', 'status': '0'}], + 'roles': [{'id': 1, 'person_id': 3, 'role': 'PointOfContact', 'status': 'active'}, {'id': 2, 'person_id': 1, 'role': 'Originator', 'status': 'active'}], 'annotations': [], 'programmes': []} assert response.json() == expected_resp -## def test_insert_duplicate(self, client, db): -## response = client.post("/timeseries/", -## json={"timeseries": -## {"id": 4, -## "additional_metadata":"{}"} -## } -## ) -## expected_status_code = 400 -## assert response.status_code == expected_status_code -## expected_resp = {'detail': 'Timeseries already registered.'} -## assert response.json() == expected_resp + def test_insert_duplicate(self, client, db): + response = client.post("/timeseries/", + json={"timeseries": + {"label": "CMA", "order": 1, "access_rights": "ByAttribution", + "sampling_frequency": "Hourly", "aggregation": "Mean", "source": "Measurement", + "data_start_date": "2003-09-07T15:30:00+02:00", + "data_end_date": "2016-12-31T14:30:00+01:00", + "measurement_method": "UV absorption", "sampling_height": 7.0, + "date_added": "2020-05-15T15:30:00+02:00", "date_modified": "2020-05-16T09:30:00+02:00", + "station_id": 2, "variable_id": 7, + "additional_metadata":"{}", + "roles": [{"role": "PointOfContact", "person_id": 3, "status": "active"}, + {"role": "Originator", "person_id": 1, "status": "active"}] + } + } + ) + expected_status_code = 400 + assert response.status_code == expected_status_code + expected_resp = {'detail': 'Timeseries already registered.'} + assert response.json() == expected_resp diff --git a/toardb/timeseries/timeseries.py b/toardb/timeseries/timeseries.py index 734be478849ff1f0c302bc5a6008e25c12d2bb4b..9cc731bfb9ab1e452b030822ca1d6bf1c85078d1 100644 --- a/toardb/timeseries/timeseries.py +++ b/toardb/timeseries/timeseries.py @@ -17,15 +17,7 @@ router = APIRouter() #get all entries of table timeseries @router.get('/timeseries/', response_model=List[schemas.Timeseries]) def get_all_timeseries(skip: int = 0, limit: int = None, db: Session = Depends(get_db)): - db_timeseries = crud.get_all_timeseries(db, skip=skip, limit=limit) - for timeseries in db_timeseries: - if timeseries.roles: - for r in timeseries.roles: - # Attention! Do not change the same object twice! - if type(r.role) is int: - r.role = get_str_from_value(db,RC_enum,r.role) - r.status = get_str_from_value(db, RS_enum, r.status) - return db_timeseries + return crud.get_all_timeseries(db, skip=skip, limit=limit) #get all metadata of one timeseries @router.get('/timeseries/{timeseries_id}', response_model=schemas.Timeseries) @@ -33,10 +25,6 @@ def get_timeseries(timeseries_id: int, db: Session = Depends(get_db)): db_timeseries = crud.get_timeseries(db, timeseries_id=timeseries_id) if db_timeseries is None: raise HTTPException(status_code=404, detail="Timeseries not found.") - if db_timeseries.roles: - for r in db_timeseries.roles: - r.role = get_str_from_value(db,RC_enum,r.role) - r.status = get_str_from_value(db, RS_enum, r.status) return db_timeseries #some more gets to be tested: @@ -51,9 +39,5 @@ def create_timeseries(timeseries: schemas.TimeseriesCreate = Body(..., embed = T if db_timeseries: raise HTTPException(status_code=400, detail="Timeseries already registered.") db_timeseries=crud.create_timeseries(db=db, timeseries=timeseries) - if db_timeseries.roles: - for r in db_timeseries.roles: - r.role = get_str_from_value(db,RC_enum,r.role) - r.status = get_str_from_value(db, RS_enum, r.status) return db_timeseries diff --git a/toardb/utils/utils.py b/toardb/utils/utils.py index b933c05c73f21b1a3aff47bfbf11a14cb70b9dc8..bd90ddd45a9a547dc712d01ca5be4a155f9d911d 100644 --- a/toardb/utils/utils.py +++ b/toardb/utils/utils.py @@ -16,19 +16,19 @@ def __get_enum_dict(db: Session, table: Table): return enum_dict # function to return code for given value -def get_str_from_value(db: Session, table: Table, value: int) -> str: - enum_error = "Invalid " + table.name - enum_dict = __get_enum_dict(db, table) - enum_entry = tuple(filter(lambda x: x.value == value, enum_dict)) - return enum_entry[0].string +#def get_str_from_value(db: Session, table: Table, value: int) -> str: +def get_str_from_value(enum_dict, value) -> str: +# enum_error = "Invalid " + table.name +# enum_dict = __get_enum_dict(db, table) + return tuple(filter(lambda x: x.value == value, enum_dict))[0].string - raise ValueError(enum_error) +# raise ValueError(enum_error) # function to return value for given code -def get_value_from_str(db: Session, table: Table, string: str) -> int: - enum_error = "Invalid " + table.name - enum_dict = __get_enum_dict(db, table) - enum_entry = tuple(filter(lambda x: x.string == string, enum_dict)) - return enum_entry[0].value +#def get_value_from_str(db: Session, table: Table, string: str) -> int: +def get_value_from_str(enum_dict, string) -> int: +# enum_error = "Invalid " + table.name +# enum_dict = __get_enum_dict(db, table) + return tuple(filter(lambda x: x.string == string, enum_dict))[0].value - raise ValueError(enum_error) +# raise ValueError(enum_error)