Demo Project
Build a practical Python project that combines core programming concepts, libraries, APIs, and backend development.
The OSDC demo-api repository is a small FastAPI project with a Python command-line client. It demonstrates how a client sends HTTP requests to a backend and reads JSON responses.
demo-api/
|-- main.py
|-- demo.py
|-- requirements.txt
|-- README.md
main.pydefines the FastAPI server.demo.pyis a terminal client usingrequests.requirements.txtinstallsfastapi[standard]andrequests.
Running the Demo
Create and activate a virtual environment:
python -m venv .venv
On Windows PowerShell:
.venv\Scripts\Activate.ps1
On macOS or Linux:
source .venv/bin/activate
Install dependencies:
pip install -r requirements.txt
Start the API from the repository directory:
fastapi dev
The API runs at:
http://127.0.0.1:8000
Keep the server running. In a second terminal, run:
python demo.py
The client displays a menu and sends requests to the running API.
Application State
The demo stores data in Python dictionaries:
creds = {}
data = {}
The credentials dictionary maps usernames to passwords:
username -> password
The data dictionary maps a heading to its description and owner:
heading -> [description, username]
This data is stored only in memory. It disappears when the API restarts. A production application should use a database and should never store plain-text passwords.
Request Models
main.py uses Pydantic models to describe and validate JSON request bodies.
class LoginData(BaseModel):
username: str
password: str
class UploadData(LoginData):
heading: str
description: str
class UpdateData(LoginData):
heading: str
new_heading: str
new_description: str
class DeleteData(LoginData):
heading: str
The upload, update, and delete models inherit username and password from LoginData.
A valid upload body is:
{
"username": "osdc-member",
"password": "example-password",
"heading": "Python",
"description": "A programming language used in the backend workshop."
}
If a required field is missing, FastAPI returns a validation error before the route runs.
API Endpoints
Root Endpoint
The root route checks whether the API is running:
@app.get("/")
async def root():
return {"message": "Hello World"}
Request:
GET http://127.0.0.1:8000/
Response:
{
"message": "Hello World"
}
View All Data
The public /data route returns all stored entries:
@app.get("/data")
async def get_data():
results = []
for heading in data:
description, username = data[heading]
results.append([heading, description, username])
return {"data": results}
Request:
GET http://127.0.0.1:8000/data
Example response:
{
"data": [
[
"Python",
"A programming language used in the backend workshop.",
"osdc-member"
]
]
}
The demo uses lists inside the response list. A production API would usually use named objects because they are easier for frontend code to read:
{
"data": [
{
"heading": "Python",
"description": "A programming language used in the backend workshop.",
"username": "osdc-member"
}
]
}
Search Data
The /search/{query} route uses a path parameter and searches both headings and descriptions without considering letter case.
@app.get("/search/{query}")
async def search_data(query: str):
results = []
for heading in data:
description, username = data[heading]
if (
query.lower() in heading.lower()
or query.lower() in description.lower()
):
results.append([heading, description, username])
return {"data": results}
Example request:
GET http://127.0.0.1:8000/search/python
Login and Registration
The /login route registers a new username or logs in an existing username:
@app.post("/login")
async def login_user(data: LoginData):
correct_password = creds.get(data.username)
if correct_password is None:
creds[data.username] = data.password
return {"message": "Register successful"}
if correct_password == data.password:
return {"message": "Login successful"}
raise HTTPException(status_code=401, detail="Password Incorrect")
The behavior is:
- A new username is registered automatically.
- The correct password logs in an existing username.
- The wrong password returns status
401.
Example request:
curl -X POST http://127.0.0.1:8000/login \
-H "Content-Type: application/json" \
-d "{\"username\": \"osdc-member\", \"password\": \"example-password\"}"
This is a teaching example. A real application should hash passwords, store users in a database, and use a proper session or token system.
Upload Data
The /upload route authenticates the user and stores a new entry:
@app.post("/upload")
async def upload_data(new_data: UploadData):
login(new_data.username, new_data.password)
data[new_data.heading] = [
new_data.description,
new_data.username
]
return {
"message": "Data uploaded successfully",
"data": data
}
Request body:
{
"username": "osdc-member",
"password": "example-password",
"heading": "FastAPI",
"description": "A Python framework for building APIs."
}
Update Data
The /update route:
- Validates credentials.
- Checks that the original heading exists.
- Checks that the authenticated user owns it.
- Checks that a new heading is not already used.
- Replaces the old dictionary entry.
@app.post("/update")
async def update_data(new_data: UpdateData):
login(new_data.username, new_data.password)
if new_data.heading not in data:
raise HTTPException(status_code=404, detail="Heading not found")
description, username = data[new_data.heading]
if username != new_data.username:
raise HTTPException(status_code=403, detail="Not your heading")
if (
new_data.new_heading != new_data.heading
and new_data.new_heading in data
):
raise HTTPException(status_code=409, detail="Heading already exists")
data.pop(new_data.heading)
data[new_data.new_heading] = [
new_data.new_description,
new_data.username
]
return {
"message": "Data updated successfully",
"data": data
}
The route uses these status codes:
401: invalid credentials404: original heading does not exist403: the user does not own the heading409: the new heading is already used
Delete Data
The /delete route uses the same authentication and ownership checks before removing an entry:
@app.post("/delete")
async def delete_data(delete_request: DeleteData):
login(delete_request.username, delete_request.password)
if delete_request.heading not in data:
raise HTTPException(status_code=404, detail="Heading not found")
description, username = data[delete_request.heading]
if username != delete_request.username:
raise HTTPException(status_code=403, detail="Not your heading")
data.pop(delete_request.heading)
return {
"message": "Data deleted successfully",
"data": data
}
The demo uses POST /delete with a JSON body. A production API might use DELETE /data/{heading} and handle authentication separately.
Authentication Helper
The login() helper centralizes credential validation:
def login(username: str, password: str):
correct_password = creds.get(username)
if correct_password is None:
raise HTTPException(status_code=401, detail="Incorrect Username!")
if correct_password != password:
raise HTTPException(status_code=401, detail="Incorrect Password!")
Upload, update, and delete call this helper instead of repeating credential checks. A larger FastAPI application could implement this behavior as a dependency using Depends().
The Python Client
demo.py uses requests and stores the server address in one constant:
import requests
BASE_URL = "http://127.0.0.1:8000"
The client functions call these endpoints:
| Client function | Request | Endpoint |
|---|---|---|
upload_data() |
POST |
/upload |
view_all() |
GET |
/data |
view_by_heading() |
GET |
/data |
update_data() |
POST |
/update |
delete_data() |
POST |
/delete |
The client converts JSON responses into Python values with response.json():
response = requests.get(f"{BASE_URL}/data")
items = response.json()["data"]
It checks the status code before displaying success or an error:
if response.status_code == 200:
print(response.json()["message"])
else:
print(f"Error: {response.json()}")
The menu repeatedly asks for a choice and calls the matching function:
while True:
print("[1] Upload data")
print("[2] View all data")
print("[3] View data by heading")
print("[4] Update data")
print("[5] Delete data")
print("[6] Exit")
choice = input("Choose an option: ")
if choice == "1":
upload_data()
elif choice == "2":
view_all()
elif choice == "3":
view_by_heading()
elif choice == "4":
update_data()
elif choice == "5":
delete_data()
elif choice == "6":
break
else:
print("Invalid choice.")
The client collects input and sends HTTP requests. The server validates requests, applies rules, changes data, and returns responses.
Important Client-Server Mismatch
The current main.py requires username and password in UploadData, UpdateData, and DeleteData. However, the current demo.py sends only the heading and description for upload, and only heading fields for update and delete.
The current client sends an upload body like this:
{
"heading": "Python",
"description": "A programming language."
}
The current server model requires this instead:
{
"username": "osdc-member",
"password": "example-password",
"heading": "Python",
"description": "A programming language."
}
Therefore, the checked-in client and server are currently out of sync. The client must collect credentials and include them in protected requests, or the server models and authentication behavior must be changed.
A corrected upload request would look like this:
response = requests.post(
f"{BASE_URL}/upload",
json={
"username": "osdc-member",
"password": "example-password",
"heading": heading,
"description": description
}
)
The same credentials must be added to update and delete request bodies.
Request Flow
An upload request travels through the application like this:
demo.py
|
| POST /upload with JSON
v
FastAPI route
|
| Pydantic validates UploadData
v
login() validates credentials
|
| data[heading] is updated
v
JSON response
|
v
demo.py reads response.json()
This is the same basic pattern used when a JavaScript frontend calls a FastAPI backend with fetch().
Endpoint Summary
| Method | Endpoint | Authentication | Purpose |
|---|---|---|---|
GET |
/ |
No | Check that the API is running |
GET |
/data |
No | Return all stored entries |
GET |
/search/{query} |
No | Search headings and descriptions |
POST |
/login |
Credentials in body | Register or log in a user |
POST |
/upload |
Required in body | Create an entry |
POST |
/update |
Required in body | Update an owned entry |
POST |
/delete |
Required in body | Delete an owned entry |
The interactive documentation is available at:
```text
http://127.0.0.1:8000/docs
