Skip to content

Commit 2cddaa5

Browse files
authored
feat: add search_query parameter to operations api (#1539)
1 parent 05c293f commit 2cddaa5

File tree

6 files changed

+97
-27
lines changed

6 files changed

+97
-27
lines changed

docs/OperationsAPI.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ paths:
2727
tags:
2828
- "operations"
2929
parameters:
30+
- name: search_query
31+
in: query
32+
description: General search query to match against feed stable id, feed name and feed provider.
33+
required: False
34+
schema:
35+
type: string
3036
- name: operation_status
3137
in: query
3238
description: Filter feeds by operational status.

functions-python/helpers/query_helper.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44
from typing import Type
55

6-
from sqlalchemy import and_, func
6+
from sqlalchemy import and_, func, or_
77
from sqlalchemy.orm import Session, joinedload
88
from sqlalchemy.orm.query import Query
99

@@ -75,6 +75,7 @@ def get_eager_loading_options(model: Type[Feed]):
7575

7676
def get_feeds_query(
7777
db_session: Session,
78+
search_query: str | None = None,
7879
operation_status: str | None = None,
7980
data_type: str | None = None,
8081
limit: int | None = None,
@@ -86,6 +87,7 @@ def get_feeds_query(
8687
8788
Args:
8889
db_session: SQLAlchemy session
90+
search_query: Optional general search query
8991
operation_status: Optional filter for operational status (wip or published)
9092
data_type: Optional filter for feed type (gtfs or gtfs_rt)
9193
limit: Maximum number of items to return
@@ -103,17 +105,27 @@ def get_feeds_query(
103105
)
104106
conditions = []
105107

106-
if data_type is None:
108+
if data_type is None or len(data_type.strip()) == 0:
107109
conditions.append(model.data_type.in_(["gtfs", "gtfs_rt"]))
108110
logging.info("Added filter to exclude gbfs feeds")
109111
else:
110112
conditions.append(model.data_type == data_type)
111113
logging.info("Added data_type filter: %s", data_type)
112114

113-
if operation_status:
115+
if operation_status and operation_status.strip():
114116
conditions.append(model.operational_status == operation_status)
115117
logging.info("Added operational_status filter: %s", operation_status)
116118

119+
if search_query and search_query.strip():
120+
search_pattern = f"%{search_query.strip()}%"
121+
conditions.append(
122+
or_(
123+
model.stable_id.ilike(search_pattern),
124+
model.feed_name.ilike(search_pattern),
125+
model.provider.ilike(search_pattern),
126+
)
127+
)
128+
logging.info("Added search_query filter: %s", search_query)
117129
query = db_session.query(model)
118130
logging.info("Created base query with model %s", model.__name__)
119131

functions-python/operations_api/function_config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
}
1919
],
2020
"ingress_settings": "ALLOW_ALL",
21-
"max_instance_request_concurrency": 1,
22-
"max_instance_count": 5,
21+
"max_instance_request_concurrency": 100,
22+
"max_instance_count": 10,
2323
"min_instance_count": 0,
2424
"available_cpu": 1,
2525
"build_settings": {

functions-python/operations_api/src/feeds_operations/impl/feeds_operations_impl.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ def assert_no_existing_feed_url(producer_url: str, db_session: Session):
109109
)
110110

111111
@with_db_session
112-
async def get_feeds(
112+
def handle_get_feeds(
113113
self,
114+
search_query: Optional[str] = None,
114115
operation_status: Optional[str] = None,
115116
data_type: Optional[str] = None,
116117
offset: str = "0",
@@ -122,8 +123,21 @@ async def get_feeds(
122123
limit_int = int(limit) if limit else 50
123124
offset_int = int(offset) if offset else 0
124125

126+
# filtered but unpaginated for total
127+
total_query = get_feeds_query(
128+
db_session=db_session,
129+
search_query=search_query,
130+
operation_status=operation_status,
131+
data_type=data_type,
132+
limit=None,
133+
offset=None,
134+
model=Feed,
135+
)
136+
total = total_query.count()
137+
125138
query = get_feeds_query(
126139
db_session=db_session,
140+
search_query=search_query,
127141
operation_status=operation_status,
128142
data_type=data_type,
129143
limit=limit_int,
@@ -133,14 +147,10 @@ async def get_feeds(
133147

134148
logging.info("Executing query with data_type: %s", data_type)
135149

136-
total = query.count()
137150
feeds = query.all()
138151
logging.info("Retrieved %d feeds from database", len(feeds))
139152

140-
feed_list = []
141-
for feed in feeds:
142-
feed_list.append(OperationFeedImpl.from_orm(feed))
143-
153+
feed_list = [OperationFeedImpl.from_orm(feed) for feed in feeds]
144154
response = GetFeeds200Response(
145155
total=total, offset=offset_int, limit=limit_int, feeds=feed_list
146156
)
@@ -153,6 +163,20 @@ async def get_feeds(
153163
status_code=500, detail=f"Internal server error: {str(e)}"
154164
)
155165

166+
async def get_feeds(
167+
self,
168+
search_query: Optional[str] = None,
169+
operation_status: Optional[str] = None,
170+
data_type: Optional[str] = None,
171+
offset: str = "0",
172+
limit: str = "50",
173+
db_session: Session = None,
174+
) -> GetFeeds200Response:
175+
"""Get a list of feeds with optional filtering and pagination."""
176+
return self.handle_get_feeds(
177+
search_query, operation_status, data_type, offset, limit
178+
)
179+
156180
@with_db_session
157181
async def get_gtfs_feed(
158182
self,

functions-python/operations_api/tests/conftest.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@
3838
stable_id="mdb-41",
3939
status="active",
4040
feed_contact_email="feed_contact_email",
41-
provider="provider",
41+
provider="provider A",
4242
entitytypes=[Entitytype(name="vp")],
43+
operational_status="published",
4344
)
4445

4546
feed_mdb_40 = Gtfsfeed(
@@ -56,7 +57,7 @@
5657
stable_id="mdb-40",
5758
status="active",
5859
feed_contact_email="feed_contact_email",
59-
provider="provider",
60+
provider="provider B",
6061
gtfs_rt_feeds=[feed_mdb_41],
6162
operational_status="wip",
6263
)
@@ -74,8 +75,9 @@
7475
stable_id="mdb-400",
7576
status="active",
7677
feed_contact_email="feed_contact_email",
77-
provider="provider",
78+
provider="provider C",
7879
gtfs_rt_feeds=[],
80+
operational_status="published",
7981
)
8082

8183
# Test license objects used by LicensesApiImpl tests

functions-python/operations_api/tests/feeds_operations/impl/test_get_feeds.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,21 @@ async def test_get_feeds_pagination():
8787
api = OperationsApiImpl()
8888

8989
response = await api.get_feeds(limit=1)
90-
assert response.total == 1
90+
assert response.total == 3
9191
assert response.limit == 1
9292
assert response.offset == 0
9393
assert len(response.feeds) == 1
9494
first_feed = response.feeds[0]
9595

9696
response = await api.get_feeds(offset=1, limit=1)
97-
assert response.total == 1
97+
assert response.total == 3
9898
assert response.limit == 1
9999
assert response.offset == 1
100100
assert len(response.feeds) == 1
101101
assert response.feeds[0].stable_id != first_feed.stable_id
102102

103103
response = await api.get_feeds(offset=3)
104-
assert response.total == 0
104+
assert response.total == 3
105105
assert response.limit == 50
106106
assert response.offset == 3
107107
assert len(response.feeds) == 0
@@ -149,26 +149,20 @@ async def test_get_feeds_combined_filters():
149149

150150
base_response = await api.get_feeds()
151151
assert base_response is not None
152-
print(f"\nTotal feeds in database: {len(base_response.feeds)}")
153152

154153
gtfs_response = await api.get_feeds(data_type="gtfs")
155154
assert gtfs_response is not None
156-
print(f"Total GTFS feeds: {len(gtfs_response.feeds)}")
157-
for feed in gtfs_response.feeds:
158-
print(f"GTFS Feed: {feed.stable_id}, status: {feed.operational_status}")
159155

160156
wip_response = await api.get_feeds(operation_status="wip")
161157
assert wip_response is not None
162-
print(f"Total WIP feeds: {len(wip_response.feeds)}")
163-
for feed in wip_response.feeds:
164-
print(f"WIP Feed: {feed.stable_id}, type: {feed.data_type}")
165158

166-
response = await api.get_feeds(data_type="gtfs", operation_status="wip")
159+
response = await api.get_feeds(data_type="gtfs", operation_status="published")
167160
assert response is not None
168161
wip_gtfs_feeds = response.feeds
169-
print(f"Total WIP GTFS feeds: {len(wip_gtfs_feeds)}")
170162

171-
assert len(wip_gtfs_feeds) == 0
163+
assert len(wip_gtfs_feeds) == 1
164+
assert wip_gtfs_feeds[0].data_type == "gtfs"
165+
assert wip_gtfs_feeds[0].operational_status == "published"
172166

173167
response = await api.get_feeds(data_type="gtfs", limit=1, offset=1)
174168
assert response is not None
@@ -231,3 +225,35 @@ async def test_get_feeds_unpublished_with_data_type():
231225
for feed in rt_response.feeds:
232226
assert feed.operational_status == "unpublished"
233227
assert feed.data_type == "gtfs_rt"
228+
229+
230+
@pytest.mark.asyncio
231+
async def test_get_feeds_search_query():
232+
"""
233+
Test get_feeds endpoint with search query filter.
234+
Should return only feeds matching the search query.
235+
"""
236+
api = OperationsApiImpl()
237+
238+
response = await api.get_feeds(search_query="RT")
239+
assert response is not None
240+
assert response.total == 1
241+
assert len(response.feeds) == 1
242+
assert response.feeds[0].feed_name == "London Transit Commission(RT"
243+
244+
response = await api.get_feeds(search_query=" Provider B ")
245+
assert response is not None
246+
assert response.total == 1
247+
assert len(response.feeds) == 1
248+
assert response.feeds[0].provider == "provider B"
249+
250+
response = await api.get_feeds(search_query="mdb-41")
251+
assert response is not None
252+
assert response.total == 1
253+
assert len(response.feeds) == 1
254+
assert response.feeds[0].stable_id == "mdb-41"
255+
256+
response = await api.get_feeds(search_query="mdb")
257+
assert response is not None
258+
assert response.total == 3
259+
assert len(response.feeds) == 3

0 commit comments

Comments
 (0)