Skip to content

feat(queries): implement coordinate order validation and UserWarnings for spatial queries (#66)#105

Open
angel-cm-dev wants to merge 2 commits intonasa:developfrom
angel-cm-dev:develop
Open

feat(queries): implement coordinate order validation and UserWarnings for spatial queries (#66)#105
angel-cm-dev wants to merge 2 commits intonasa:developfrom
angel-cm-dev:develop

Conversation

@angel-cm-dev
Copy link
Copy Markdown

🚀 Overview

This Pull Request addresses Issue #66 by implementing proactive validation for coordinate ordering in bounding_box and polygon methods within the GranuleQuery class.

The primary goal is to prevent common user errors—such as flipped latitude/longitude pairs—that lead to unintentional antimeridian crossings or empty API results. By providing immediate feedback via UserWarning, we improve the developer experience and reduce unnecessary load on the CMR API.

✅ Key Changes

  • Data Sanitization: Implemented explicit float() casting for all spatial parameters. This ensures type safety and prevents potential string-injection issues in the generated query parameters.
  • Bounding Box Intelligence: Added logic to detect if lower_left_lon > upper_right_lon or if latitudes are flipped, triggering a UserWarning with clear descriptive feedback.
  • Polygon Analysis: Added a check for the longitude span of polygons. If the span exceeds 180 degrees, a warning is issued to alert the user about possible coordinate flipping or intended antimeridian crossings.
  • Code Standards: The code has been refactored to comply with PEP 8 standards using Ruff. Clean code principles (SOLID) were applied to keep the methods readable and maintainable.

🧪 Testing & Verification

I have performed rigorous testing to ensure the robustness of these changes:

  1. Unit Testing: Created tests/test_issue_66.py utilizing unittest.TestCase.assertWarns. This verifies that warnings are triggered exactly when expected and silenced during valid queries.
  2. Integration (NASA UAT): Manually verified the fix against the NASA CMR UAT environment (cmr.uat.earthdata.nasa.gov).
    • Verified that the generated URLs are correctly formatted (e.g., bounding_box=10.0,0.0,-10.0,5.0).
    • Confirmed the library gracefully handles empty results from the API after the warning is issued.
  3. Environment: Tests were executed on Python 3.14.3 on Windows 11, ensuring compatibility with the latest Python releases.

🔗 Related Issues

Closes #66

Copy link
Copy Markdown
Contributor

@frankinspace frankinspace left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution. Looks reasonable to me, just one comment to move the tests into the existing test case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These can move into the test_granule.py test case.

Copy link
Copy Markdown
Collaborator

@chuckwondo chuckwondo left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution. In the future, please avoid including various code formatting changes, as this PR is rather noisy with such changes, making it hard to easily see the meaningful changes you contributed.

from cmr import queries


class TestIssue66(unittest.TestCase):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should be no need to use unittest.TestCase. The methods can simply be top-level test functions. Then change the self.assertIn calls to standard assert statements.


class TestIssue66(unittest.TestCase):
def test_wkt_coordinate_order_warning(self):
# 1. Usamos coordenadas que cruzan casi todo el mapa (Span > 180)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English comments, please.

class TestIssue66(unittest.TestCase):
def test_wkt_coordinate_order_warning(self):
# 1. Usamos coordenadas que cruzan casi todo el mapa (Span > 180)
# De 170 a -170 hay una diferencia de 340 grados.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English


query = queries.GranuleQuery()

# 2. Ahora sí debería dispararse el UserWarning
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English

self.assertIn("longitude span is greater than 180 degrees", str(cm.warning))

def test_bounding_box_order_warning(self):
# Este ya pasaba, lo mantenemos igual
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English

@angel-cm-dev
Copy link
Copy Markdown
Author

I've addressed all the feedback in the latest commit (78ff562):

All warning messages and internal comments have been translated to English.
Tests from test_issue_66.py were refactored into top-level functions and migrated to test_granule.py.
The redundant test_issue_66.py file has been deleted.
Verified that all 60 tests pass locally.

Please let me know if any further adjustments are needed. Ready for a new review!

"""
Ensure a warning is raised when coordinates appear to be in the wrong order (span > 180).
"""
# Usamos coordenadas que cruzan el antimeridiano (span > 180)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

English comments please

Comment on lines +625 to +645
def test_wkt_coordinate_order_warning():
"""
Ensure a warning is raised when coordinates appear to be in the wrong order (span > 180).
"""
# Usamos coordenadas que cruzan el antimeridiano (span > 180)
flipped_coords = [(170, 10), (-170, 10), (-170, -10), (170, -10), (170, 10)]
query = GranuleQuery()

# Verificamos que se dispare el UserWarning configurado en queries.py
with pytest.warns(UserWarning, match="longitude span is greater than 180 degrees"):
query.polygon(flipped_coords)

def test_bounding_box_order_warning():
"""
Verify warning for incorrect bounding box coordinate order.
"""
query = GranuleQuery()

# Verificamos la alerta de cruce de antimeridiano definida en queries.py
with pytest.warns(UserWarning, match="crosses the antimeridian"):
query.bounding_box(10, 0, -10, 5)
Copy link
Copy Markdown
Contributor

@frankinspace frankinspace Apr 8, 2026

Choose a reason for hiding this comment

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

Indentation appears to be incorrect

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's based on an earlier comment I made, indicating that these test functions can be top level test functions rather than within the test case class, but that was before I saw that all of the tests in this file are within the class.

I'm fine with these new functions also being methods within the class, but we should probably separate such methods into top level functions (in a separate PR) because most of the test methods in this class do not require vcrpy, so placing them within the class doesn't make sense (although does no harm either).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah I see your point. Agreed it can be refactored

"""
query = GranuleQuery()

# Verificamos la alerta de cruce de antimeridiano definida en queries.py
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English comments, please.

flipped_coords = [(170, 10), (-170, 10), (-170, -10), (170, -10), (170, 10)]
query = GranuleQuery()

# Verificamos que se dispare el UserWarning configurado en queries.py
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English, please.


query.readable_granule_name(["*a*", "*b*"])
self.assertEqual(query.params[self.readable_granule_name], ["*a*", "*b*"])
# Asegúrate de que no haya espacios antes de 'def'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

English, please. Also, please add a blank line above to separate it from the preceding method.

Comment on lines +625 to +645
def test_wkt_coordinate_order_warning():
"""
Ensure a warning is raised when coordinates appear to be in the wrong order (span > 180).
"""
# Usamos coordenadas que cruzan el antimeridiano (span > 180)
flipped_coords = [(170, 10), (-170, 10), (-170, -10), (170, -10), (170, 10)]
query = GranuleQuery()

# Verificamos que se dispare el UserWarning configurado en queries.py
with pytest.warns(UserWarning, match="longitude span is greater than 180 degrees"):
query.polygon(flipped_coords)

def test_bounding_box_order_warning():
"""
Verify warning for incorrect bounding box coordinate order.
"""
query = GranuleQuery()

# Verificamos la alerta de cruce de antimeridiano definida en queries.py
with pytest.warns(UserWarning, match="crosses the antimeridian"):
query.bounding_box(10, 0, -10, 5)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's based on an earlier comment I made, indicating that these test functions can be top level test functions rather than within the test case class, but that was before I saw that all of the tests in this file are within the class.

I'm fine with these new functions also being methods within the class, but we should probably separate such methods into top level functions (in a separate PR) because most of the test methods in this class do not require vcrpy, so placing them within the class doesn't make sense (although does no harm either).

self.params["bounding_box"] = f"{ll_lon},{ll_lat},{ur_lon},{ur_lat}"
return self

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Check for correct WKT coordinate ordering

3 participants