Skip to content
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

Osiris Rex Query Builder Initial Addition #73

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ jobs:
uses: djdefi/cloc-action@6
with:
options: --report-file=cloc.md


-
name: Upload SLOC
uses: actions/upload-artifact@v4
Expand Down
8 changes: 0 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ repos:
- id: reorder-python-imports
files: ^src/|tests/

- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy src
language: system
pass_filenames: false

- repo: local
hooks:
- id: black
Expand Down
6 changes: 1 addition & 5 deletions src/pds/peppi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*-
"""PDS peppi."""
from .client import PDSRegistryClient # noqa
from .orex import OrexProducts # noqa
from .products import Products # noqa

# For future consideration:
#
# - Other metadata (__docformat__, __copyright__, etc.)
# - N̶a̶m̶e̶s̶p̶a̶c̶e̶ ̶p̶a̶c̶k̶a̶g̶e̶s̶ we got this
2 changes: 2 additions & 0 deletions src/pds/peppi/orex/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Osiris Rex (OREX) tailored package for use with Peppi."""
from .products import OrexProducts # noqa
18 changes: 18 additions & 0 deletions src/pds/peppi/orex/products.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Main class of the Osiris Rex (OREX) Peppi package."""
from pds.peppi.client import PDSRegistryClient
from pds.peppi.orex.result_set import OrexResultSet


class OrexProducts(OrexResultSet):
"""Specialized Products class used to query specfically for Osiris Rex (OREX) products."""

def __init__(self, client: PDSRegistryClient):
"""Creates a new instance of OrexProducts.

Parameters
----------
client : PDSRegistryClient
Client defining the connection with the PDS Search API.

"""
super().__init__(client)
87 changes: 87 additions & 0 deletions src/pds/peppi/orex/result_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Module containing the Osiris Rex (OREX) tailored ResultSet class."""
from pds.peppi.client import PDSRegistryClient
from pds.peppi.result_set import ResultSet


class OrexResultSet(ResultSet):
"""Inherits the functionality of the ResultSet class, but adds implementations for stubs in QueryBuilder."""

orex_instrument_lidvid = "urn:nasa:pds:context:instrument:ovirs.orex"

def __init__(self, client: PDSRegistryClient):
"""Creates a new instance of OrexResultSet.

Parameters
----------
client : PDSRegistryClient
Client defining the connection with the PDS Search API.

"""
super().__init__(client)

# By default, all query results are filtered to just those applicable to
# the OREX instrument
self._q_string = f'ref_lid_instrument eq "{self.orex_instrument_lidvid}"'

def has_instrument(self, identifier: str):
"""Adds a query clause selecting products having an instrument matching the provided identifier.

Notes
-----
For OrexResultsSet, this method is not impemented since the instrument
is implicitly always fixed to Osiris Rex.

Parameters
----------
identifier : str
Identifier (LIDVID) of the instrument.

Raises
------
NotImplementedError

"""
raise NotImplementedError(f"Cannot specify an additional instrument on {self.__class__.__name__}")

def within_range(self, range_in_km: float):
"""Adds a query clause selecting products within the provided range value.

Parameters
----------
range_in_km : float
The range in kilometers to use with the query.

Returns
-------
This OrexResultSet instance with the "within range" filter applied.

"""
self._add_clause(f"orex:Spatial.orex:target_range le {range_in_km}")

return self

def within_bbox(self, lat_min: float, lat_max: float, lon_min: float, lon_max: float):
"""Adds a query clause selecting products which fall within the bounds of the provided bounding box.

Parameters
----------
lat_min : float
Minimum latitude boundary.
lat_max : float
Maximum latitude boundary.
lon_min : float
Minimum longitude boundary.
lon_max : float
Maximum longitude boundary.

Returns
-------
This OrexResultSet instance with the "within bounding box" filter applied.

"""
self._add_clause(f"orex:Spatial.orex:latitude ge {lat_min}")
self._add_clause(f"orex:Spatial.orex:latitude le {lat_max}")
self._add_clause(f"orex:Spatial.orex:longitude ge {lon_min}")
self._add_clause(f"orex:Spatial.orex:longitude le {lon_max}")

return self
89 changes: 66 additions & 23 deletions src/pds/peppi/query_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,15 @@ class QueryBuilder:
"""QueryBuilder provides method to elaborate complex PDS queries."""

def __init__(self):
"""Creates a new instance of the QueryBuilder class.

Parameters
----------
client: PDSRegistryClient
The client object used to interact with the PDS Registry API.

"""
"""Creates a new instance of the QueryBuilder class."""
self._q_string = ""
self._fields: list[str] = []

def __add_clause(self, clause):
def __str__(self):
"""Returns a formatted string representation of the current query."""
return "\n and".join(self._q_string.split("and"))

def _add_clause(self, clause):
"""Adds the provided clause to the query string to use on the next fetch of products from the Registry API.

Repeated calls to this method results in a joining with any previously
Expand Down Expand Up @@ -88,7 +85,7 @@ def has_target(self, identifier: str):

"""
clause = f'ref_lid_target eq "{identifier}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def has_investigation(self, identifier: str):
Expand All @@ -105,7 +102,7 @@ def has_investigation(self, identifier: str):

"""
clause = f'ref_lid_investigation eq "{identifier}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def before(self, dt: datetime):
Expand All @@ -123,7 +120,7 @@ def before(self, dt: datetime):
"""
iso8601_datetime = dt.isoformat().replace("+00:00", "Z")
clause = f'pds:Time_Coordinates.pds:start_date_time le "{iso8601_datetime}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def after(self, dt: datetime):
Expand All @@ -141,7 +138,7 @@ def after(self, dt: datetime):
"""
iso8601_datetime = dt.isoformat().replace("+00:00", "Z")
clause = f'pds:Time_Coordinates.pds:stop_date_time ge "{iso8601_datetime}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def of_collection(self, identifier: str):
Expand All @@ -158,7 +155,7 @@ def of_collection(self, identifier: str):

"""
clause = f'ops:Provenance.ops:parent_collection_identifier eq "{identifier}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def observationals(self):
Expand All @@ -170,7 +167,7 @@ def observationals(self):

"""
clause = 'product_class eq "Product_Observational"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def collections(self, collection_type: Optional[str] = None):
Expand All @@ -188,11 +185,11 @@ def collections(self, collection_type: Optional[str] = None):

"""
clause = 'product_class eq "Product_Collection"'
self.__add_clause(clause)
self._add_clause(clause)

if collection_type:
clause = f'pds:Collection.pds:collection_type eq "{collection_type}"'
self.__add_clause(clause)
self._add_clause(clause)

return self

Expand All @@ -205,7 +202,7 @@ def bundles(self):

"""
clause = 'product_class eq "Product_Bundle"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def has_instrument(self, identifier: str):
Expand All @@ -222,7 +219,7 @@ def has_instrument(self, identifier: str):

"""
clause = f'ref_lid_instrument eq "{identifier}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def has_instrument_host(self, identifier: str):
Expand All @@ -239,7 +236,7 @@ def has_instrument_host(self, identifier: str):

"""
clause = f'ref_lid_instrument_host eq "{identifier}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def has_processing_level(self, processing_level: PROCESSING_LEVELS = "raw"):
Expand All @@ -257,9 +254,55 @@ def has_processing_level(self, processing_level: PROCESSING_LEVELS = "raw"):

"""
clause = f'pds:Primary_Result_Summary.pds:processing_level eq "{processing_level.title()}"'
self.__add_clause(clause)
self._add_clause(clause)
return self

def within_range(self, range_in_km: float):
"""Adds a query clause selecting products within the provided range value.

Notes
-----
This method should be implemented by product-specific inheritors that
support the notion of range to a given target.

Parameters
----------
range_in_km : float
The range in kilometers to use with the query.

Raises
------
NotImplementedError

"""
raise NotImplementedError("within_range is not available for base QueryBuilder")

def within_bbox(self, lat_min: float, lat_max: float, lon_min: float, lon_max: float):
"""Adds a query clause selecting products which fall within the bounds of the provided bounding box.

Notes
-----
This method should be implemented by product-specific inheritors that
support the notion of bounding box to filter results by.

Parameters
----------
lat_min : float
Minimum latitude boundary.
lat_max : float
Maximum latitude boundary.
lon_min : float
Minimum longitude boundary.
lon_max : float
Maximum longitude boundary.

Raises
------
NotImplementedError

"""
raise NotImplementedError("within_bbox is not available for base QueryBuilder")

def get(self, identifier: str):
"""Adds a query clause selecting the product with a LIDVID matching the provided value.

Expand All @@ -273,7 +316,7 @@ def get(self, identifier: str):
This Products instance with the "LIDVID identifier" filter applied.

"""
self.__add_clause(f'lidvid like "{identifier}"')
self._add_clause(f'lidvid like "{identifier}"')
return self

def fields(self, fields: list):
Expand All @@ -293,5 +336,5 @@ def filter(self, clause: str):
-------
This Products instance with the provided filtering clause applied.
"""
self.__add_clause(clause)
self._add_clause(clause)
return self
4 changes: 4 additions & 0 deletions src/pds/peppi/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def __init__(self, client: PDSRegistryClient):
self._page_counter = None
self._expected_pages = None

def __str__(self):
"""Returns a string representation of the ResultSet, including the current query string."""
return f"Current {self.__class__.__name__} query:\n{super().__str__()}"

def _init_new_page(self):
"""Queries the PDS API for the next page of results.

Expand Down
24 changes: 0 additions & 24 deletions tests/pds/peppi/demo_candidates_A.py

This file was deleted.

21 changes: 0 additions & 21 deletions tests/pds/peppi/demo_candidates_B.py

This file was deleted.

Loading
Loading