-
Notifications
You must be signed in to change notification settings - Fork 0
Add support for passing in property filters #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,10 +7,11 @@ | |||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| import requests | ||||||||||||||
| from typing import List, Dict, Any, Optional | ||||||||||||||
| from typing import List, Dict, Any, Optional, Literal | ||||||||||||||
| from dataclasses import dataclass | ||||||||||||||
| import logging | ||||||||||||||
| from urllib.parse import urljoin | ||||||||||||||
| import json | ||||||||||||||
|
|
||||||||||||||
| # Import pydantic Entity from bertron_schema_pydantic | ||||||||||||||
| from schema.datamodel.bertron_schema_pydantic import Entity | ||||||||||||||
|
|
@@ -85,6 +86,105 @@ def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: | |||||||||||||
| logger.error(f"API request failed: {e}") | ||||||||||||||
| raise BertronAPIError(f"API request failed: {e}") | ||||||||||||||
|
|
||||||||||||||
| def create_property_filter(self, | ||||||||||||||
| property_name: Optional[str] = None, | ||||||||||||||
| property_id: Optional[str] = None, | ||||||||||||||
| property_value: Any = None, | ||||||||||||||
| property_type: Optional[Literal["raw", "numeric", "regex", "range"]] = "raw") -> Dict[str, Any]: | ||||||||||||||
| """ | ||||||||||||||
| Create a filter dictionary for querying entities using $elemMatch. | ||||||||||||||
|
|
||||||||||||||
| This ensures all conditions match the SAME property object in the properties array, | ||||||||||||||
| preventing false matches across different property objects. | ||||||||||||||
|
|
||||||||||||||
| Args: | ||||||||||||||
| property_name: Name of the property to filter on (e.g., "depth", "elevation") | ||||||||||||||
| property_id: ID of the property to filter on (e.g., "MIXS:0000018") | ||||||||||||||
| property_value: Value to filter by | ||||||||||||||
| property_type: Type of filter - must be one of: | ||||||||||||||
| - "raw": Match exact string in value or raw_value fields | ||||||||||||||
| - "numeric": Match exact numeric value | ||||||||||||||
| - "regex": Match regex pattern in value field | ||||||||||||||
| - "range": Match numeric range [min, max] | ||||||||||||||
|
|
||||||||||||||
| Returns: | ||||||||||||||
| Dictionary representing the filter with $elemMatch | ||||||||||||||
|
|
||||||||||||||
| Examples: | ||||||||||||||
| # Filter by property name and raw value | ||||||||||||||
| filter = create_property_filter( | ||||||||||||||
| property_name="depth", | ||||||||||||||
| property_value="0 - 0.1m", | ||||||||||||||
| property_type="raw" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| # Filter by property ID and numeric value | ||||||||||||||
| filter = create_property_filter( | ||||||||||||||
| property_id="MIXS:0000093", | ||||||||||||||
| property_value=24, | ||||||||||||||
| property_type="numeric" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| # Filter by numeric range | ||||||||||||||
| filter = create_property_filter( | ||||||||||||||
| property_name="elevation", | ||||||||||||||
| property_value=[20, 30], | ||||||||||||||
| property_type="range" | ||||||||||||||
| ) | ||||||||||||||
| """ | ||||||||||||||
| if not property_name and not property_id: | ||||||||||||||
| raise ValueError("Either property_name or property_id must be provided") | ||||||||||||||
|
|
||||||||||||||
| # Build the $elemMatch conditions | ||||||||||||||
| elem_match_conditions = {} | ||||||||||||||
|
|
||||||||||||||
| # Add attribute filters | ||||||||||||||
| if property_name: | ||||||||||||||
| elem_match_conditions["attribute.label"] = property_name | ||||||||||||||
| if property_id: | ||||||||||||||
| elem_match_conditions["attribute.id"] = property_id | ||||||||||||||
|
|
||||||||||||||
| # If no value specified, just filter by attribute | ||||||||||||||
| if property_value is None: | ||||||||||||||
| return {"properties": {"$elemMatch": elem_match_conditions}} | ||||||||||||||
|
|
||||||||||||||
| # Add value filters based on property_type | ||||||||||||||
| if property_type == "regex": | ||||||||||||||
| elem_match_conditions["value"] = {"$regex": property_value} | ||||||||||||||
|
|
||||||||||||||
| elif property_type == "numeric": | ||||||||||||||
| elem_match_conditions["numeric_value"] = property_value | ||||||||||||||
|
|
||||||||||||||
| elif property_type == "range": | ||||||||||||||
| # For range queries, we need to handle both single numeric_value | ||||||||||||||
| # and minimum_numeric_value/maximum_numeric_value pairs | ||||||||||||||
| # Use $or to match either case | ||||||||||||||
| elem_match_conditions["$or"] = [ | ||||||||||||||
| { | ||||||||||||||
| "numeric_value": { | ||||||||||||||
| "$gte": property_value[0], | ||||||||||||||
| "$lte": property_value[1] | ||||||||||||||
| } | ||||||||||||||
| }, | ||||||||||||||
| { | ||||||||||||||
| "minimum_numeric_value": {"$lte": property_value[1]}, | ||||||||||||||
| "maximum_numeric_value": {"$gte": property_value[0]} | ||||||||||||||
| } | ||||||||||||||
| ] | ||||||||||||||
|
Comment on lines
+158
to
+173
|
||||||||||||||
|
|
||||||||||||||
| elif property_type == "raw": | ||||||||||||||
| # Match either value or raw_value field | ||||||||||||||
| elem_match_conditions["$or"] = [ | ||||||||||||||
| {"value": property_value}, | ||||||||||||||
| {"raw_value": property_value} | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| else: | ||||||||||||||
| raise ValueError("Invalid property_type. Must be one of 'raw', 'numeric', 'regex', 'range'") | ||||||||||||||
|
|
||||||||||||||
| return {"properties": {"$elemMatch": elem_match_conditions}} | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def health_check(self) -> Dict[str, Any]: | ||||||||||||||
| """ | ||||||||||||||
| Check the health of the BERtron API server. | ||||||||||||||
|
|
@@ -153,7 +253,11 @@ def find_entities( | |||||||||||||
| return QueryResponse(entities=entities, count=response["count"]) | ||||||||||||||
|
|
||||||||||||||
| def find_nearby_entities( | ||||||||||||||
| self, latitude: float, longitude: float, radius_meters: float | ||||||||||||||
| self, | ||||||||||||||
| latitude: float, | ||||||||||||||
| longitude: float, | ||||||||||||||
| radius_meters: float, | ||||||||||||||
| filter_dict: Optional[Dict[str, Any]] = None, | ||||||||||||||
| ) -> QueryResponse: | ||||||||||||||
| """ | ||||||||||||||
| Find entities within a specified radius of a geographic point. | ||||||||||||||
|
|
@@ -166,11 +270,14 @@ def find_nearby_entities( | |||||||||||||
| Returns: | ||||||||||||||
| QueryResponse containing nearby entities (sorted by distance) | ||||||||||||||
| """ | ||||||||||||||
| params = { | ||||||||||||||
| params: Dict[str, Any] = { | ||||||||||||||
| "latitude": latitude, | ||||||||||||||
| "longitude": longitude, | ||||||||||||||
| "radius_meters": radius_meters, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if filter_dict: | ||||||||||||||
| params["filter_json"] = json.dumps(filter_dict) | ||||||||||||||
|
|
||||||||||||||
| response = self._make_request("GET", "/bertron/geo/nearby", params=params) | ||||||||||||||
| entities = [Entity(**doc) for doc in response["documents"]] | ||||||||||||||
|
|
@@ -194,6 +301,7 @@ def find_entities_in_bounding_box( | |||||||||||||
| southwest_lng: float, | ||||||||||||||
| northeast_lat: float, | ||||||||||||||
| northeast_lng: float, | ||||||||||||||
| filter_dict: Optional[Dict[str, Any]] = None | ||||||||||||||
| ) -> QueryResponse: | ||||||||||||||
| """ | ||||||||||||||
| Find entities within a rectangular bounding box. | ||||||||||||||
|
|
@@ -207,12 +315,15 @@ def find_entities_in_bounding_box( | |||||||||||||
| Returns: | ||||||||||||||
| QueryResponse containing entities within the bounding box | ||||||||||||||
| """ | ||||||||||||||
| params = { | ||||||||||||||
| params: Dict[str, Any] = { | ||||||||||||||
| "southwest_lat": southwest_lat, | ||||||||||||||
| "southwest_lng": southwest_lng, | ||||||||||||||
| "northeast_lat": northeast_lat, | ||||||||||||||
| "northeast_lng": northeast_lng, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if filter_dict: | ||||||||||||||
| params["filter_json"] = json.dumps(filter_dict) | ||||||||||||||
|
|
||||||||||||||
| response = self._make_request("GET", "/bertron/geo/bbox", params=params) | ||||||||||||||
| entities = [Entity(**doc) for doc in response["documents"]] | ||||||||||||||
|
|
@@ -232,7 +343,7 @@ def find_entities_in_bounding_box( | |||||||||||||
| metadata=metadata, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| def find_entities_by_source(self, source: str) -> QueryResponse: | ||||||||||||||
| def find_entities_by_source(self, source: str, filter_dict: Optional[Dict[str, Any]] = None) -> QueryResponse: | ||||||||||||||
| """ | ||||||||||||||
| Find entities from a specific BER data source. | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -242,9 +353,12 @@ def find_entities_by_source(self, source: str) -> QueryResponse: | |||||||||||||
| Returns: | ||||||||||||||
| QueryResponse containing entities from the specified source | ||||||||||||||
| """ | ||||||||||||||
| return self.find_entities(filter_dict={"ber_data_source": source}) | ||||||||||||||
| base_filter = {"ber_data_source": source} | ||||||||||||||
| if filter_dict: | ||||||||||||||
| base_filter.update(filter_dict) | ||||||||||||||
|
Comment on lines
+356
to
+358
|
||||||||||||||
| return self.find_entities(filter_dict=base_filter) | ||||||||||||||
|
|
||||||||||||||
| def find_entities_by_entity_type(self, entity_type: str) -> QueryResponse: | ||||||||||||||
| def find_entities_by_entity_type(self, entity_type: str, filter_dict: Optional[Dict[str, Any]] = None) -> QueryResponse: | ||||||||||||||
| """ | ||||||||||||||
| Find entities of a specific entity type. | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -254,7 +368,10 @@ def find_entities_by_entity_type(self, entity_type: str) -> QueryResponse: | |||||||||||||
| Returns: | ||||||||||||||
| QueryResponse containing entities of the specified type | ||||||||||||||
| """ | ||||||||||||||
| return self.find_entities(filter_dict={"entity_type": entity_type}) | ||||||||||||||
| base_filter = {"entity_type": entity_type} | ||||||||||||||
| if filter_dict: | ||||||||||||||
| base_filter.update(filter_dict) | ||||||||||||||
|
Comment on lines
+371
to
+373
|
||||||||||||||
| base_filter = {"entity_type": entity_type} | |
| if filter_dict: | |
| base_filter.update(filter_dict) | |
| # Ensure the entity_type argument always takes precedence over filter_dict | |
| base_filter = dict(filter_dict) if filter_dict else {} | |
| base_filter["entity_type"] = entity_type |
Uh oh!
There was an error while loading. Please reload this page.