From b00075ab87f0e100bae689d268992bf1710dcc4c Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 25 May 2024 20:53:48 +0200 Subject: [PATCH] Added backend support to perform exact matches --- spoolman/api/v1/filament.py | 15 ++++++--- spoolman/api/v1/spool.py | 17 ++++++---- spoolman/api/v1/vendor.py | 8 +++-- spoolman/database/utils.py | 10 ++++++ tests_integration/tests/filament/test_find.py | 32 +++++++++++++++++-- 5 files changed, 66 insertions(+), 16 deletions(-) diff --git a/spoolman/api/v1/filament.py b/spoolman/api/v1/filament.py index 1444d1f..555ae84 100644 --- a/spoolman/api/v1/filament.py +++ b/spoolman/api/v1/filament.py @@ -174,7 +174,8 @@ async def find( title="Vendor Name", description=( "Partial case-insensitive search term for the filament vendor name. " - "Separate multiple terms with a comma. Specify an empty string to match filaments with no vendor name." + "Separate multiple terms with a comma. Specify an empty string to match filaments with no vendor name. " + "Surround a term with quotes to search for the exact term." ), ), vendor_id: Optional[str] = Query( @@ -193,7 +194,8 @@ async def find( title="Filament Name", description=( "Partial case-insensitive search term for the filament name. Separate multiple terms with a comma. " - "Specify an empty string to match filaments with no name." + "Specify an empty string to match filaments with no name. " + "Surround a term with quotes to search for the exact term." ), ), material: Optional[str] = Query( @@ -201,7 +203,8 @@ async def find( title="Filament Material", description=( "Partial case-insensitive search term for the filament material. Separate multiple terms with a comma. " - "Specify an empty string to match filaments with no material." + "Specify an empty string to match filaments with no material. " + "Surround a term with quotes to search for the exact term." ), ), article_number: Optional[str] = Query( @@ -210,7 +213,8 @@ async def find( description=( "Partial case-insensitive search term for the filament article number. " "Separate multiple terms with a comma. " - "Specify an empty string to match filaments with no article number." + "Specify an empty string to match filaments with no article number. " + "Surround a term with quotes to search for the exact term." ), ), color_hex: Optional[str] = Query( @@ -231,7 +235,8 @@ async def find( description=( "Find filaments imported by the given external ID. " "Separate multiple IDs with a comma. " - "Specify empty string to match filaments with no external ID." + "Specify empty string to match filaments with no external ID. " + "Surround a term with quotes to search for the exact term." ), example="polymaker_pla_polysonicblack_1000_175", ), diff --git a/spoolman/api/v1/spool.py b/spoolman/api/v1/spool.py index 8cc8a28..aea65e3 100644 --- a/spoolman/api/v1/spool.py +++ b/spoolman/api/v1/spool.py @@ -170,8 +170,9 @@ async def find( default=None, title="Filament Name", description=( - "Partial case-insensitive search term for the filament name. Separate multiple terms with a comma." - " Specify an empty string to match spools with no filament name." + "Partial case-insensitive search term for the filament name. Separate multiple terms with a comma. " + "Specify an empty string to match spools with no filament name. " + "Surround a term with quotes to search for the exact term." ), ), filament_id: Optional[str] = Query( @@ -188,7 +189,8 @@ async def find( title="Filament Material", description=( "Partial case-insensitive search term for the filament material. Separate multiple terms with a comma. " - "Specify an empty string to match spools with no filament material." + "Specify an empty string to match spools with no filament material. " + "Surround a term with quotes to search for the exact term." ), ), filament_vendor_name: Optional[str] = Query( @@ -197,7 +199,8 @@ async def find( title="Vendor Name", description=( "Partial case-insensitive search term for the filament vendor name. Separate multiple terms with a comma. " - "Specify an empty string to match spools with no vendor name." + "Specify an empty string to match spools with no vendor name. " + "Surround a term with quotes to search for the exact term." ), ), filament_vendor_id: Optional[str] = Query( @@ -216,7 +219,8 @@ async def find( title="Location", description=( "Partial case-insensitive search term for the spool location. Separate multiple terms with a comma. " - "Specify an empty string to match spools with no location." + "Specify an empty string to match spools with no location. " + "Surround a term with quotes to search for the exact term." ), ), lot_nr: Optional[str] = Query( @@ -224,7 +228,8 @@ async def find( title="Lot/Batch Number", description=( "Partial case-insensitive search term for the spool lot number. Separate multiple terms with a comma. " - "Specify an empty string to match spools with no lot nr." + "Specify an empty string to match spools with no lot nr. " + "Surround a term with quotes to search for the exact term." ), ), allow_archived: bool = Query( diff --git a/spoolman/api/v1/vendor.py b/spoolman/api/v1/vendor.py index 01233e8..14bc150 100644 --- a/spoolman/api/v1/vendor.py +++ b/spoolman/api/v1/vendor.py @@ -83,7 +83,10 @@ async def find( name: Optional[str] = Query( default=None, title="Vendor Name", - description="Partial case-insensitive search term for the vendor name. Separate multiple terms with a comma.", + description=( + "Partial case-insensitive search term for the vendor name. Separate multiple terms with a comma. " + "Surround a term with quotes to search for the exact term." + ), ), external_id: Optional[str] = Query( default=None, @@ -91,7 +94,8 @@ async def find( description=( "Exact match for the vendor external ID. " "Separate multiple IDs with a comma. " - "Specify empty string to match filaments with no external ID." + "Specify empty string to match filaments with no external ID. " + "Surround a term with quotes to search for the exact term." ), ), sort: Optional[str] = Query( diff --git a/spoolman/database/utils.py b/spoolman/database/utils.py index 5002e68..cd54d9b 100644 --- a/spoolman/database/utils.py +++ b/spoolman/database/utils.py @@ -47,9 +47,14 @@ def add_where_clause_str_opt( if value is not None: conditions = [] for value_part in value.split(","): + # If part is empty, search for empty fields if len(value_part) == 0: conditions.append(field.is_(None)) conditions.append(field == "") + # Do exact match if value_part is surrounded by quotes + elif value_part[0] == '"' and value_part[-1] == '"': + conditions.append(field == value_part[1:-1]) + # Do fuzzy match if value_part is not surrounded by quotes else: conditions.append(field.ilike(f"%{value_part}%")) @@ -66,8 +71,13 @@ def add_where_clause_str( if value is not None: conditions = [] for value_part in value.split(","): + # If part is empty, search for empty fields if len(value_part) == 0: conditions.append(field == "") + # Do exact match if value_part is surrounded by quotes + elif value_part[0] == '"' and value_part[-1] == '"': + conditions.append(field == value_part[1:-1]) + # Do fuzzy match if value_part is not surrounded by quotes else: conditions.append(field.ilike(f"%{value_part}%")) diff --git a/tests_integration/tests/filament/test_find.py b/tests_integration/tests/filament/test_find.py index d2f477e..44299ae 100644 --- a/tests_integration/tests/filament/test_find.py +++ b/tests_integration/tests/filament/test_find.py @@ -61,7 +61,7 @@ def filaments(random_vendor_mod: dict[str, Any], random_empty_vendor_mod: dict[s f"{URL}/api/v1/filament", json={ "name": "Filament Z", - "material": "PETG", + "material": "PLA+", "price": 200, "density": 1.25, "diameter": 1.75, @@ -256,7 +256,20 @@ def test_find_filaments_by_name(filaments: Fixture): # Execute result = httpx.get( f"{URL}/api/v1/filament", - params={"name": "Filament X"}, + params={"name": "Filament"}, + ) + result.raise_for_status() + + # Verify + filaments_result = result.json() + assert_lists_compatible(filaments_result, filaments.filaments[:3]) + + +def test_find_filaments_by_exact_name(filaments: Fixture): + # Execute + result = httpx.get( + f"{URL}/api/v1/filament", + params={"name": '"Filament X"'}, ) result.raise_for_status() @@ -301,7 +314,20 @@ def test_find_filaments_by_multiple_materials(filaments: Fixture): # Verify filaments_result = result.json() - assert_lists_compatible(filaments_result, filaments.filaments[:2]) + assert_lists_compatible(filaments_result, filaments.filaments[:3]) + + +def test_find_filaments_by_exact_material(filaments: Fixture): + # Execute + result = httpx.get( + f"{URL}/api/v1/filament", + params={"material": '"PLA"'}, + ) + result.raise_for_status() + + # Verify + filaments_result = result.json() + assert_lists_compatible(filaments_result, filaments.filaments[:1]) def test_find_filaments_by_empty_material(filaments: Fixture):