Skip to content

Commit a3ac64a

Browse files
committed
Add prospects search endpoint and exclude hidden
Move the /prospects/search endpoint into a dedicated app/api/prospects/search.py router and register it in app/api/routes.py and app/api/prospects/__init__.py. Update prospects queries to ignore hidden records (WHERE hide IS NOT TRUE) for paginated listing, init aggregations, and single-item reads. Remove the inline search implementation from prospects.py and tidy imports. Bump package version to 2.0.4.
1 parent e4ca286 commit a3ac64a

5 files changed

Lines changed: 67 additions & 57 deletions

File tree

app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""NX AI - FastAPI, Python, Postgres, tsvector"""
22

33
# Current Version
4-
__version__ = "2.0.2"
4+
__version__ = "2.0.4"

app/api/prospects/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
"""Prospect Routes"""
22

33
from .prospects import router as prospects_router
4+
from .search import router as search_router

app/api/prospects/prospects.py

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ def prospects_read(
2424
page: int = Query(1, ge=1, description="Page number (1-based)"),
2525
limit: int = Query(50, ge=1, le=500, description="Records per page (default 50, max 500)")
2626
) -> dict:
27-
"""Read and return paginated rows from the prospects table."""
27+
"""Read and return paginated rows from the prospects table, excluding hidden."""
2828
meta = make_meta("success", "Read paginated prospects")
2929
conn_gen = get_db_connection()
3030
conn = next(conn_gen)
3131
cur = conn.cursor()
3232
offset = (page - 1) * limit
3333
try:
34-
cur.execute('SELECT COUNT(*) FROM prospects;')
34+
cur.execute('SELECT COUNT(*) FROM prospects WHERE hide IS NOT TRUE;')
3535
count_row = cur.fetchone() if cur.description is not None else None
3636
total = count_row[0] if count_row is not None else 0
37-
cur.execute(f'SELECT * FROM prospects OFFSET %s LIMIT %s;', (offset, limit))
37+
cur.execute(f'SELECT * FROM prospects WHERE hide IS NOT TRUE OFFSET %s LIMIT %s;', (offset, limit))
3838
if cur.description is not None:
3939
columns = [desc[0] for desc in cur.description]
4040
rows = cur.fetchall()
@@ -59,61 +59,16 @@ def prospects_read(
5959
"data": data,
6060
}
6161

62-
from typing import Optional
62+
6363

6464
# Schema for update
6565
from pydantic import BaseModel
66+
from typing import Optional
6667

6768
class ProspectUpdate(BaseModel):
6869
flag: Optional[bool] = None
6970
hide: Optional[bool] = None
7071

71-
# endpoint: /prospects/search
72-
@router.get("/prospects/search")
73-
def prospects_search(query: Optional[str] = Query(None, description="Search query string"),
74-
page: int = Query(1, ge=1, description="Page number (1-based)"),
75-
limit: int = Query(50, ge=1, le=500, description="Records per page (default 50, max 500)")) -> dict:
76-
"""Search prospects using full-text search on search_vector column."""
77-
meta = make_meta("success", f"Search prospects for query: {query}")
78-
data = []
79-
total = 0
80-
if not query or not query.strip():
81-
meta = make_meta("error", "Query parameter is required for search.")
82-
return {"meta": meta, "data": [], "pagination": {"page": page, "limit": limit, "total": 0, "pages": 0}}
83-
conn_gen = get_db_connection()
84-
conn = next(conn_gen)
85-
cur = conn.cursor()
86-
offset = (page - 1) * limit
87-
try:
88-
# Count total matches
89-
cur.execute("SELECT COUNT(*) FROM prospects WHERE search_vector @@ plainto_tsquery('english', %s);", (query,))
90-
count_row = cur.fetchone() if cur.description is not None else None
91-
total = count_row[0] if count_row is not None else 0
92-
# Fetch paginated results
93-
cur.execute("SELECT * FROM prospects WHERE search_vector @@ plainto_tsquery('english', %s) OFFSET %s LIMIT %s;", (query, offset, limit))
94-
if cur.description is not None:
95-
columns = [desc[0] for desc in cur.description]
96-
rows = cur.fetchall()
97-
data = [dict(zip(columns, row)) for row in rows]
98-
else:
99-
data = []
100-
except Exception as e:
101-
meta = make_meta("error", f"Search failed: {str(e)}")
102-
data = []
103-
total = 0
104-
finally:
105-
cur.close()
106-
conn.close()
107-
return {
108-
"meta": meta,
109-
"pagination": {
110-
"page": page,
111-
"limit": limit,
112-
"total": total,
113-
"pages": (total // limit) + (1 if total % limit else 0)
114-
},
115-
"data": data,
116-
}
11772

11873
# endpoint: /prospects/init
11974
@router.get("/prospects/init")
@@ -130,12 +85,12 @@ def prospects_init() -> dict:
13085
sub_departments = []
13186
total_unique_sub_departments = 0
13287
try:
133-
cur.execute('SELECT COUNT(*) FROM prospects;')
88+
cur.execute('SELECT COUNT(*) FROM prospects WHERE hide IS NOT TRUE;')
13489
row = cur.fetchone()
13590
total = row[0] if row is not None else 0
13691

13792
# Get unique titles and their counts (column is 'title')
138-
cur.execute('SELECT title, COUNT(*) FROM prospects WHERE title IS NOT NULL GROUP BY title ORDER BY COUNT(*) DESC;')
93+
cur.execute('SELECT title, COUNT(*) FROM prospects WHERE title IS NOT NULL AND hide IS NOT TRUE GROUP BY title ORDER BY COUNT(*) DESC;')
13994
title_rows = cur.fetchall()
14095
def slugify(text):
14196
import re
@@ -151,7 +106,7 @@ def slugify(text):
151106
total_unique_title = len(title)
152107

153108
# Get unique seniority and their counts (column is 'seniority')
154-
cur.execute('SELECT seniority, COUNT(*) FROM prospects WHERE seniority IS NOT NULL GROUP BY seniority ORDER BY COUNT(*) DESC;')
109+
cur.execute('SELECT seniority, COUNT(*) FROM prospects WHERE seniority IS NOT NULL AND hide IS NOT TRUE GROUP BY seniority ORDER BY COUNT(*) DESC;')
155110
seniority_rows = cur.fetchall()
156111
seniority = [
157112
{"label": str(s[0]), "value": slugify(s[0])}
@@ -161,7 +116,7 @@ def slugify(text):
161116
total_unique_seniority = len(seniority)
162117

163118
# Get unique sub_departments and their counts (column is 'sub_departments')
164-
cur.execute('SELECT sub_departments, COUNT(*) FROM prospects WHERE sub_departments IS NOT NULL GROUP BY sub_departments ORDER BY COUNT(*) DESC;')
119+
cur.execute('SELECT sub_departments, COUNT(*) FROM prospects WHERE sub_departments IS NOT NULL AND hide IS NOT TRUE GROUP BY sub_departments ORDER BY COUNT(*) DESC;')
165120
sub_department_rows = cur.fetchall()
166121
sub_departments = [
167122
{"label": str(sd[0]), "value": slugify(sd[0])}
@@ -205,13 +160,13 @@ def slugify(text):
205160
# endpoint: /prospects/{id}
206161
@router.get("/prospects/{id}")
207162
def prospects_read_one(id: int = Path(..., description="ID of the prospect to retrieve")) -> dict:
208-
"""Read and return a single prospect document by id."""
163+
"""Read and return a single prospect document by id, unless hidden."""
209164
meta = make_meta("success", f"Read prospect with id {id}")
210165
conn_gen = get_db_connection()
211166
conn = next(conn_gen)
212167
cur = conn.cursor()
213168
try:
214-
cur.execute('SELECT * FROM prospects WHERE id = %s;', (id,))
169+
cur.execute('SELECT * FROM prospects WHERE id = %s AND hide IS NOT TRUE;', (id,))
215170
if cur.description is not None:
216171
row = cur.fetchone()
217172
if row is not None:

app/api/prospects/search.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from fastapi import APIRouter, Query
2+
from typing import Optional
3+
from app.utils.make_meta import make_meta
4+
from app.utils.db import get_db_connection
5+
6+
router = APIRouter()
7+
8+
@router.get("/prospects/search")
9+
def prospects_search(query: str = Query(..., description="Search query string"),
10+
page: int = Query(1, ge=1, description="Page number (1-based)"),
11+
limit: int = Query(50, ge=1, le=500, description="Records per page (default 50, max 500)")) -> dict:
12+
"""Search prospects using full-text search on search_vector column, excluding hidden."""
13+
meta = make_meta("success", f"Search prospects for query: {query}")
14+
data = []
15+
total = 0
16+
if not query or not query.strip():
17+
meta = make_meta("error", "Query parameter is required for search.")
18+
return {"meta": meta, "data": [], "pagination": {"page": page, "limit": limit, "total": 0, "pages": 0}}
19+
conn_gen = get_db_connection()
20+
conn = next(conn_gen)
21+
cur = conn.cursor()
22+
offset = (page - 1) * limit
23+
try:
24+
# Count total matches
25+
cur.execute("SELECT COUNT(*) FROM prospects WHERE search_vector @@ plainto_tsquery('english', %s) AND hide IS NOT TRUE;", (query,))
26+
count_row = cur.fetchone() if cur.description is not None else None
27+
total = count_row[0] if count_row is not None else 0
28+
# Fetch paginated results
29+
cur.execute("SELECT * FROM prospects WHERE search_vector @@ plainto_tsquery('english', %s) AND hide IS NOT TRUE OFFSET %s LIMIT %s;", (query, offset, limit))
30+
if cur.description is not None:
31+
columns = [desc[0] for desc in cur.description]
32+
rows = cur.fetchall()
33+
data = [dict(zip(columns, row)) for row in rows]
34+
else:
35+
data = []
36+
except Exception as e:
37+
meta = make_meta("error", f"Search failed: {str(e)}")
38+
data = []
39+
total = 0
40+
finally:
41+
cur.close()
42+
conn.close()
43+
return {
44+
"meta": meta,
45+
"pagination": {
46+
"page": page,
47+
"limit": limit,
48+
"total": total,
49+
"pages": (total // limit) + (1 if total % limit else 0)
50+
},
51+
"data": data,
52+
}

app/api/routes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from app.api.health import router as health_router
1515
from app.api.prompts.prompts import router as prompts_router
1616
from app.api.prospects.prospects import router as prospects_router
17+
from app.api.prospects.search import router as prospects_search_router
1718
from app.api.prospects.database.alter import router as prospects_alter_router
1819
from app.api.prospects.database.seed import router as prospects_seed_router
1920
from app.api.prospects.database.empty import router as prospects_empty_router
@@ -22,6 +23,7 @@
2223
router.include_router(root_router)
2324
router.include_router(health_router)
2425
router.include_router(prompts_router)
26+
router.include_router(prospects_search_router)
2527
router.include_router(prospects_router)
2628
router.include_router(prospects_alter_router)
2729
router.include_router(prospects_seed_router)

0 commit comments

Comments
 (0)