Skip to content

Commit 1ae00e3

Browse files
Merge pull request #31 from goldlabelapps/staging
This pull request introduces new endpoints for reading prospect data and improves the documentation for the FastAPI/Postgres application.
2 parents 38d1216 + 524f19e commit 1ae00e3

3 files changed

Lines changed: 87 additions & 49 deletions

File tree

README.md

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
## I
1+
## I_Python
22

3-
> FastAPI/Python/Postgres/tsvector.
3+
> Python with FastAPI using Postgres & tsvector.
44
Open Source, production ready Python FastAPI/Postgres app for [NX](https://goldlabel.pro?s=python-nx-ai)
55

66
```sh
@@ -32,7 +32,8 @@ The API is at <http://localhost:8000>.
3232
The prospects table includes a `search_vector` column (type: tsvector) that is automatically computed from all text fields on insert. A GIN index is created for this column, enabling fast and scalable full-text search queries.
3333

3434
**How it works:**
35-
- On every insert (via `/prospects/seed` or `/prospects/process`), the `search_vector` is computed from all text columns using PostgreSQL's `to_tsvector('english', ...)`.
35+
36+
- On every insert or update, the `search_vector` is computed from all text columns using PostgreSQL's `to_tsvector('english', ...)`.
3637
- The GIN index (`idx_prospects_search_vector`) allows efficient search queries like:
3738

3839
```sql
@@ -45,52 +46,13 @@ This makes searching across all text fields in the prospects table extremely fas
4546
- **Pytest** — testing framework
4647
- **HTTPX / TestClient**
4748

49+
#### Docs
50+
4851
FastAPI automatically generates interactive documentation:
4952

5053
- Swagger UI: <http://localhost:8000/docs>
5154
- ReDoc: <http://localhost:8000/redoc>
5255

53-
#### Structure
54-
55-
```
56-
app/
57-
__init__.py
58-
main.py # FastAPI application entry point
59-
api/
60-
__init__.py
61-
routes.py # API endpoint definitions
62-
tests/
63-
__init__.py
64-
test_routes.py # Unit and integration tests
65-
requirements.txt
66-
```
67-
68-
69-
#### Endpoints
70-
71-
| Method | Path | Description |
72-
|--------|-----------|---------------------------------|
73-
| GET | `/` | Welcome message |
74-
| GET | `/health` | Health check — returns `ok` |
75-
| POST | `/echo` | Echoes the JSON `message` field |
76-
| GET | `/prospects/seed` | (Re)create prospects table and seed with sample data |
77-
| DELETE | `/prospects/process` | (Legacy) Empties the prospects table |
78-
| GET | `/prospects/process` | Process and insert all records from big.csv into prospects table |
79-
8056
### Processing Large CSV Files
8157

8258
The `/prospects/process` endpoint is designed for robust, scalable ingestion of large CSV files (e.g., 1300+ rows, 300KB+). It follows the same normalization and insertion pattern as `/prospects/seed`, but is optimized for large files:
83-
84-
85-
#### Example usage
86-
87-
1. Seed the table structure:
88-
- `GET /prospects/seed`
89-
2. (Optional) Empty the table:
90-
- `DELETE /prospects/empty`
91-
3. Process the large CSV:
92-
- `GET /prospects/process`
93-
94-
The endpoint will return the number of records inserted. This is the core ingestion workflow for production-scale data.
95-
96-

app/api/prospects/prospects.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,66 @@
1+
12
from app import __version__
23
import os
34
from app.utils.make_meta import make_meta
4-
from fastapi import APIRouter
5+
from fastapi import APIRouter, Query, Path
56
from app.utils.db import get_db_connection
67

78
router = APIRouter()
8-
99
base_url = os.getenv("BASE_URL", "http://localhost:8000")
1010

1111
@router.get("/prospects")
1212
def root() -> dict:
1313
"""GET /prospects endpoint."""
14-
meta = make_meta("success", "Prospects placeholder")
15-
data = {"init": f"{base_url}/prospects/init"}
14+
meta = make_meta("success", "Prospects start")
15+
data = [
16+
{"init": f"{base_url}/prospects/init"},
17+
{"single": f"{base_url}/prospects/101"},
18+
{"read": f"{base_url}/prospects/read"},
19+
]
1620
return {"meta": meta, "data": data}
1721

22+
23+
# endpoint: /prospects/read
24+
@router.get("/prospects/read")
25+
def prospects_read(
26+
page: int = Query(1, ge=1, description="Page number (1-based)"),
27+
limit: int = Query(50, ge=1, le=500, description="Records per page (default 50, max 500)")
28+
) -> dict:
29+
"""Read and return paginated rows from the prospects table."""
30+
meta = make_meta("success", "Read paginated prospects")
31+
conn_gen = get_db_connection()
32+
conn = next(conn_gen)
33+
cur = conn.cursor()
34+
offset = (page - 1) * limit
35+
try:
36+
cur.execute('SELECT COUNT(*) FROM prospects;')
37+
count_row = cur.fetchone() if cur.description is not None else None
38+
total = count_row[0] if count_row is not None else 0
39+
cur.execute(f'SELECT * FROM prospects OFFSET %s LIMIT %s;', (offset, limit))
40+
if cur.description is not None:
41+
columns = [desc[0] for desc in cur.description]
42+
rows = cur.fetchall()
43+
data = [dict(zip(columns, row)) for row in rows]
44+
else:
45+
data = []
46+
except Exception as e:
47+
data = []
48+
total = 0
49+
meta = make_meta("error", f"Failed to read prospects: {str(e)}")
50+
finally:
51+
cur.close()
52+
conn.close()
53+
return {
54+
"meta": meta,
55+
"pagination": {
56+
"page": page,
57+
"limit": limit,
58+
"total": total,
59+
"pages": (total // limit) + (1 if total % limit else 0)
60+
},
61+
"data": data,
62+
}
63+
1864
# endpoint: /prospects/init
1965
@router.get("/prospects/init")
2066
def prospects_init() -> dict:
@@ -98,3 +144,33 @@ def slugify(text):
98144
},
99145
}
100146
return {"meta": meta, "data": data}
147+
148+
149+
# endpoint: /prospects/{id}
150+
@router.get("/prospects/{id}")
151+
def prospects_read_one(id: int = Path(..., description="ID of the prospect to retrieve")) -> dict:
152+
"""Read and return a single prospect document by id."""
153+
meta = make_meta("success", f"Read prospect with id {id}")
154+
conn_gen = get_db_connection()
155+
conn = next(conn_gen)
156+
cur = conn.cursor()
157+
try:
158+
cur.execute('SELECT * FROM prospects WHERE id = %s;', (id,))
159+
if cur.description is not None:
160+
row = cur.fetchone()
161+
if row is not None:
162+
columns = [desc[0] for desc in cur.description]
163+
data = dict(zip(columns, row))
164+
else:
165+
data = None
166+
meta = make_meta("error", f"No prospect found with id {id}")
167+
else:
168+
data = None
169+
meta = make_meta("error", f"No prospect found with id {id}")
170+
except Exception as e:
171+
data = None
172+
meta = make_meta("error", f"Failed to read prospect: {str(e)}")
173+
finally:
174+
cur.close()
175+
conn.close()
176+
return {"meta": meta, "data": data}

app/utils/make_meta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ def make_meta(severity: str, title: str) -> dict:
1010
"severity": severity,
1111
"title": title,
1212
"version": __version__,
13-
"base_url": base_url,
13+
"endpoint": f"{base_url}/prospects",
1414
"time": epoch,
1515
}

0 commit comments

Comments
 (0)