Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
19 changes: 14 additions & 5 deletions agave/chalice/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
from pydantic import BaseModel, ValidationError

from ..core.blueprints.decorators import copy_attributes
from ..core.query_params import (
EmptyQueryMapping,
query_params_for_url,
validate_query_params,
)


class RestApiBlueprint(Blueprint):
Expand Down Expand Up @@ -238,9 +243,13 @@ def query():
next_page = <url_for_next_items>
}
"""
params = self.current_request.query_params or dict()
query_mapping = (
self.current_request.query_params or EmptyQueryMapping()
)
try:
query_params = cls.query_validator(**params)
query_params = validate_query_params(
query_mapping, cls.query_validator
)
except ValidationError as e:
return Response(e.json(), status_code=400)

Expand Down Expand Up @@ -296,11 +305,11 @@ def _all(query: QueryParams, filters: Q):
if wants_more and has_more:
query.created_before = item_dicts[-1]['created_at']
path = self.current_request.context['resourcePath']
params = query.model_dump()
params = query_params_for_url(query)
if self.user_id_filter_required():
params.pop('user_id')
params.pop('user_id', None)
if self.platform_id_filter_required():
params.pop('platform_id')
params.pop('platform_id', None)
next_page_uri = f'{path}?{urlencode(params)}'
return dict(items=item_dicts, next_page_uri=next_page_uri)

Expand Down
11 changes: 11 additions & 0 deletions agave/core/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,30 @@
from mongoengine import Q


def _ids_filter_value(ids: str | list[str]) -> list[str]:
if isinstance(ids, str):
return [part.strip() for part in ids.split(',') if part.strip()]
return list(ids)


def generic_query(query: QueryParams, excluded: list[str] = []) -> Q:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix mutable default argument.

The default argument excluded: list[str] = [] is a mutable object that is shared across all function calls. If any caller mutates this list, it will affect subsequent calls. Replace with None and initialize inside the function.

🔧 Proposed fix
-def generic_query(query: QueryParams, excluded: list[str] = []) -> Q:
+def generic_query(query: QueryParams, excluded: list[str] | None = None) -> Q:
+    if excluded is None:
+        excluded = []
     filters = Q()

As per coding guidelines, the static analysis tool Ruff flagged this with rule B006: "Do not use mutable data structures for argument defaults."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def generic_query(query: QueryParams, excluded: list[str] = []) -> Q:
def generic_query(query: QueryParams, excluded: list[str] | None = None) -> Q:
if excluded is None:
excluded = []
filters = Q()
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 11-11: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agave/core/filters.py` at line 11, The function generic_query currently uses
a mutable default argument excluded: list[str] = []; change the signature to use
excluded: Optional[list[str]] = None and inside generic_query initialize
excluded = [] if excluded is None to avoid sharing the same list across calls;
update any type hints/imports as needed and ensure all places that reference
excluded still work with the possibly None input before initialization.

filters = Q()
if query.created_before:
filters &= Q(created_at__lt=query.created_before)
if query.created_after:
filters &= Q(created_at__gt=query.created_after)
ids = getattr(query, 'ids', None)
if ids is not None:
id_list = _ids_filter_value(ids)
filters &= Q(id__in=id_list) if id_list else Q(id__in=[])
exclude_fields = {
'created_before',
'created_after',
'active',
'limit',
'page_size',
'key',
'ids',
*excluded,
}
fields = query.model_dump(exclude=exclude_fields)
Expand Down
75 changes: 75 additions & 0 deletions agave/core/query_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

from types import UnionType
from typing import Any, Iterator, TypeVar, Union, get_args, get_origin

from pydantic import BaseModel

ModelT = TypeVar('ModelT', bound=BaseModel)


def comma_separated_list(value: str | None) -> list[str]:
if not value:
return []
return [part.strip() for part in value.split(',') if part.strip()]


def _is_list_annotation(annotation: Any) -> bool:
origin = get_origin(annotation)
if origin is list:
return True
if origin in (Union, UnionType):
return any(
_is_list_annotation(arg)
for arg in get_args(annotation)
if arg is not type(None)
)
return False


def build_query_dict(
query_mapping: Any, model_cls: type[BaseModel]
) -> dict[str, Any]:
params: dict[str, Any] = {}
for name in query_mapping:
raw = query_mapping.get(name)
if name in model_cls.model_fields:
field = model_cls.model_fields[name]
if _is_list_annotation(field.annotation):
if raw is None:
continue
if isinstance(raw, str):
params[name] = comma_separated_list(raw)
else:
params[name] = list(raw)
else:
params[name] = raw
else:
params[name] = raw
return params


def validate_query_params(
query_mapping: Any, model_cls: type[ModelT]
) -> ModelT:
return model_cls(**build_query_dict(query_mapping, model_cls))


def query_params_for_url(query: BaseModel) -> dict[str, Any]:
params = query.model_dump()
for name, field in type(query).model_fields.items():
value = params.get(name)
if _is_list_annotation(field.annotation) and isinstance(value, list):
params[name] = ','.join(value)
return params
Comment on lines +58 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

❓ Verification inconclusive

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from cuenca_validations.types import QueryParams
for n, f in QueryParams.model_fields.items():
    print(n, f.default, f.annotation)
PY
git log -n 5 --oneline -- agave/fastapi/rest_api.py
git show HEAD~1:agave/fastapi/rest_api.py 2>/dev/null | sed -n '430,445p'

Repository: cuenca-mx/agave

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate QueryParams in-repo
fd -a "query_params.py" -t f . || true
rg -n "class QueryParams|QueryParams\s*=" . || true
rg -n "query_params_for_url\(" agave || true

# Inspect the exact urlencode/next_page_uri code path
rg -n "next_page_uri|urlencode\(" agave/fastapi/rest_api.py agave/fastapi -S || true

# Show the current function implementation and surrounding context
rg -n "def query_params_for_url" agave/core/query_params.py -n
sed -n '1,140p' agave/core/query_params.py

# Show the relevant portion of rest_api.py (around the urlencode/next_page_uri)
sed -n '380,520p' agave/fastapi/rest_api.py || true

Repository: cuenca-mx/agave

Length of output: 6974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the QueryParams model definition
rg -n "class QueryParams|QueryParams\s*=" . -S

# Inspect the tests for query_params_for_url / next_page_uri
sed -n '1,220p' tests/core/test_query_params.py 2>/dev/null || true

# If QueryParams is defined elsewhere, print a smaller window around it
# (Uses file discovery first)
query_file="$(rg -l "class QueryParams" . -S | head -n 1 || true)"
if [ -n "${query_file}" ]; then
  echo "QueryParams file: $query_file"
  # Print around the class definition
  rg -n "class QueryParams" "$query_file" -n
  line="$(rg -n "class QueryParams" "$query_file" | head -n 1 | cut -d: -f1)"
  start=$((line-30))
  end=$((line+220))
  sed -n "${start},${end}p" "$query_file"
fi

# Also inspect query_params_for_url-focused tests, if any
rg -n "query_params_for_url|next_page_uri" tests/core/test_query_params.py tests -S || true

Repository: cuenca-mx/agave

Length of output: 41


🏁 Script executed:

#!/bin/bash
set -u
set +e

rg -n "class QueryParams|QueryParams\s*=" . -S

echo "---- tests/core/test_query_params.py ----"
ls -la tests/core/test_query_params.py 2>/dev/null || ls -la ./tests/core/test_query_params.py 2>/dev/null
sed -n '1,240p' tests/core/test_query_params.py 2>/dev/null || sed -n '1,240p' ./tests/core/test_query_params.py 2>/dev/null

echo "---- search for next_page_uri / query_params_for_url in tests ----"
rg -n "query_params_for_url|next_page_uri" tests -S || true

Repository: cuenca-mx/agave

Length of output: 3331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the QueryParams model definition
rg -n "class QueryParams\b" agave -S || true
rg -n "QueryParams\(" agave -S || true

# Search for likely field names to find the model
rg -n "created_after|created_before|page_size|platform_id|user_id|limit:" agave -S || true
rg -n "created_after|created_before|page_size|platform_id|user_id|limit" agave -S --glob='*.py'

echo "---- inspect pagination tests around next_page_uri ----"
sed -n '250,620p' tests/blueprint/test_blueprint.py

Repository: cuenca-mx/agave

Length of output: 23536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find QueryParams definition across whole repo
rg -n "class QueryParams\b" . -S || true
rg -n "QueryParams\s*=\s*|QueryParams\s*\(" . -S || true

# Search for core filter logic to see which fields are used and how
sed -n '1,160p' agave/core/filters.py

# Inspect next-page tests that may expose inclusion of None params
rg -n "next_page_uri" tests/blueprint/test_blueprint.py -n
sed -n '280,430p' tests/blueprint/test_blueprint.py
sed -n '430,590p' tests/blueprint/test_blueprint.py

# Inspect blueprint models / query param models in tests folder
rg -n "created_before|created_after|page_size|limit:" tests -S --glob='*.py'
rg -n "platform_id|user_id" tests -S --glob='*.py'

Repository: cuenca-mx/agave

Length of output: 17364


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' tests/core/test_query_params.py
sed -n '220,500p' tests/core/test_query_params.py

# Search for expectations about "None" in query params / next_page_uri
rg -n "\"None\"|limit=None|created_before=None|created_after=None|exclude_none" tests -S || true

# Inspect agave/core/query_params.py to see if model_dump is customized by config elsewhere
sed -n '1,120p' agave/core/query_params.py

Repository: cuenca-mx/agave

Length of output: 4231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any Pydantic config/default that might exclude None during model_dump
rg -n "exclude_none" agave tests -S --glob='*.py' || true

# Search for field definitions used by pagination (created_before/after, page_size, limit, etc.)
rg -n "created_before|created_after|page_size|limit:" . -S --glob='*.py' || true

# Inspect the QueryParams reference type in tests (import path indicates external package)
rg -n "from cuenca_validations\.types import QueryParams" -S . || true

# Also check if agave provides its own QueryParams somewhere else (besides external)
rg -n "QueryParams\s*:" agave -S --glob='*.py' || true
rg -n "class .*QueryParams" . -S --glob='*.py' || true

Repository: cuenca-mx/agave

Length of output: 2722


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any model_dump override in this repo
rg -n "def model_dump\(" . -S --glob='*.py' || true

# Inspect query param field declarations in examples (subclasses of cuenca_validations.types.QueryParams)
sed -n '1,220p' examples/validators.py

# Inspect imports/top of rest_api.py to confirm urlencode source
sed -n '1,120p' agave/fastapi/rest_api.py
sed -n '400,470p' agave/fastapi/rest_api.py

# Inspect chalice version too (also uses query_params_for_url)
sed -n '240,360p' agave/chalice/rest_api.py

Repository: cuenca-mx/agave

Length of output: 11511


🌐 Web query:

cuenca_validations types QueryParams model_dump exclude_none

💡 Result:

In Pydantic (and libraries utilizing it, such as cuenca-validations), model_dump is a method used to convert a model instance into a dictionary [1][2]. When using the exclude_none=True argument, all fields with a value of None are excluded from the output [1][3]. This functionality is a standard feature of Pydantic v2 [4][3]. In the context of QueryParams or similar objects, calling.model_dump(exclude_none=True) is a common pattern to clean up request or filtering data, ensuring that parameters explicitly set to None are not included in the resulting dictionary, which helps prevent issues when constructing subsequent queries or API requests [1][4]. While cuenca-validations is a specialized library used by the Cuenca organization for common shared validations [5][6], it relies on Pydantic v2 [6][7]. Therefore, its models behave like standard Pydantic models, and you should use the official Pydantic documentation for behaviors related to model_dump and field exclusion [1][2][8]. Important Considerations: - Precedence: When using both include and exclude_none=True in model_dump, the exclusion logic often takes precedence, meaning even if you explicitly include a field, it may be dropped if its value is None [4]. - Alternatives: If you need to keep specific fields even if they are None, you may need to use a model_serializer to explicitly handle that logic [4]. - Dependency: Ensure your environment uses a compatible version of Pydantic (v2.10.0 or higher is typically used in recent versions of cuenca-validations) [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where cuenca-validations is pinned
rg -n "cuenca-validations|cuenca_validations" pyproject.toml requirements*.txt setup.cfg setup.py poetry.lock Pipfile.lock -S || true

# Print pyproject/build metadata sections that might include dependencies
ls -ლა
ls -la pyproject.toml 2>/dev/null || true
sed -n '1,220p' pyproject.toml 2>/dev/null || true

Repository: cuenca-mx/agave

Length of output: 408


🌐 Web query:

cuenca-validations QueryParams class model_dump exclude_none

💡 Result:

The cuenca-validations library is a shared Python package used across various Cuenca projects [1][2]. The QueryParams class within this library is a Pydantic model (inheriting from BaseModel) used to standardize query parameter handling [1][3][4]. When you invoke model_dump(exclude_none=True) on a QueryParams instance, Pydantic's native behavior is applied [5][3]. This method generates a dictionary representation of the model while omitting any fields that have a value of None [3]. If you are encountering issues where this behavior does not meet your specific requirements (such as needing to exclude None values while still including certain fields, or vice versa), Pydantic provides several mechanisms to customize serialization [5][6]: 1. model_serializer: You can use the @model_serializer decorator to define custom logic for how the model is dumped [5][6]. 2. Field(exclude_if=...): In modern Pydantic versions, you can use the exclude_if argument within a Field definition to specify fine-grained control over when a field should be excluded from serialization [6]. 3. Configuration: You can adjust the model's ConfigDict to modify default serialization behaviors if necessary [6]. For technical implementation questions specific to your project, check the source code in the cuenca-validations repository on GitHub, as it relies heavily on standard Pydantic functionality [2][4].

Citations:


🌐 Web query:

site:github.com cuenca-mx/cuenca-validations "class QueryParams" model_config exclude_none model_dump

💡 Result:

I can’t find any source that shows the exact Cuenca type definition you referenced (the file path and the snippet: class QueryParams with model_config, exclude_none, and model_dump). The available search results only point to the repository and an unrelated issue/discussion, without exposing the current QueryParams class implementation. [1][2][3] What I did find (related but not your requested snippet): there is a GitHub issue in this repo about “move related_transaction out of QueryParams”, which implies QueryParams exists and is located under cuenca_validations/types/queries ..., but the issue snippet doesn’t include the model_config / exclude_none / model_dump code you asked for. [1] If you can share either (a) the specific GitHub file URL you’re looking at (or the commit SHA), or (b) the repository version/tag you care about, I can re-run the search targeted to that exact file/line range and extract the exact answer you want.

Citations:


Exclude None values from query_params_for_url before building next_page_uri

query_params_for_url() uses query.model_dump() (no exclude_none), and pagination builds next_page_uri via urlencode(params). If the dumped dict includes Optional fields set to None, they can end up serialized as "None" in the URL (e.g., limit=None&created_after=None), which can break downstream parsing/validation.

Exclude None before serialization:

🐛 Proposed fix
 def query_params_for_url(query: BaseModel) -> dict[str, Any]:
-    params = query.model_dump()
+    params = query.model_dump(exclude_none=True)
     for name, field in type(query).model_fields.items():
         value = params.get(name)
         if _is_list_annotation(field.annotation) and isinstance(value, list):
             params[name] = ','.join(value)
     return params
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agave/core/query_params.py` around lines 58 - 64, The function
query_params_for_url currently uses query.model_dump() without excluding None,
which lets Optional fields become the string "None" when later urlencoded;
change it to omit None-valued keys by calling
query.model_dump(exclude_none=True) (or filter out keys with value is None from
the params dict) before the list-annotation handling so that Optional fields are
not serialized into the query string; keep the existing _is_list_annotation and
join logic intact after the None-filtering.



class EmptyQueryMapping:
def __contains__(self, key: str) -> bool:
return False

def __iter__(self) -> Iterator[str]:
return iter(())

def get(self, key: str, default: Any = None) -> Any:
return default
11 changes: 7 additions & 4 deletions agave/fastapi/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from ..core.blueprints.decorators import copy_attributes
from ..core.exc import NotFoundError, UnprocessableEntity
from ..core.query_params import query_params_for_url, validate_query_params

SAMPLE_404 = {
"summary": "Not found item",
Expand Down Expand Up @@ -358,7 +359,9 @@ class QueryResponse(BaseModel):

def validate_params(request: Request):
try:
return cls.query_validator(**request.query_params)
return validate_query_params(
request.query_params, cls.query_validator
)
except ValidationError as e:
raise UnprocessableEntity(e.json())

Expand Down Expand Up @@ -430,11 +433,11 @@ async def _all(query: QueryParams, filters: Q, resource_path: str):
next_page_uri: Optional[str] = None
if wants_more and has_more:
query.created_before = item_dicts[-1]['created_at']
params = query.model_dump()
params = query_params_for_url(query)
if self.user_id_filter_required():
params.pop('user_id')
params.pop('user_id', None)
if self.platform_id_filter_required():
params.pop('platform_id')
params.pop('platform_id', None)
next_page_uri = f'{resource_path}?{urlencode(params)}'
return dict(items=item_dicts, next_page_uri=next_page_uri)

Expand Down
2 changes: 1 addition & 1 deletion agave/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.5.4'
__version__ = '1.5.4.dev3'
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
boto3==1.35.74
types-boto3[sqs]==1.35.74
cuenca-validations==2.1.3
cuenca-validations==2.1.35.dev2
chalice==1.31.3
mongoengine==0.29.1
fastapi==0.115.11
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
packages=find_packages(),
include_package_data=True,
package_data=dict(agave=['py.typed']),
python_requires='>=3.9',
python_requires='>=3.10',
install_requires=[
'cuenca-validations>=2.1.0,<3.0.0',
'mongoengine>=0.29.0,<0.30.0',
Expand Down Expand Up @@ -54,7 +54,6 @@
],
},
classifiers=[
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
Expand Down
37 changes: 37 additions & 0 deletions tests/blueprint/test_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,43 @@ def test_query_count_resource(
assert json_body['count'] == 1


@pytest.mark.parametrize(
"client_fixture", ["fastapi_client", "chalice_client"]
)
@pytest.mark.usefixtures('accounts')
def test_query_with_comma_separated_ids_param(
client_fixture: str,
request: pytest.FixtureRequest,
) -> None:
client = request.getfixturevalue(client_fixture)
resp = client.get('/accounts?ids=US1,US2')
assert resp.status_code == 200
assert 'items' in resp.json()


@pytest.mark.parametrize(
"client_fixture", ["fastapi_client", "chalice_client"]
)
@pytest.mark.usefixtures('accounts')
def test_query_pagination_preserves_comma_separated_ids(
client_fixture: str,
request: pytest.FixtureRequest,
accounts: list[Account],
) -> None:
client = request.getfixturevalue(client_fixture)
account_ids = [accounts[0].id, accounts[1].id]
ids_param = ','.join(account_ids)
resp = client.get(f'/accounts?ids={ids_param}&page_size=1&limit=10')
assert resp.status_code == 200
json_body = resp.json()
next_page_uri = json_body['next_page_uri']
assert next_page_uri is not None
assert f'ids={ids_param}' in next_page_uri.replace('%2C', ',')

resp = client.get(next_page_uri)
assert resp.status_code == 200


@pytest.mark.parametrize(
"client_fixture", ["fastapi_client", "chalice_client"]
)
Expand Down
67 changes: 67 additions & 0 deletions tests/core/test_query_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import Optional

import pytest
from pydantic import BaseModel, ConfigDict, ValidationError
from starlette.datastructures import QueryParams

from agave.core.query_params import (
EmptyQueryMapping,
comma_separated_list,
query_params_for_url,
validate_query_params,
)


class SampleQuery(BaseModel):
model_config = ConfigDict(extra='forbid')

ids: Optional[list[str]] = None
name: Optional[str] = None
active: Optional[bool] = None


def test_comma_separated_list() -> None:
assert comma_separated_list('a,b') == ['a', 'b']
assert comma_separated_list('a, b ,c') == ['a', 'b', 'c']
assert comma_separated_list('US1') == ['US1']
assert comma_separated_list('') == []
assert comma_separated_list(None) == []


def test_validate_query_params_comma_separated_ids() -> None:
query = QueryParams('ids=a,b&name=Frida')
validated = validate_query_params(query, SampleQuery)
assert validated.ids == ['a', 'b']
assert validated.name == 'Frida'


def test_validate_query_params_scalar_field() -> None:
query = QueryParams('name=Frida')
validated = validate_query_params(query, SampleQuery)
assert validated.name == 'Frida'
assert validated.ids is None


def test_validate_query_params_rejects_unknown_fields() -> None:
query = QueryParams('wrong_param=value')
with pytest.raises(ValidationError):
validate_query_params(query, SampleQuery)


def test_empty_query_mapping() -> None:
mapping = EmptyQueryMapping()
assert 'x' not in mapping
assert mapping.get('x') is None
assert mapping.get('x', 'default') == 'default'
assert list(mapping) == []


def test_validate_query_params_empty_mapping() -> None:
validated = validate_query_params(EmptyQueryMapping(), SampleQuery)
assert validated.ids is None
assert validated.name is None


def test_query_params_for_url_serializes_list_as_comma_separated() -> None:
query = SampleQuery(ids=['a', 'b'], name='Frida')
assert query_params_for_url(query)['ids'] == 'a,b'
Comment on lines +65 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider asserting None handling in query_params_for_url.

This test only checks the list field. Adding an assertion that None-valued fields are not emitted as "None" would lock in the serialization contract and guard the pagination-URL path.

💚 Suggested addition
 def test_query_params_for_url_serializes_list_as_comma_separated() -> None:
     query = SampleQuery(ids=['a', 'b'], name='Frida')
-    assert query_params_for_url(query)['ids'] == 'a,b'
+    params = query_params_for_url(query)
+    assert params['ids'] == 'a,b'
+    assert 'active' not in params  # None fields must not leak as "None"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/test_query_params.py` around lines 65 - 67, The test only checks
list serialization but not None-handling; update
test_query_params_for_url_serializes_list_as_comma_separated to also assert that
None-valued fields from SampleQuery (e.g., a field like cursor or other optional
attribute) are not emitted as the string "None" by calling
query_params_for_url(query) and asserting the result either does not contain
that key or that result.get('cursor') is None, ensuring query_params_for_url
preserves omission of None values rather than serializing them to "None".

Loading