Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions apps/project/graphql/queries.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

import strawberry
import strawberry_django
from django.db.models import QuerySet
Expand All @@ -7,7 +9,14 @@

from apps.project.custom_options import get_custom_options
from apps.project.graphql.inputs.inputs import ProjectNameInput
from apps.project.graphql.types.project_types.validate import (
TestValidateAoiObjectsResponse,
TestValidateTaskingManagerProjectResponse,
)
from apps.project.models import Organization, Project, ProjectAsset, ProjectTypeEnum
from project_types.base.project import ValidationException
from project_types.validate.project import ValidateProject
from utils import fields
from utils.geo.raster_tile_server.config import RasterConfig, RasterTileServerNameEnum, RasterTileServerNameEnumWithoutCustom
from utils.geo.vector_tile_server.config import VectorConfig, VectorTileServerNameEnum, VectorTileServerNameEnumWithoutCustom

Expand All @@ -25,6 +34,8 @@
ProjectType,
)

logger = logging.getLogger(__name__)


def get_tile_servers() -> RasterTileServersType:
def _get_raster_tile_server_type(enum: RasterTileServerNameEnumWithoutCustom):
Expand Down Expand Up @@ -79,6 +90,73 @@ def default_custom_options(self, project_type: ProjectTypeEnum) -> list[CustomOp
for item in custom_options
]

@strawberry.field(extensions=[IsAuthenticated()])
def test_aoi_objects(
self,
project_id: strawberry.ID | None,
asset_id: strawberry.ID | None,
ohsome_filter: str | None,
) -> TestValidateAoiObjectsResponse:
response = TestValidateAoiObjectsResponse(
project_id=project_id,
asset_id=asset_id,
ohsome_filter=ohsome_filter,
)

if project_id is None:
return response.generate_error("project_id is required to test aoi elements")

if asset_id is None:
return response.generate_error("asset_id is required to test aoi elements")

if ohsome_filter is None:
return response.generate_error("ohsome_filter is required to test aoi elements")
Comment on lines +96 to +113
Copy link
Collaborator

Choose a reason for hiding this comment

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

These should be required on the API schema itself.


try:
object_count = ValidateProject.test_ohsome_objects_from_aoi_asset(
project_id,
asset_id,
ohsome_filter,
)

response.object_count = object_count
return response
except ValidationException as e:
return response.generate_error(str(e))
except Exception as e:
raise GraphQLError(str(e)) from e

@strawberry.field(extensions=[IsAuthenticated()])
def test_tasking_manager_project(
self,
hot_tm_id: fields.PydanticId | None,
ohsome_filter: str | None,
) -> TestValidateTaskingManagerProjectResponse:
response = TestValidateTaskingManagerProjectResponse(
hot_tm_id=hot_tm_id,
ohsome_filter=ohsome_filter,
)

if hot_tm_id is None:
return response.generate_error("hot_tm_id is required to test HOT project aoi elements")

if ohsome_filter is None:
return response.generate_error("ohsome_filter is required to test HOT project aoi elements")
Comment on lines +129 to +144
Copy link
Collaborator

Choose a reason for hiding this comment

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

These should be required on the API schema itself.


try:
object_count = ValidateProject.test_tasking_manager_project(
hot_tm_id,
ohsome_filter,
)

response.object_count = object_count

return response
except ValidationException as e:
return response.generate_error(str(e))
except Exception as e:
raise GraphQLError(str(e)) from e

tile_servers: RasterTileServersType = strawberry.field(resolver=get_tile_servers, extensions=[IsAuthenticated()])

# Private --------------------
Expand Down
28 changes: 28 additions & 0 deletions apps/project/graphql/types/project_types/validate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import strawberry

from project_types.validate import project as validate_project
from utils import fields


@strawberry.experimental.pydantic.type(model=validate_project.ValidateObjectSourceConfig, all_fields=True)
Expand All @@ -9,3 +10,30 @@ class ValidateObjectSourceConfig: ...

@strawberry.experimental.pydantic.type(model=validate_project.ValidateProjectProperty, all_fields=True)
class ValidateProjectPropertyType: ...


DEFAULT_TEST_RESPONSE_ERROR_MESSAGE: str = "Something unexpected has occurred. Please contact an admin to fix this issue."


@strawberry.type
class ValidateTestAoiResponse:
ok: bool = True
error: str | None = None
object_count: int | None = None
ohsome_filter: str | None = None

def generate_error(self, message: str = DEFAULT_TEST_RESPONSE_ERROR_MESSAGE):
self.ok = False
self.error = message
return self


@strawberry.type
class TestValidateAoiObjectsResponse(ValidateTestAoiResponse):
project_id: strawberry.ID | None = None
asset_id: strawberry.ID | None = None


@strawberry.type
class TestValidateTaskingManagerProjectResponse(ValidateTestAoiResponse):
hot_tm_id: fields.PydanticId | None = None
35 changes: 35 additions & 0 deletions apps/project/tests/e2e_create_validate_project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,32 @@ class Mutation:
}
"""

class Query:
TEST_AOI_OBJECTS = """
query TestAoiObjects($assetId: ID, $projectId: ID, $ohsomeFilter: String) {
testAoiObjects(assetId: $assetId, projectId: $projectId, ohsomeFilter: $ohsomeFilter) {
ok
error
objectCount
assetId
projectId
ohsomeFilter
}
}
"""

TEST_TASKING_MANAGER_PROJECT = """
query TestTaskingManagerProject($hotTmId: String, $ohsomeFilter: String) {
testTaskingManagerProject(hotTmId: $hotTmId, ohsomeFilter: $ohsomeFilter) {
ok
error
objectCount
hotTmId
ohsomeFilter
}
}
"""

@pytest.mark.vcr("assets/tests/projects/validate/cassette")
def test_validate_project_e2e(self):
# TODO(susilnem): Add more test with filters
Expand Down Expand Up @@ -398,6 +424,15 @@ def _test_project(self, filename: str):
assert aoi_response["ok"]
aoi_id = aoi_response["result"]["id"]

# Test AOI objects
ohsomeFilter = "building=* and geometry:polygon"
Copy link
Collaborator

Choose a reason for hiding this comment

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

ohsome_filter

test_aoi_objects_content = self.query_check(
self.Query.TEST_AOI_OBJECTS,
variables={"assetId": aoi_id, "projectId": project_id, "ohsomeFilter": ohsomeFilter},
)
test_aoi_objects_response = test_aoi_objects_content["data"]["testAoiObjects"]
assert test_aoi_objects_response["ok"]

# Update project
update_project_data = test_data["update_project"]
update_project_data["image"] = image_id
Expand Down
35 changes: 35 additions & 0 deletions project_types/validate/api_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from main.config import Config
from main.logging import log_extra_response
from utils.fields import PydanticLongText

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -175,6 +176,40 @@ def remove_noise_and_add_user_info(json: dict[str, Any]) -> dict[str, Any]:
return json


# fixme(frozenhelium): merge this function with `ohsome` and also add appropriate messages to raised exceptions
def get_object_count_from_ohsome(area: str, ohsome_filter: PydanticLongText) -> int | None:
url = Config.OHSOME_API_LINK + "elements/count"
data = {"bpolys": area, "filter": ohsome_filter}

logger.info("Target: %s", url)
logger.info("Filter: %s", ohsome_filter)

# fixme(frozenhelium): use httpx for proper timeout
response = requests.post(url, data=data, timeout=100)
if response.status_code != 200:
logger.warning(
"ohsome element count request failed: check for errors in filter or geometries",
extra=log_extra_response(response=response),
)
raise ValidateApiCallError
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to include the error message in the exception?
Will it be helpful for the users?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it does, currently we're replicating the behaviour of ohsome request call, we should refactor both of these functions. I'll add appropriate 'fixme' for now

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe add this as a NOTE

logger.info("Query successful.")

response_json = response.json()
results = response_json.get("result", None)
if results is None:
return None

first_result = results[0]
if first_result is None:
return None

value = first_result.get("value", None)
if value is None:
return None

return int(value)


def ohsome(request: dict[str, Any], area: str, properties: str | None = None) -> dict[str, Any]:
"""Request data from Ohsome API."""
url = Config.OHSOME_API_LINK + request["endpoint"]
Expand Down
Loading
Loading