From cde066447bde0e537707025c49c6cc74f529f9c2 Mon Sep 17 00:00:00 2001 From: cassandwich Date: Thu, 17 Apr 2025 09:51:04 -0400 Subject: [PATCH 1/8] Updated test file and models.py --- app/versions/v1/app.py | 17 +++++-- app/versions/v1/models.py | 94 +++++++++++++++++++++++++-------------- test/conftest.py | 36 +++++++++++++++ 3 files changed, 109 insertions(+), 38 deletions(-) diff --git a/app/versions/v1/app.py b/app/versions/v1/app.py index 8b6329a..1a84cdb 100644 --- a/app/versions/v1/app.py +++ b/app/versions/v1/app.py @@ -15,10 +15,19 @@ async def post_data_request( data_request: Annotated[DataRequest, AfterValidator(DataRequest.model_validate)], session: SessionDep ) -> DataRequestPublic: """Create a new data request and return the newly created data request.""" - session.add(data_request) - session.commit() - session.refresh(data_request) - return data_request + try: + session.add(data_request) + session.commit() + session.refresh(data_request) + return data_request + except Exception as e: + session.rollback() + raise HTTPException(status_code=500, detail="Internal Server Error") + + #session.add(data_request) + #session.commit() + #session.refresh(data_request) + #return data_request @app.patch("/data-publish-request/{request_id}") diff --git a/app/versions/v1/models.py b/app/versions/v1/models.py index 389d1cd..53e215e 100644 --- a/app/versions/v1/models.py +++ b/app/versions/v1/models.py @@ -1,7 +1,11 @@ from datetime import datetime -from pydantic import field_validator +from pydantic import field_validator, model_validator from sqlmodel import Field, SQLModel +from fastapi import FastAPI +from typing import List + +app = FastAPI(version="1") class DataRequestBase(SQLModel): @@ -10,37 +14,44 @@ class DataRequestBase(SQLModel): This object contains field validators. """ + start_date: datetime + end_date: datetime - @field_validator("start_date","end_date", check_fields=False) - def validate_timeperiod(cls, start_date: datetime, end_date: datetime) -> datetime: - """Check that the time periods are correct""" - if end_date < start_date: - raise ValueError("End date can not be earlier than start date") - return start_date, end_date + @model_validator(mode="before") + def validate_timeperiod(cls, values): + start_time = values.__dict__.get("start_date") + end_time = values.__dict__.get("end_date") + try: + if isinstance(start_time, str): + start_time = datetime.fromisoformat(start_time) + if isinstance(end_time, str): + end_time = datetime.fromisoformat(end_time) + except ValueError as e: + raise ValueError(f"Invalid date format: {e}") + # Check if the end date is earlier than the start date + if end_time < start_time: + raise ValueError("End date cannot be earlier than start date.") + + return values # Does nothing, just returns the values as they are - @field_validator("longitude", "latitude", "myFile", check_fields=False) - def validate_title(cls, latitude: str, longitude: str, myFile:str) -> str: - """Ensure list lengths match""" - if len(latitude) != len(longitude): - raise ValueError("Latitude and longitude lists are different lengths") + @model_validator(mode="before") + def validate_location(cls, values): + """Ensure list lengths match and check latitude/longitude validity.""" + lat = values.__dict__.get("latitude") + lon = values.__dict__.get("longitude") + file = values.__dict__.get("myFile") + """Ensure list lengths match.""" + #if len(latitude) != len(longitude): + # raise ValueError("Latitude and longitude lists are different lengths") """If there is no latitude and longitude, make sure there is a GeoJSON""" - if latitude == None: - if myFile == None: - raise ValueError("Must include either GeoJSON file or manually inputted latitude and longitudes") + if lat is None and file is None: + raise ValueError("Must include either GeoJSON file or manually inputted latitude and longitudes") """Check latitude and longitude ranges""" - for i in latitude: - if i > 90: - raise ValueError("Latitudes must be between -90 and 90 degrees") - if i < -90: - raise ValueError("Latitudes must be between -90 and 90 degrees") - for i in longitude: - if i > 180: - raise ValueError("Longitudes must be between -180 and 180 degrees") - if i < -180: - raise ValueError("Longitudes must be between -180 and 180 degrees") - - return latitude, longitude, myFile - + if lat > 90 or lat < -90: + raise ValueError("Latitudes must be between -90 and 90 degrees") + if lon > 180 or lon < -180: + raise ValueError("Latitudes must be between -90 and 90 degrees") + return values class DataRequest(DataRequestBase, table=True): """ @@ -48,6 +59,7 @@ class DataRequest(DataRequestBase, table=True): This object contains the representation of the data in the database. """ + id: int | None = Field(default=None, primary_key=True) username: str title: str @@ -66,7 +78,7 @@ class DataRequest(DataRequestBase, table=True): path: str input: str | None link: str | None - + class DataRequestPublic(DataRequestBase): """ @@ -77,15 +89,16 @@ class DataRequestPublic(DataRequestBase): be included in this object. """ - id: int | None = Field(default=None, primary_key=True) + id: int + username: str title: str desc: str | None fname: str lname: str email: str geometry: str - latitude: str | None - longitude: str | None + latitude: str | None + longitude: str | None myFile: str | None start_date: datetime end_date: datetime @@ -104,7 +117,20 @@ class DataRequestUpdate(DataRequestBase): Fields should be optional unless they *must* be updated every time a change is made. """ + username: str | None = None title: str | None = None desc: str | None = None - date: datetime | None = None - # TODO: make sure parameters added in DataRequest are made optional here + fname: str | None = None + lname: str | None = None + email: str | None = None + geometry: str | None = None + latitude: str | None = None + longitude: str | None = None + myFile: str | None = None + start_date: datetime | None = None + end_date: datetime | None = None + variables: str | None = None + models: str | None = None + path: str | None = None + input: str | None = None + link: str | None = None \ No newline at end of file diff --git a/test/conftest.py b/test/conftest.py index e69de29..690e9e8 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -0,0 +1,36 @@ +# /marble-api/tests/test_data_request.py +import httpx +from datetime import datetime + +# Replace this with your actual server's IP and port +BASE_URL = "http://127.0.0.1:8000/v1" + +def test_valid_data_request(): + payload = { + "start_date": "1990-01-01T00:00:00", + "end_date": "2024-01-01T00:00:00", + "latitude": "37.7749", + "longitude": "-122.899', + "username": "user123", + "title": "Test Request", + "fname": "John", + "lname": "Doe", + "email": "john.doe@example.com", + "geometry": "Point", + "myFile": None, + "variables": None, + "models": None, + "path": "/some/path", + "input": None, + "link": None + } + + response = httpx.post(f"{BASE_URL}/data-publish-request", json=payload) + print(response.status_code) + print(response.text) # This will print out the error message + assert response.status_code == 200 + json_data = response.json() + + # Instead of checking for "message", check for the presence of the "id" field + assert "id" in json_data # Ensure that the returned data has an "id" field + assert json_data["username"] == "user123" \ No newline at end of file From 1f1a0984b078e1e137f56f7c61c50b5e586bad5e Mon Sep 17 00:00:00 2001 From: cassandwich Date: Thu, 17 Apr 2025 15:19:53 -0400 Subject: [PATCH 2/8] Modified to align with list format lat/lon --- app/versions/v1/models.py | 56 +++++++++++++++++++++------------------ test/conftest.py | 10 +++---- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/app/versions/v1/models.py b/app/versions/v1/models.py index 53e215e..db0e13a 100644 --- a/app/versions/v1/models.py +++ b/app/versions/v1/models.py @@ -1,12 +1,14 @@ from datetime import datetime -from pydantic import field_validator, model_validator +from pydantic import field_validator, model_validator, validator from sqlmodel import Field, SQLModel +from sqlalchemy import JSON +import ast from fastapi import FastAPI -from typing import List +from typing import List, Optional -app = FastAPI(version="1") +app = FastAPI(version="1") class DataRequestBase(SQLModel): """ @@ -37,8 +39,8 @@ def validate_timeperiod(cls, values): @model_validator(mode="before") def validate_location(cls, values): """Ensure list lengths match and check latitude/longitude validity.""" - lat = values.__dict__.get("latitude") - lon = values.__dict__.get("longitude") + lat = ast.literal_eval(values.__dict__.get("latitude")) + lon = ast.literal_eval(values.__dict__.get("longitude")) file = values.__dict__.get("myFile") """Ensure list lengths match.""" #if len(latitude) != len(longitude): @@ -47,10 +49,12 @@ def validate_location(cls, values): if lat is None and file is None: raise ValueError("Must include either GeoJSON file or manually inputted latitude and longitudes") """Check latitude and longitude ranges""" - if lat > 90 or lat < -90: - raise ValueError("Latitudes must be between -90 and 90 degrees") - if lon > 180 or lon < -180: - raise ValueError("Latitudes must be between -90 and 90 degrees") + for i in lat: + if i > 90 or i < -90: + raise ValueError("Latitudes must be between -90 and 90 degrees") + for i in lon: + if float(i) > 180 or float(i) < -180: + raise ValueError("Latitudes must be between -90 and 90 degrees") return values class DataRequest(DataRequestBase, table=True): @@ -64,15 +68,15 @@ class DataRequest(DataRequestBase, table=True): username: str title: str desc: str | None - fname: str - lname: str - email: str + authorFNames: str + authorLNames: str + authorEmails: str geometry: str latitude: str | None longitude: str | None myFile: str | None - start_date: datetime - end_date: datetime + start_date: str | None + end_date: str | None variables: str | None models: str | None path: str @@ -93,15 +97,15 @@ class DataRequestPublic(DataRequestBase): username: str title: str desc: str | None - fname: str - lname: str - email: str + authorFNames: str + authorLNames: str + authorEmails: str geometry: str - latitude: str | None - longitude: str | None + latitude: str | None + longitude: str | None myFile: str | None - start_date: datetime - end_date: datetime + start_date: str | None + end_date: str | None variables: str | None models: str | None path: str @@ -120,15 +124,15 @@ class DataRequestUpdate(DataRequestBase): username: str | None = None title: str | None = None desc: str | None = None - fname: str | None = None - lname: str | None = None - email: str | None = None + authorFNames: str | None = None + authorLNames: str | None = None + authorEmails: str | None = None geometry: str | None = None latitude: str | None = None longitude: str | None = None myFile: str | None = None - start_date: datetime | None = None - end_date: datetime | None = None + start_date: str | None = None + end_date: str | None = None variables: str | None = None models: str | None = None path: str | None = None diff --git a/test/conftest.py b/test/conftest.py index 690e9e8..2dfda33 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,13 +9,13 @@ def test_valid_data_request(): payload = { "start_date": "1990-01-01T00:00:00", "end_date": "2024-01-01T00:00:00", - "latitude": "37.7749", - "longitude": "-122.899', + "latitude": "[37.7749, 85]", + "longitude": "[-122.899,-80]", "username": "user123", "title": "Test Request", - "fname": "John", - "lname": "Doe", - "email": "john.doe@example.com", + "authorFNames": "['John', 'Cassie']", + "authorLNames": "['Doe', 'Chanen']", + "email": "['john.doe@example.com', 'cassie@gmail.com']", "geometry": "Point", "myFile": None, "variables": None, From 9a7095581724e432d973ade4de69a9763ecd17a2 Mon Sep 17 00:00:00 2001 From: cassandwich Date: Thu, 22 May 2025 09:53:03 -0400 Subject: [PATCH 3/8] no major changes --- app/versions/v1/app.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/app/versions/v1/app.py b/app/versions/v1/app.py index 1a84cdb..8b6329a 100644 --- a/app/versions/v1/app.py +++ b/app/versions/v1/app.py @@ -15,19 +15,10 @@ async def post_data_request( data_request: Annotated[DataRequest, AfterValidator(DataRequest.model_validate)], session: SessionDep ) -> DataRequestPublic: """Create a new data request and return the newly created data request.""" - try: - session.add(data_request) - session.commit() - session.refresh(data_request) - return data_request - except Exception as e: - session.rollback() - raise HTTPException(status_code=500, detail="Internal Server Error") - - #session.add(data_request) - #session.commit() - #session.refresh(data_request) - #return data_request + session.add(data_request) + session.commit() + session.refresh(data_request) + return data_request @app.patch("/data-publish-request/{request_id}") From 8c171d195a56c338818d3831fdec2e8f12fa2e25 Mon Sep 17 00:00:00 2001 From: cassandwich Date: Thu, 22 May 2025 09:53:26 -0400 Subject: [PATCH 4/8] updated tests --- app/versions/v1/models.py | 44 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/app/versions/v1/models.py b/app/versions/v1/models.py index db0e13a..ddc7ebc 100644 --- a/app/versions/v1/models.py +++ b/app/versions/v1/models.py @@ -7,9 +7,6 @@ from fastapi import FastAPI from typing import List, Optional - -app = FastAPI(version="1") - class DataRequestBase(SQLModel): """ SQL model base object for Data Requests. @@ -19,28 +16,29 @@ class DataRequestBase(SQLModel): start_date: datetime end_date: datetime - @model_validator(mode="before") + @model_validator(mode="after") def validate_timeperiod(cls, values): - start_time = values.__dict__.get("start_date") - end_time = values.__dict__.get("end_date") - try: - if isinstance(start_time, str): - start_time = datetime.fromisoformat(start_time) - if isinstance(end_time, str): - end_time = datetime.fromisoformat(end_time) - except ValueError as e: - raise ValueError(f"Invalid date format: {e}") + start_time = self.get("start_date") + end_time = self.get("end_date") + # try: + # if isinstance(start_time, str): + # start_time = datetime.fromisoformat(start_time) + # if isinstance(end_time, str): + # end_time = datetime.fromisoformat(end_time) + # except ValueError as e: + # raise ValueError(f"Invalid date format: {e}") # Check if the end date is earlier than the start date if end_time < start_time: raise ValueError("End date cannot be earlier than start date.") return values # Does nothing, just returns the values as they are - @model_validator(mode="before") + @model_validator(mode="after") def validate_location(cls, values): """Ensure list lengths match and check latitude/longitude validity.""" - lat = ast.literal_eval(values.__dict__.get("latitude")) - lon = ast.literal_eval(values.__dict__.get("longitude")) + #json list + lat = json.loads(self.get("latitude")) + lon = json.loads(self.get("longitude")) file = values.__dict__.get("myFile") """Ensure list lengths match.""" #if len(latitude) != len(longitude): @@ -68,15 +66,15 @@ class DataRequest(DataRequestBase, table=True): username: str title: str desc: str | None - authorFNames: str + authorFNames: str #add check auhor1 authorLNames: str authorEmails: str geometry: str latitude: str | None longitude: str | None myFile: str | None - start_date: str | None - end_date: str | None + start_date: datetime | None + end_date: datetime | None variables: str | None models: str | None path: str @@ -104,8 +102,8 @@ class DataRequestPublic(DataRequestBase): latitude: str | None longitude: str | None myFile: str | None - start_date: str | None - end_date: str | None + start_date: datetime | None + end_date: datetime | None variables: str | None models: str | None path: str @@ -131,8 +129,8 @@ class DataRequestUpdate(DataRequestBase): latitude: str | None = None longitude: str | None = None myFile: str | None = None - start_date: str | None = None - end_date: str | None = None + start_date: datetime | None = None + end_date: datetime | None = None variables: str | None = None models: str | None = None path: str | None = None From 1c53cb352e6176c6aa8fe1734a28c54afcb6ffc1 Mon Sep 17 00:00:00 2001 From: cassandwich Date: Thu, 19 Jun 2025 09:59:50 -0400 Subject: [PATCH 5/8] Updated to add conversion to STAC item to api file --- app/versions/v1/app.py | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/app/versions/v1/app.py b/app/versions/v1/app.py index 8b6329a..5b288e5 100644 --- a/app/versions/v1/app.py +++ b/app/versions/v1/app.py @@ -6,6 +6,7 @@ from app.database import SessionDep from app.versions.v1.models import DataRequest, DataRequestPublic, DataRequestUpdate +import geojson as gj app = FastAPI(version="1") @@ -37,9 +38,47 @@ async def patch_data_request( @app.get("/data-publish-request/{request_id}") -async def get_data_request(request_id: str, session: SessionDep) -> DataRequestPublic: +async def get_data_request(request_id: str, session: SessionDep, stac: bool = False) -> DataRequestPublic: """Get a data request with the given request_id.""" request = session.get(DataRequest, request_id) + if stac: + request = { + #generate footprint and bbox from geometry + #STAC item time (creation time) + datetime_utc = datetime.now(tz=timezone.utc) + #code to convert to a STAC item + item = pystac.Item(id=id, + geometry=myfile, + bbox= gj.bbox_get(myfile), + datetime=datetime_utc, + properties={}) + item_properties = { + "title": title, + "description": desc, + "authors_firstname": authorFNames, + "authors_lastname": authorLNames, + # date + "start_datetime": start_date, + "end_datetime": end_date, + "created": fdict["date"], + # variables + "variables": variables, + # models + "models": models, + "item_links": [{ + "rel": "self", + "href": path + }, + { + "rel": "derived_from", + "href": input + }, + { + "rel": "linked_files", + "href": link + }] + } + } if not request: raise HTTPException(status_code=404, detail="data publish request not found") return request From ad7a4ab28904f49a6fc412d15ff84ceeca3c190a Mon Sep 17 00:00:00 2001 From: cassandwich Date: Fri, 18 Jul 2025 10:31:34 -0400 Subject: [PATCH 6/8] Updated to add geometry bbox function --- app/versions/v1/app.py | 48 +++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/app/versions/v1/app.py b/app/versions/v1/app.py index 5b288e5..2eff874 100644 --- a/app/versions/v1/app.py +++ b/app/versions/v1/app.py @@ -6,7 +6,7 @@ from app.database import SessionDep from app.versions.v1.models import DataRequest, DataRequestPublic, DataRequestUpdate -import geojson as gj +import gbbox app = FastAPI(version="1") @@ -43,15 +43,9 @@ async def get_data_request(request_id: str, session: SessionDep, stac: bool = Fa request = session.get(DataRequest, request_id) if stac: request = { - #generate footprint and bbox from geometry #STAC item time (creation time) datetime_utc = datetime.now(tz=timezone.utc) #code to convert to a STAC item - item = pystac.Item(id=id, - geometry=myfile, - bbox= gj.bbox_get(myfile), - datetime=datetime_utc, - properties={}) item_properties = { "title": title, "description": desc, @@ -78,7 +72,45 @@ async def get_data_request(request_id: str, session: SessionDep, stac: bool = Fa "href": link }] } - } + #generate bbox from geometry + if myFile == None: + if geomtery == Point: + myfile = { + "type": "Point", + "coordinates": [latitude, longitude] + } + if geometry == Line: + myfile = { + "type": "Point", + "coordinates": [[latitude[0], longitude[0]]] + } + for i in range(1, len(latitude)): + myFile['coordinates'].append([latitude[i], longitude[i]]) + else: + if geomtery == Point: + shape = gbbox.Point(**geometry) + geobbox = shape.bbox() + if geomtery == Line: + shape = gbbox.Line(**geometry) + geobbox = shape.bbox() + if geomtery == LineString: + shape = gbbox.LineString(**geometry) + geobbox = shape.bbox() + if geomtery == MultiLineString: + shape = gbbox.MultiLineString(**geometry) + geobbox = shape.bbox() + if geomtery == Polygon: + shape = gbbox.Polygon(**geometry) + geobbox = shape.bbox() + if geomtery == MultiPolygon: + shape = gbbox.MultiPolygon(**geometry) + geobbox = shape.bbox() + #generate footprint and bbox from geometry + item = pystac.Item(id=id, + geometry = myfile, + bbox = geobbox, + datetime=datetime_utc, + properties=item_properties) if not request: raise HTTPException(status_code=404, detail="data publish request not found") return request From b62b53766b294acfdd13686a27245272072e0d67 Mon Sep 17 00:00:00 2001 From: cassandwich Date: Fri, 18 Jul 2025 10:55:26 -0400 Subject: [PATCH 7/8] Updated to add geometry bbox function --- app/versions/v1/app.py | 79 +++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/app/versions/v1/app.py b/app/versions/v1/app.py index 2eff874..2ec56bb 100644 --- a/app/versions/v1/app.py +++ b/app/versions/v1/app.py @@ -45,7 +45,7 @@ async def get_data_request(request_id: str, session: SessionDep, stac: bool = Fa request = { #STAC item time (creation time) datetime_utc = datetime.now(tz=timezone.utc) - #code to convert to a STAC item + #Create list of custom item properties (other than geometry and time), which will be added to the STAC item item_properties = { "title": title, "description": desc, @@ -72,7 +72,7 @@ async def get_data_request(request_id: str, session: SessionDep, stac: bool = Fa "href": link }] } - #generate bbox from geometry + #generate bbox from geometry, in the case of a simple geometry where user did not provide GeoJSON geometry input if myFile == None: if geomtery == Point: myfile = { @@ -85,32 +85,63 @@ async def get_data_request(request_id: str, session: SessionDep, stac: bool = Fa "coordinates": [[latitude[0], longitude[0]]] } for i in range(1, len(latitude)): - myFile['coordinates'].append([latitude[i], longitude[i]]) - else: - if geomtery == Point: - shape = gbbox.Point(**geometry) - geobbox = shape.bbox() - if geomtery == Line: - shape = gbbox.Line(**geometry) - geobbox = shape.bbox() - if geomtery == LineString: - shape = gbbox.LineString(**geometry) - geobbox = shape.bbox() - if geomtery == MultiLineString: - shape = gbbox.MultiLineString(**geometry) - geobbox = shape.bbox() - if geomtery == Polygon: - shape = gbbox.Polygon(**geometry) - geobbox = shape.bbox() - if geomtery == MultiPolygon: - shape = gbbox.MultiPolygon(**geometry) - geobbox = shape.bbox() - #generate footprint and bbox from geometry - item = pystac.Item(id=id, + myfile['coordinates'].append([latitude[i], longitude[i]]) + if geometry == LineString: + myfile = { + "type": "LineString", + "coordinates": [[latitude[0], longitude[0]]] + } + for i in range(1, len(latitude)): + myfile['coordinates'].append([latitude[i], longitude[i]]) + if geometry == MultiPoint: + myfile = { + "type": "MultiPoint", + "coordinates": [[latitude[0], longitude[0]]] + } + for i in range(1, len(latitude)): + myfile['coordinates'].append([latitude[i], longitude[i]]) + if geometry == Polygon: + myfile = { + "type": "Polygon", + "coordinates": [[latitude[0], longitude[0]]] + } + for i in range(1, len(latitude)): + myfile['coordinates'].append([latitude[i], longitude[i]]) + #Now that each file has a GeoJSON geometry feature (either user inputted or generated above), use gbbox package to generate gbbox + if geomtery == Point: + shape = gbbox.Point(**myfile) + geobbox = shape.bbox() + if geomtery == Line: + shape = gbbox.Line(**myfile) + geobbox = shape.bbox() + if geomtery == LineString: + shape = gbbox.LineString(**myfile) + geobbox = shape.bbox() + if geomtery == MultiLineString: + shape = gbbox.MultiLineString(**myfile) + geobbox = shape.bbox() + if geomtery == Polygon: + shape = gbbox.Polygon(**myfile) + geobbox = shape.bbox() + if geomtery == MultiPolygon: + shape = gbbox.MultiPolygon(**myfile) + geobbox = shape.bbox() + if geometry == GeometryCollection: + shape = gbbox.GeometryCollection(**myfile) + geobbox = shape.bbox() + #Create STAC item (null case and regular case) + if geometry == null: + item = pystac.Item(id=id, + geometry = null, + datetime=datetime_utc, + properties=item_properties) + elif geometry != null: + item = pystac.Item(id=id, geometry = myfile, bbox = geobbox, datetime=datetime_utc, properties=item_properties) + } if not request: raise HTTPException(status_code=404, detail="data publish request not found") return request From 64279dbcd781672bf5908136eb75a3d19077777f Mon Sep 17 00:00:00 2001 From: cassandwich Date: Wed, 27 Aug 2025 14:45:58 -0400 Subject: [PATCH 8/8] updated to fix errors --- app/versions/v1/app.py | 96 +++++++++++++++++++-------------------- app/versions/v1/models.py | 46 ++++++++----------- 2 files changed, 64 insertions(+), 78 deletions(-) diff --git a/app/versions/v1/app.py b/app/versions/v1/app.py index 2ec56bb..2d292a8 100644 --- a/app/versions/v1/app.py +++ b/app/versions/v1/app.py @@ -7,6 +7,10 @@ from app.database import SessionDep from app.versions.v1.models import DataRequest, DataRequestPublic, DataRequestUpdate import gbbox +from shapely.geometry import shape as shp +import json +from datetime import datetime, timezone +import pystac app = FastAPI(version="1") @@ -21,7 +25,6 @@ async def post_data_request( session.refresh(data_request) return data_request - @app.patch("/data-publish-request/{request_id}") async def patch_data_request( request_id: str, data_request: DataRequestUpdate, session: SessionDep @@ -36,116 +39,109 @@ async def patch_data_request( session.refresh(data_request) return data_request - @app.get("/data-publish-request/{request_id}") async def get_data_request(request_id: str, session: SessionDep, stac: bool = False) -> DataRequestPublic: """Get a data request with the given request_id.""" - request = session.get(DataRequest, request_id) + data_request = session.get(DataRequest, request_id) if stac: - request = { #STAC item time (creation time) datetime_utc = datetime.now(tz=timezone.utc) #Create list of custom item properties (other than geometry and time), which will be added to the STAC item item_properties = { - "title": title, - "description": desc, - "authors_firstname": authorFNames, - "authors_lastname": authorLNames, + "title": data_request.title, + "description": data_request.desc, + "authors_firstname": data_request.authorFNames, + "authors_lastname": data_request.authorLNames, # date - "start_datetime": start_date, - "end_datetime": end_date, - "created": fdict["date"], + "start_datetime": data_request.start_date, + "end_datetime": data_request.end_date, # variables - "variables": variables, + "variables": data_request.variables, # models - "models": models, + "models": data_request.models, "item_links": [{ "rel": "self", - "href": path + "href": data_request.path }, { "rel": "derived_from", - "href": input + "href": data_request.input }, { "rel": "linked_files", - "href": link + "href": data_request.link }] } #generate bbox from geometry, in the case of a simple geometry where user did not provide GeoJSON geometry input - if myFile == None: - if geomtery == Point: - myfile = { - "type": "Point", - "coordinates": [latitude, longitude] - } - if geometry == Line: + myfile = data_request.myFile + if data_request.myFile == "": + latitude = json.loads(data_request.latitude) + longitude = json.loads(data_request.longitude) + if data_request.geometry == "Point": myfile = { "type": "Point", - "coordinates": [[latitude[0], longitude[0]]] + "coordinates": [data_request.latitude, data_request.longitude] } - for i in range(1, len(latitude)): - myfile['coordinates'].append([latitude[i], longitude[i]]) - if geometry == LineString: + if data_request.geometry == "LineString": myfile = { "type": "LineString", "coordinates": [[latitude[0], longitude[0]]] } for i in range(1, len(latitude)): myfile['coordinates'].append([latitude[i], longitude[i]]) - if geometry == MultiPoint: + if data_request.geometry == "MultiPoint": myfile = { "type": "MultiPoint", - "coordinates": [[latitude[0], longitude[0]]] + "coordinates": [[data_request.latitude[0], data_request.longitude[0]]] } - for i in range(1, len(latitude)): - myfile['coordinates'].append([latitude[i], longitude[i]]) - if geometry == Polygon: + for i in range(1, len(data_request.latitude)): + myfile['coordinates'].append([data_request.latitude[i], data_request.longitude[i]]) + if data_request.geometry == "Polygon": myfile = { "type": "Polygon", - "coordinates": [[latitude[0], longitude[0]]] + "coordinates": [[data_request.latitude[0], data_request.longitude[0]]] } - for i in range(1, len(latitude)): - myfile['coordinates'].append([latitude[i], longitude[i]]) + for i in range(1, len(data_request.latitude)): + myfile['coordinates'].append([data_request.latitude[i], data_request.longitude[i]]) #Now that each file has a GeoJSON geometry feature (either user inputted or generated above), use gbbox package to generate gbbox - if geomtery == Point: + if data_request.geometry == "Point": shape = gbbox.Point(**myfile) geobbox = shape.bbox() - if geomtery == Line: - shape = gbbox.Line(**myfile) - geobbox = shape.bbox() - if geomtery == LineString: + if data_request.geometry == "Line": + geom = shp(myfile) + geobbox = geom.bounds + if data_request.geometry == "LineString": shape = gbbox.LineString(**myfile) geobbox = shape.bbox() - if geomtery == MultiLineString: + if data_request.geometry == "MultiLineString": shape = gbbox.MultiLineString(**myfile) geobbox = shape.bbox() - if geomtery == Polygon: + if data_request.geometry == "Polygon": shape = gbbox.Polygon(**myfile) geobbox = shape.bbox() - if geomtery == MultiPolygon: + if data_request.geometry == "MultiPolygon": shape = gbbox.MultiPolygon(**myfile) geobbox = shape.bbox() - if geometry == GeometryCollection: + if data_request.geometry == "GeometryCollection": shape = gbbox.GeometryCollection(**myfile) geobbox = shape.bbox() #Create STAC item (null case and regular case) - if geometry == null: + if data_request.geometry == "null": item = pystac.Item(id=id, geometry = null, datetime=datetime_utc, properties=item_properties) - elif geometry != null: + elif data_request.geometry != "null": item = pystac.Item(id=id, geometry = myfile, bbox = geobbox, datetime=datetime_utc, properties=item_properties) - } - if not request: + return item.to_dict() #CHECK WORKING + if not data_request: raise HTTPException(status_code=404, detail="data publish request not found") - return request - + #return request if not stac + return data_request class _LinksResponse(BaseModel): rel: str diff --git a/app/versions/v1/models.py b/app/versions/v1/models.py index ddc7ebc..e3c6ffb 100644 --- a/app/versions/v1/models.py +++ b/app/versions/v1/models.py @@ -13,47 +13,37 @@ class DataRequestBase(SQLModel): This object contains field validators. """ - start_date: datetime - end_date: datetime + start_date: str + end_date: str @model_validator(mode="after") def validate_timeperiod(cls, values): - start_time = self.get("start_date") - end_time = self.get("end_date") - # try: - # if isinstance(start_time, str): - # start_time = datetime.fromisoformat(start_time) - # if isinstance(end_time, str): - # end_time = datetime.fromisoformat(end_time) - # except ValueError as e: - # raise ValueError(f"Invalid date format: {e}") - # Check if the end date is earlier than the start date - if end_time < start_time: + if values.__dict__.get("end_date") < values.__dict__.get("start_date"): raise ValueError("End date cannot be earlier than start date.") return values # Does nothing, just returns the values as they are - @model_validator(mode="after") +""" @model_validator(mode="after") def validate_location(cls, values): - """Ensure list lengths match and check latitude/longitude validity.""" + #Ensure list lengths match and check latitude/longitude validity. #json list - lat = json.loads(self.get("latitude")) - lon = json.loads(self.get("longitude")) + lat = json.loads(values.__dict__.get("latitude")) + lon = json.loads(values.__dict__.get("longitude")) file = values.__dict__.get("myFile") - """Ensure list lengths match.""" + #Ensure list lengths match. #if len(latitude) != len(longitude): # raise ValueError("Latitude and longitude lists are different lengths") - """If there is no latitude and longitude, make sure there is a GeoJSON""" + #If there is no latitude and longitude, make sure there is a GeoJSON if lat is None and file is None: raise ValueError("Must include either GeoJSON file or manually inputted latitude and longitudes") - """Check latitude and longitude ranges""" + #Check latitude and longitude ranges for i in lat: if i > 90 or i < -90: raise ValueError("Latitudes must be between -90 and 90 degrees") for i in lon: if float(i) > 180 or float(i) < -180: raise ValueError("Latitudes must be between -90 and 90 degrees") - return values + return values """ class DataRequest(DataRequestBase, table=True): """ @@ -66,15 +56,15 @@ class DataRequest(DataRequestBase, table=True): username: str title: str desc: str | None - authorFNames: str #add check auhor1 + authorFNames: str authorLNames: str authorEmails: str geometry: str latitude: str | None longitude: str | None myFile: str | None - start_date: datetime | None - end_date: datetime | None + start_date: str | None + end_date: str | None variables: str | None models: str | None path: str @@ -102,8 +92,8 @@ class DataRequestPublic(DataRequestBase): latitude: str | None longitude: str | None myFile: str | None - start_date: datetime | None - end_date: datetime | None + start_date: str | None + end_date: str | None variables: str | None models: str | None path: str @@ -129,8 +119,8 @@ class DataRequestUpdate(DataRequestBase): latitude: str | None = None longitude: str | None = None myFile: str | None = None - start_date: datetime | None = None - end_date: datetime | None = None + start_date: str | None = None + end_date: str | None = None variables: str | None = None models: str | None = None path: str | None = None