Build a Reliable Inventory Sync API
Build a FastAPI service that safely applies inventory events once with SQLite.
Introduction
30 Second Summary
When a store sends the same stock update twice, a system can count the same delivery twice. The recorded quantity drifts away from reality.
In this project, you will build a retail inventory REST API with FastAPI as Part 1 of App Dev. You will reproduce a duplicate-event bug before making repeated deliveries safe.
What You'll Build
You submit one stock event twice through interactive documentation to see the replay return applied: false with the quantity unchanged.
By the end of this project, you'll have:
- An interactive stock workflow that accepts a stock event. You can query the resulting quantity for its store and SKU.
- A safe replay demonstration where the same event returns applied: false on its second submission. The quantity stays unchanged.
- A test-backed project whose README explains the demo. Its pytest suite reports four passing tests.
- Secret Mission: Add a rollback-safe business rule that rejects any event that would make inventory negative.
Are there any prerequisites?
A Windows computer is enough for this project. The guide sets up Python and Visual Studio Code from scratch.
Before We Start
Before any hands-on work, lock in the API's promise: duplicate event delivery must never change inventory twice. A delayed or lost store-system response can trigger a retry, which your demo handles by returning applied: false without changing stored stock.
Set Up the Windows Workspace
Your duplicate-event demo depends on every later command using the same tested runtime. A reproducible workspace prevents machine-wide package conflicts from changing the API's behavior.
You'll prepare Visual Studio Code with Python 3.14.7. A virtual environment will isolate the project's pinned dependencies.
In this step, get ready to:
- Prepare Visual Studio Code with Python 3.14.7.
- Activate an isolated .venv environment inside reliable-inventory-sync-api.
- Install the pinned dependencies from requirements.txt.
Prepare the editor and runtime
Visual Studio Code gives you one place for the project files and terminal. Python runs the API code that you build in the next step.
- Press the Windows key to open Windows search.
- Type Visual Studio Code.
- Press Enter.
✔️ Visual Studio Code opens
Visual Studio Code is ready for your project workspace.
ⓧ Visual Studio Code is missing
- Visit the official Visual Studio Code setup page.
- Download the User setup installer for Windows.
- Run the downloaded installer.
- Complete the installer using its default options.
- Press the Windows key after installation finishes.
- Type Visual Studio Code.
- Press Enter.
Installation not completing?
Close any running Visual Studio Code windows before retrying the installer. Use the help me troubleshoot the Visual Studio Code User setup link if the setup still fails.
A PowerShell window gives you a quick way to check which Python runtimes are available before you create the project folder.
- Press the Windows key to open Windows search.
- Type Windows PowerShell.
- Press Enter.
- List the available Python runtimes by running this command:
py list
What does this check?
The py list command asks Python Install Manager to show the runtimes available on your computer. The result tells you whether the project runtime is ready.
✔️ I see Python 3.14.7
Python 3.14.7 is available for the project.
ⓧ I see an older version
Your existing runtime cannot provide the exact environment used by this project. Python Install Manager can add the current Python 3.14 release.
- Install Python 3.14 by running this command:
py install 3.14
What does this command do?
Python Install Manager downloads the current Python 3.14 runtime. The project can then use Python 3.14.7 without replacing unrelated project environments.
- Confirm the installed runtime by running the check again:
py list
What should you see?
The runtime list should now include Python 3.14.7.
🙋♀️ Python installation failing?
Check that your internet connection is active before retrying the installation. Ask for help with a failed Python 3.14 installation if the command still fails.
ⓧ Command not found
Windows needs Python Install Manager before it can run the py command.
- Visit the official Python downloads page.
- Download Python Install Manager for Windows.
- Run the downloaded installer.
- Complete the installation prompts.
- Close the current PowerShell window.
- Open a new PowerShell window through Windows search.
- Install Python 3.14 by running this command:
py install 3.14
What does this command do?
Python Install Manager downloads the current Python 3.14 runtime. Opening a new PowerShell window also makes the newly installed command available.
- Confirm the installed runtime by running this command:
py list
What should you see?
The runtime list should include Python 3.14.7.
🙋♀️ Still seeing command not found?
Restart Windows so PowerShell can load the updated command path. Ask for help finding Python Install Manager after installation if the command remains unavailable.
The project folder gives every file a predictable home. Opening that folder as the Visual Studio Code workspace also makes its integrated terminal start in the correct location.
- Open File Explorer.
- Select Desktop in the left navigation.
- Select New in the toolbar.
- Select Folder.
- Enter reliable-inventory-sync-api as the folder name.
- Press Enter.
Why use one project folder?
Keeping the virtual environment and project files together makes the workspace reproducible. It also ensures every later terminal command starts from reliable-inventory-sync-api.
- Return to Visual Studio Code.
- Select File in the top menu.
- Select Open Folder.
- Select the reliable-inventory-sync-api folder on your Desktop.
- Select Select Folder.
- Confirm that you trust the folder if Visual Studio Code asks.
- Select View in the top menu.
- Select Terminal.
✔️ The terminal shows PowerShell
The terminal prompt should end inside the reliable-inventory-sync-api folder. Your commands now run from the project root.
ⓧ Another shell opens
- Select the terminal profiles dropdown in the terminal panel.
- Select PowerShell.
- Confirm that the new prompt ends inside reliable-inventory-sync-api.
Keep package versions separate
A virtual environment keeps this API's packages separate from packages used by other Python projects. The .venv folder becomes the isolated environment for this workspace.
- Create the .venv environment by running this command:
python -m venv .venv
What does this command do?
Python creates an isolated environment inside .venv. Packages installed after activation stay scoped to this project.
- Confirm that .venv appears in the Visual Studio Code Explorer.
- Activate the environment by running this command:
.\.venv\Scripts\Activate.ps1
What does activation change?
Activation points the current PowerShell session at the Python interpreter inside .venv. Later installation commands place packages in this project environment.
✔️ I see (.venv)
The PowerShell prompt now starts with (.venv). Your isolated environment is active.
ⓧ PowerShell blocks the script
PowerShell can block local activation scripts through its execution policy. This change affects your current Windows user, so review the warning before approving it.
- Allow locally created scripts for your Windows user by running this command:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
What does this policy change?
The command changes the execution policy for your current user. PowerShell may ask you to confirm the change before it returns to the prompt.
- Review the PowerShell warning before approving the policy change.
- Activate the environment again by running this command:
.\.venv\Scripts\Activate.ps1
What should you see now?
The prompt should start with (.venv). This confirms that the project interpreter is active.
🙋♀️ Activation still blocked?
Confirm that the terminal is PowerShell. Ask for help activating this Windows virtual environment if the script still cannot run.
Lock the dependency versions
A requirements file records the exact direct dependencies used by the API. Pinning each version gives future installations the same package baseline.
- Select the New File control in the Visual Studio Code Explorer.
- Enter requirements.txt as the file name.
- Paste these pinned dependencies into requirements.txt:
fastapi[standard]==0.141.1
httpx==0.28.1
pydantic==2.13.5
pytest==9.1.1
What do these dependencies provide?
- FastAPI 0.141.1 provides the API framework and development command.
- HTTPX 0.28.1 supports the API test client used later.
- Pydantic 2.13.5 validates request data against typed models.
- pytest 9.1.1 runs the automated test suite.
- Select File in the top menu.
- Select Save.
✔️ Awesome, I've got everything!
Your dependency manifest now contains all four pinned direct dependencies.
ⓧ I'd like to double check the full code
fastapi[standard]==0.141.1
httpx==0.28.1
pydantic==2.13.5
pytest==9.1.1
The first installation may take a few minutes while pip downloads the packages. A busy terminal during this step is expected.
- Install the pinned dependencies by running this command:
python -m pip install -r requirements.txt
What does this command do?
The command reads every pin from requirements.txt. It installs those packages into the active .venv environment.
When the prompt returns without an installation error, the virtual environment contains the project dependencies.
Dependency installation failing?
Confirm that the prompt starts with (.venv). Check that every line in requirements.txt matches the pinned file above.
Ask for help with the pinned dependency installation if the command still fails.
Before you run the final checks, which Python version and package versions do you expect to see?
- Verify the runtime and installed packages by running these commands:
py list
python -m pip list
What should you see?
- The runtime list includes Python 3.14.7.
- The package list includes FastAPI at 0.141.1.
- The package list includes HTTPX at 0.28.1.
- The package list includes Pydantic at 2.13.5.
- The package list includes pytest at 9.1.1.
🙋♀️ Versions do not match?
Confirm that your PowerShell prompt starts with (.venv) before reinstalling from requirements.txt.
Ask for help correcting the Python or package versions if the checks still show different versions.
That's your development foundation locked in. Next, you'll launch the API and see its first health response in the browser.
Launch the Health API
Your Windows workspace now has Python 3.14.7 inside an activated virtual environment. The pinned dependencies are ready.
A working API needs a visible request path before storage logic adds complexity. In this step, you will use FastAPI to serve a health response through interactive documentation.
In this step, get ready to:
- Create a FastAPI application with a health endpoint.
- Run the local development server.
- Verify the health response through interactive documentation.
Create the health API
The app object represents your API. A health endpoint gives you a quick way to prove that the request path works.
- In the Visual Studio Code Explorer sidebar, click the New File icon.
- Enter main.py in the file name field.
You will see main.py open in the editor. The file also appears in the Explorer sidebar.
- Build the smallest runnable API by pasting this code into main.py:
from fastapi import FastAPI
app = FastAPI(title="Reliable Inventory Sync API")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
What does this code do?
- The FastAPI(...) call creates the application object. Its title identifies the API in the interactive documentation.
- The @app.get("/health") decorator connects GET requests for /health to the function below it.
- The health() function returns a dictionary. FastAPI converts that dictionary into a JSON response.
- Save main.py from the Visual Studio Code File menu.
✔️ Awesome, I've got everything!
Your main.py file is ready. Make sure the saved file appears without an unsaved-change indicator in its editor tab.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
app = FastAPI(title="Reliable Inventory Sync API")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
Run the development server
The development server loads the app object from main.py. It listens for local requests while you keep building.
- Switch back to the PowerShell terminal from earlier.
- Start the FastAPI development server by running:
fastapi dev
Your API is now listening for requests. You will see startup output that includes http://127.0.0.1:8000.
Server not starting?
- Confirm that the .venv virtual environment is still activated in PowerShell.
- Check that main.py is saved inside reliable-inventory-sync-api.
- Compare your file with the full-code tab above.
Help me diagnose why my FastAPI development server is not starting from this project folder.
Verify the health response
FastAPI generates interactive API documentation from your route definitions. The documentation lets you call the health endpoint without writing a separate client.
- Use the Windows taskbar to open your preferred web browser.
- Enter http://127.0.0.1:8000/docs in the address bar.
- Expand GET /health.
- Select Try it out.
Before you select Execute, consider whether the endpoint can already answer a request.
- Select Execute.
You will see the response body {"status":"ok"}. That response proves the local request path works from the browser to your health function.
Health response missing?
- Confirm that the PowerShell terminal still shows the development server running.
- Refresh http://127.0.0.1:8000/docs in your browser.
- Check that the route in main.py is exactly /health.
Help me find out why GET /health is not returning the expected JSON response.
Your API now answers a real request through its interactive documentation. Next, you will build the first inventory event flow on top of this working path.
Expose the Duplicate Event Bug
Your health endpoint proved that requests can reach the local API. Store systems now need a way to send stock changes into that working request path.
Delivery retries are realistic when a response is delayed or lost. You will replay one event to test whether this first storage path can tell a retry from a new change.
In this step, get ready to:
- Define request and response models for inventory events.
- Store inventory quantities in a local SQLite database.
- Replay one event to expose the duplicate-processing bug.
Define the API contract
A clear API contract gives every incoming event a predictable shape. Pydantic validates that shape before FastAPI passes the event to your application.
- Select the reliable-inventory-sync-api folder in the Visual Studio Code Explorer sidebar.
- Use the new-file icon at the top of the Explorer sidebar.
- Enter models.py as the file name.
- Build the complete API contract by pasting this code into models.py:
from pydantic import BaseModel
class InventoryEvent(BaseModel):
event_id: str
store_id: str
sku: str
quantity_change: int
class InventoryResult(BaseModel):
event_id: str
store_id: str
sku: str
applied: bool
quantity: int
class InventorySnapshot(BaseModel):
store_id: str
sku: str
quantity: int
What do these models define?
- InventoryEvent defines the four values that every stock-change request must provide.
- InventoryResult gives the event endpoint a stable response containing the resulting quantity.
- InventorySnapshot defines the response returned when a caller queries current stock.
- quantity_change uses the integer type so malformed values receive a validation response before they reach storage.
- Save models.py.
- Confirm that models.py is listed beside main.py in the Explorer sidebar.
You now have one input model plus two response models. Their field names become the contract shown in the interactive documentation.
Seeing a problem in models.py?
Check that all three classes inherit from BaseModel. Make sure each field remains indented beneath its class.
Help me check my Pydantic model definitions.
✔️ Awesome, I've got everything!
Great. Save models.py before you continue.
ⓧ I'd like to double check the full code
from pydantic import BaseModel
class InventoryEvent(BaseModel):
event_id: str
store_id: str
sku: str
quantity_change: int
class InventoryResult(BaseModel):
event_id: str
store_id: str
sku: str
applied: bool
quantity: int
class InventorySnapshot(BaseModel):
store_id: str
sku: str
quantity: int
Compare this reference with your saved models.py file.
Build the first SQLite storage path
SQLite stores the inventory in a local file without a separate database server. Each store and SKU pair becomes one inventory row.
- Select the reliable-inventory-sync-api folder in the Explorer sidebar.
- Use the new-file icon at the top of the Explorer sidebar.
- Enter database.py as the file name.
- Create the database connection plus the inventory table by pasting this first chunk into database.py:
import sqlite3
from models import InventoryEvent
DB_PATH = "inventory.db"
def _connect() -> sqlite3.Connection:
return sqlite3.connect(DB_PATH)
def initialize_database() -> None:
connection = _connect()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
store_id TEXT NOT NULL,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (store_id, sku)
)
"""
)
connection.commit()
finally:
connection.close()
What does this storage setup do?
- DB_PATH keeps the local database file name in one place.
- _connect() opens a connection to that file whenever a database operation begins.
- initialize_database() creates the inventory table when it does not exist.
- PRIMARY KEY (store_id, sku) keeps one row for each store and product combination.
- Save database.py.
- Confirm that database.py is listed in the Explorer sidebar.
Seeing an indentation warning?
Keep the table definition inside the triple quotes. Align finally: with try: so the connection always closes.
Help me fix the database initialization code.
- Add the quantity reader below initialize_database() by pasting this code:
def _read_quantity(
connection: sqlite3.Connection,
store_id: str,
sku: str,
) -> int:
row = connection.execute(
"SELECT quantity FROM inventory WHERE store_id = ? AND sku = ?",
(store_id, sku),
).fetchone()
return int(row[0]) if row else 0
How is the quantity read safely?
The question-mark placeholders bind store_id plus sku as query parameters. The function returns zero when no inventory row exists.
- Save database.py.
- Confirm that _read_quantity() appears below initialize_database() in the editor.
Is the quantity query underlined?
Check that the query contains two question-mark placeholders. Check that the parameter tuple contains store_id plus sku in the same order.
Help me inspect the parameterized quantity query.
- Add the event-processing shell below _read_quantity() by pasting this code:
def apply_event(event: InventoryEvent) -> tuple[bool, int]:
connection = _connect()
try:
current_quantity = _read_quantity(connection, event.store_id, event.sku)
new_quantity = current_quantity + event.quantity_change
connection.commit()
return True, new_quantity
except Exception:
connection.rollback()
raise
finally:
connection.close()
def get_quantity(store_id: str, sku: str) -> int:
connection = _connect()
try:
return _read_quantity(connection, store_id, sku)
finally:
connection.close()
What does this transaction shell provide?
- apply_event() calculates the proposed quantity inside a database connection.
- commit() saves a successful transaction.
- rollback() reverses pending database changes when an unexpected error occurs.
- get_quantity() exposes the stored quantity to the query endpoint.
- Find the blank line immediately below new_quantity = current_quantity + event.quantity_change.
- Insert the inventory update logic into that blank line by pasting this code:
if current_quantity == 0:
existing_row = connection.execute(
"SELECT 1 FROM inventory WHERE store_id = ? AND sku = ?",
(event.store_id, event.sku),
).fetchone()
else:
existing_row = True
if existing_row:
connection.execute(
"""
UPDATE inventory
SET quantity = ?
WHERE store_id = ? AND sku = ?
""",
(new_quantity, event.store_id, event.sku),
)
else:
connection.execute(
"""
INSERT INTO inventory (store_id, sku, quantity)
VALUES (?, ?, ?)
""",
(event.store_id, event.sku, new_quantity),
)
How does the inventory write work?
The first query checks whether a zero quantity belongs to an existing row. The second branch updates that row or inserts a new one.
Every SQL value uses a question-mark placeholder. The commit saves the calculated quantity after the write succeeds.
- Save database.py.
- Confirm that connection.commit() remains below the complete update branch.
Is the inventory branch misaligned?
Place both if blocks inside the outer try: block. Keep connection.commit() aligned with the first inventory condition.
Help me fix the apply_event indentation.
✔️ Awesome, I've got everything!
Your first SQLite storage path is ready. Save database.py before wiring it into the API.
ⓧ I'd like to double check the full code
import sqlite3
from models import InventoryEvent
DB_PATH = "inventory.db"
def _connect() -> sqlite3.Connection:
return sqlite3.connect(DB_PATH)
def initialize_database() -> None:
connection = _connect()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
store_id TEXT NOT NULL,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (store_id, sku)
)
"""
)
connection.commit()
finally:
connection.close()
def _read_quantity(
connection: sqlite3.Connection,
store_id: str,
sku: str,
) -> int:
row = connection.execute(
"SELECT quantity FROM inventory WHERE store_id = ? AND sku = ?",
(store_id, sku),
).fetchone()
return int(row[0]) if row else 0
def apply_event(event: InventoryEvent) -> tuple[bool, int]:
connection = _connect()
try:
current_quantity = _read_quantity(connection, event.store_id, event.sku)
new_quantity = current_quantity + event.quantity_change
if current_quantity == 0:
existing_row = connection.execute(
"SELECT 1 FROM inventory WHERE store_id = ? AND sku = ?",
(event.store_id, event.sku),
).fetchone()
else:
existing_row = True
if existing_row:
connection.execute(
"""
UPDATE inventory
SET quantity = ?
WHERE store_id = ? AND sku = ?
""",
(new_quantity, event.store_id, event.sku),
)
else:
connection.execute(
"""
INSERT INTO inventory (store_id, sku, quantity)
VALUES (?, ?, ?)
""",
(event.store_id, event.sku, new_quantity),
)
connection.commit()
return True, new_quantity
except Exception:
connection.rollback()
raise
finally:
connection.close()
def get_quantity(store_id: str, sku: str) -> int:
connection = _connect()
try:
return _read_quantity(connection, store_id, sku)
finally:
connection.close()
Compare this reference with your saved database.py file.
Wire the inventory endpoints
REST endpoints connect the models to the storage functions. One endpoint accepts events. The other returns the quantity stored for a specific store and SKU.
- Return to main.py in Visual Studio Code.
- Find this startup code at the top of the file:
from fastapi import FastAPI
app = FastAPI(title="Reliable Inventory Sync API")
What is changing at startup?
The current startup creates the application without loading models or storage functions. The replacement initializes the local database before FastAPI begins serving requests.
- Replace everything from the first import through the application assignment with this code:
from fastapi import FastAPI
from database import apply_event, get_quantity, initialize_database
from models import InventoryEvent, InventoryResult, InventorySnapshot
initialize_database()
app = FastAPI(title="Reliable Inventory Sync API", version="1.0.0")
What does the replacement add?
- The database imports make the storage operations available to each endpoint.
- The model imports give the request plus response bodies explicit types.
- initialize_database() creates inventory.db plus the inventory table during startup.
- version="1.0.0" adds the API version to its generated documentation.
- Add the event endpoint below health() by pasting this code:
@app.post("/inventory/events", response_model=InventoryResult)
def receive_inventory_event(event: InventoryEvent) -> InventoryResult:
applied, quantity = apply_event(event)
return InventoryResult(
event_id=event.event_id,
store_id=event.store_id,
sku=event.sku,
applied=applied,
quantity=quantity,
)
How does the event endpoint respond?
receive_inventory_event() passes the validated request to apply_event(). It returns the event identity plus the resulting quantity through InventoryResult.
- Save main.py.
- Start the revised local API from the activated PowerShell terminal by running this command:
fastapi dev
What does this command start?
The development server detects the app object in main.py. It reloads the API when you save later changes.
- Return to the browser tab showing http://127.0.0.1:8000/docs.
- Refresh the interactive documentation using your browser's refresh control.
- Confirm that POST /inventory/events is listed below the health endpoint.
- Confirm that inventory.db is listed in the Visual Studio Code Explorer sidebar.
The API now exposes an event-ingestion path. The local database file proves that startup also initialized durable storage.
Does the API fail to start?
Confirm that models.py plus database.py are saved beside main.py. If another local server is still using the port, stop that process with Ctrl+C before running the command again.
Help me diagnose why the revised FastAPI app will not start.
- Return to main.py in Visual Studio Code.
- Add the inventory query endpoint below receive_inventory_event() by pasting this code:
@app.get(
"/inventory/{store_id}/{sku}",
response_model=InventorySnapshot,
)
def read_inventory(store_id: str, sku: str) -> InventorySnapshot:
return InventorySnapshot(
store_id=store_id,
sku=sku,
quantity=get_quantity(store_id, sku),
)
How does the query endpoint work?
The path captures store_id plus sku from the request URL. get_quantity() reads the matching database row for the response.
- Save main.py.
- Return to the interactive documentation in your browser.
- Refresh the page using your browser's refresh control.
- Confirm that GET /inventory/{store_id}/{sku} is listed with the other endpoints.
That is the full request path in place. You can now submit an event and query the value that SQLite stores.
Is the query endpoint missing?
Confirm that the server is still running in PowerShell. Check that the entire read_inventory() function sits below the event endpoint.
Help me find why the inventory query endpoint is missing.
✔️ Awesome, I've got everything!
Both inventory endpoints are visible. Keep the development server running for the duplicate-event test.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
from database import apply_event, get_quantity, initialize_database
from models import InventoryEvent, InventoryResult, InventorySnapshot
initialize_database()
app = FastAPI(title="Reliable Inventory Sync API", version="1.0.0")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/inventory/events", response_model=InventoryResult)
def receive_inventory_event(event: InventoryEvent) -> InventoryResult:
applied, quantity = apply_event(event)
return InventoryResult(
event_id=event.event_id,
store_id=event.store_id,
sku=event.sku,
applied=applied,
quantity=quantity,
)
@app.get(
"/inventory/{store_id}/{sku}",
response_model=InventorySnapshot,
)
def read_inventory(store_id: str, sku: str) -> InventorySnapshot:
return InventorySnapshot(
store_id=store_id,
sku=sku,
quantity=get_quantity(store_id, sku),
)
Compare this reference with your saved main.py file.
The event endpoint is ready for its first stock change. Start with one request so you have a clear baseline before replaying it.
- Expand POST /inventory/events in the interactive documentation.
- Select Try it out.
- Replace the example request body with this event:
{
"event_id": "event-1001",
"store_id": "store-101",
"sku": "coffee-beans",
"quantity_change": 5
}
What does this event represent?
The event adds five units of coffee-beans to the inventory for store-101. Its event-1001 identifier represents this specific integration message.
- Select Execute to submit the event once.
You will see a successful response with applied set to true. The response quantity is 5.
Your first inventory event now reaches SQLite successfully. The stored quantity matches the five-unit change you submitted.
Did the first event fail?
Check that the request contains all four model fields. Make sure quantity_change is the number 5.
Help me debug the first inventory event request.
Before you replay the request, do you think the stored quantity will remain at 5 or increase again?
- Select Execute again without changing the request body.
You will see applied set to true again. The quantity incorrectly increases to 10.
The duplicate bug is now visible
The API treats the replay as a fresh stock change because this version stores no processed event record. A delayed delivery retry can therefore corrupt the inventory total.
One final query checks the value that persisted in SQLite. Before you run it, which quantity do you expect the database to return?
- Expand GET /inventory/{store_id}/{sku} in the interactive documentation.
- Select Try it out.
- Enter store-101 in the store_id field.
- Enter coffee-beans in the sku field.
- Select Execute to query the stored inventory.
You will see store-101 plus coffee-beans in the response. The stored quantity is 10.
You have reproduced the integration failure with a concrete database result. Next, you will record processed event IDs so the same replay becomes a safe no-op.
Make Event Processing Idempotent
Your duplicate-event test proved that a retry can corrupt the stored quantity. The API currently treats every delivery as a new inventory change.
A unique processed-event record adds idempotency to the API. One SQLite transaction commits the event record with its inventory change.
In this step, get ready to:
- Store each processed event ID once.
- Commit each event record with its inventory change.
- Replay one event to prove the quantity changes once.
Record processed event IDs
The processed_events table acts as a durable event ledger. Its primary key allows each event_id to appear once.
- Select database.py in the Visual Studio Code Explorer sidebar.
- Find the initialize_database() function near the top of the file.
- Paste this replacement over the complete initialize_database() function:
def initialize_database() -> None:
connection = _connect()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
store_id TEXT NOT NULL,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (store_id, sku)
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS processed_events (
event_id TEXT PRIMARY KEY
)
"""
)
connection.commit()
finally:
connection.close()
How Does the Event Ledger Work?
- The inventory table keeps the current quantity for each store and SKU.
- The processed_events table stores accepted event IDs.
- The PRIMARY KEY constraint rejects an event ID that already exists.
- The final commit() saves both table definitions.
- Save database.py.
- Return to the PowerShell terminal from earlier.
The development server reloads the updated file. You should see it return to its ready state.
Server Fails to Reload?
Check that both table definitions have closing triple quotes. Confirm that the new connection.execute() call remains inside the existing try block.
Ask for help comparing the function with the target structure.
The ledger still needs a duplicate guard. A failed event ID insert identifies the request as a replay.
The nested exception handling is a little fiddly. The duplicate handler must remain inside the outer try block.
- Find the apply_event() function in database.py.
- Locate the outer try: line below connection = _connect().
- Insert this duplicate guard before the current_quantity assignment:
try:
connection.execute(
"INSERT INTO processed_events (event_id) VALUES (?)",
(event.event_id,),
)
except sqlite3.IntegrityError:
connection.rollback()
quantity = _read_quantity(connection, event.store_id, event.sku)
return False, quantity
What Does the Duplicate Guard Do?
- The first request inserts its event_id into the ledger.
- A replay violates the primary-key constraint.
- The sqlite3.IntegrityError handler rolls back the failed insert attempt.
- The _read_quantity() call retrieves the quantity committed by the first request.
- The returned False value reports that the replay was ignored.
- Save database.py.
- Return to the PowerShell terminal from earlier.
The development server reloads again. You should see the revised database logic load successfully.
Seeing a Reload Error?
Check that except sqlite3.IntegrityError: aligns with the nested try: line. Confirm that both lines sit inside the outer try block.
Ask for help checking the duplicate guard.
✔️ Awesome, I've got everything!
Your revised database file now contains the event ledger and duplicate guard.
ⓧ I'd like to double check the full code
import sqlite3
from models import InventoryEvent
DB_PATH = "inventory.db"
def _connect() -> sqlite3.Connection:
return sqlite3.connect(DB_PATH)
def initialize_database() -> None:
connection = _connect()
try:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS inventory (
store_id TEXT NOT NULL,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (store_id, sku)
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS processed_events (
event_id TEXT PRIMARY KEY
)
"""
)
connection.commit()
finally:
connection.close()
def _read_quantity(
connection: sqlite3.Connection,
store_id: str,
sku: str,
) -> int:
row = connection.execute(
"SELECT quantity FROM inventory WHERE store_id = ? AND sku = ?",
(store_id, sku),
).fetchone()
return int(row[0]) if row else 0
def apply_event(event: InventoryEvent) -> tuple[bool, int]:
connection = _connect()
try:
try:
connection.execute(
"INSERT INTO processed_events (event_id) VALUES (?)",
(event.event_id,),
)
except sqlite3.IntegrityError:
connection.rollback()
quantity = _read_quantity(connection, event.store_id, event.sku)
return False, quantity
current_quantity = _read_quantity(connection, event.store_id, event.sku)
new_quantity = current_quantity + event.quantity_change
if current_quantity == 0:
existing_row = connection.execute(
"SELECT 1 FROM inventory WHERE store_id = ? AND sku = ?",
(event.store_id, event.sku),
).fetchone()
else:
existing_row = True
if existing_row:
connection.execute(
"""
UPDATE inventory
SET quantity = ?
WHERE store_id = ? AND sku = ?
""",
(new_quantity, event.store_id, event.sku),
)
else:
connection.execute(
"""
INSERT INTO inventory (store_id, sku, quantity)
VALUES (?, ?, ?)
""",
(event.store_id, event.sku, new_quantity),
)
connection.commit()
return True, new_quantity
except Exception:
connection.rollback()
raise
finally:
connection.close()
def get_quantity(store_id: str, sku: str) -> int:
connection = _connect()
try:
return _read_quantity(connection, store_id, sku)
finally:
connection.close()
What Should the Full File Contain?
The event ID insert appears before the quantity calculation. The existing commit remains after the inventory write.
Keep the new event atomic
The event record and stock change use the same connection. The existing final commit saves both writes as one unit.
- Find connection.commit() near the end of apply_event().
- Confirm that the commit appears after the inventory update or insert.
- Find the outer except Exception: block.
- Confirm that connection.rollback() appears before raise.
Why Does Atomicity Matter?
A successful request commits the event ID with the new quantity. An unexpected failure rolls back both pending writes.
This prevents a partial inventory event from remaining in the database.
Re-run the duplicate demonstration
The existing inventory.db contains the incorrect quantity from the earlier demonstration. A fresh database gives the revised schema a clean starting point.
Deleting this local database clears the disposable demo data. The revised application recreates the file when it starts.
- Return to the PowerShell terminal from earlier.
- Stop the running development server by pressing Ctrl+C.
- Right-click inventory.db in the Visual Studio Code Explorer sidebar.
- Use the context menu to remove inventory.db.
- Approve the file removal in the confirmation prompt.
- Return to the activated PowerShell terminal.
- Restart the local API by running this command:
fastapi dev
What Does This Command Do?
The command starts the development server from the revised code. The initialize_database() call creates a fresh inventory table plus the processed-event ledger.
You should see the development server return to its ready state. The interactive documentation is available again at http://127.0.0.1:8000/docs.
Server Does Not Start?
Confirm that the earlier server process stopped. Check that the PowerShell prompt still shows the activated .venv environment.
Ask for help restarting the revised API.
- Switch back to the interactive documentation from earlier.
- Refresh the documentation page.
- Expand POST /inventory/events.
- Select Try it out.
- Replace the request body with this event:
{
"event_id": "event-1001",
"store_id": "store-101",
"sku": "coffee-beans",
"quantity_change": 5
}
What Does This Event Represent?
This event adds five units of coffee-beans inventory for store-101.
The event-1001 identifier stays unchanged during the replay. That repeated identifier gives the API its duplicate signal.
Before you submit this clean event, do you expect the API to apply it or ignore it?
- Select Execute.
The first response shows "applied": true. The quantity is 5.
Before you replay the request, do you expect the quantity to increase again?
- Select Execute again.
The second response shows "applied": false. The quantity remains 5.
Before you query the stored inventory, what quantity do you expect the database to return?
- Expand GET /inventory/{store_id}/{sku} in the interactive documentation.
- Select Try it out.
- Enter store-101 in the store_id field.
- Enter coffee-beans in the sku field.
- Select Execute.
The response reports a quantity of 5. This proves that the stored inventory changed once.
Replay Still Changes the Quantity?
Confirm that you removed the old inventory.db file before restarting the server. Check that the event ID insert appears before current_quantity is read.
Ask for help tracing the duplicate request.
That reliability promise now works. One event produces one inventory change. Next, you'll turn this demonstration into repeatable automated evidence.
Test and Document the API
Your API now protects inventory from duplicate events. A manual demonstration proves the behavior once. It cannot catch a regression after the code changes.
In this step, you'll use pytest with FastAPI TestClient to verify four API behaviors. You'll also write the documentation that lets someone else run the project.
In this step, get ready to:
- Give every API test its own temporary database.
- Prove the API's four critical behaviors with automated tests.
- Create operating documentation for the finished API.
Isolate each test database
Tests need predictable data to produce trustworthy results. A temporary SQLite database gives every test a clean starting point without changing your local inventory.db file.
- In the Explorer sidebar, create a tests folder inside reliable-inventory-sync-api.
- Inside tests, create test_main.py.
- Add the isolated test client to tests/test_main.py by pasting this code:
import pytest
from fastapi.testclient import TestClient
import database
from main import app
@pytest.fixture
def client(tmp_path, monkeypatch) -> TestClient:
test_database = tmp_path / "inventory.db"
monkeypatch.setattr(database, "DB_PATH", str(test_database))
database.initialize_database()
return TestClient(app)
How does the fixture isolate tests?
- The tmp_path fixture supplies a unique directory for one test.
- The monkeypatch fixture redirects database.DB_PATH to a database inside that directory.
- The initialize_database() call creates fresh tables before the test begins.
- The TestClient instance sends requests directly to app.
- Save tests/test_main.py.
- Confirm that test_main.py is listed inside the tests folder in the Explorer sidebar.
Test file in the wrong place?
- Move test_main.py into the tests folder if it appears beside main.py.
- Check that the file name uses an underscore between test and main.
Help me check my isolated pytest fixture.
The health endpoint is the smallest proof that the test client can reach your application. This first test checks the response before the inventory scenarios add more state.
- Add the health test below the client fixture by pasting this code:
def test_health(client: TestClient) -> None:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
What does the health test prove?
- The client.get("/health") call sends a request through the application.
- The first assertion verifies a successful HTTP response.
- The second assertion verifies the exact response body.
- Save tests/test_main.py.
- Switch back to the PowerShell terminal that is running fastapi dev.
- Stop the development server by pressing Ctrl+C.
You'll return to the PowerShell prompt with the virtual environment still active.
- Run the first automated check with this command:
python -m pytest
The terminal reports one passing test. Your temporary database fixture can now reach the application safely.
First test not passing?
- Check that PowerShell still shows the activated .venv environment.
- Check that from main import app appears exactly as shown.
- Check that test_health begins with test_.
Help me diagnose my first pytest failure.
Cover updates, replays, and validation
The remaining tests focus on the behaviors that protect inventory data. Each test describes one promise that future code changes must preserve.
- Add the successful inventory update test below test_health by pasting this code:
def test_event_updates_inventory(client: TestClient) -> None:
response = client.post(
"/inventory/events",
json={
"event_id": "event-1001",
"store_id": "store-101",
"sku": "coffee-beans",
"quantity_change": 5,
},
)
assert response.status_code == 200
assert response.json()["applied"] is True
assert response.json()["quantity"] == 5
snapshot = client.get("/inventory/store-101/coffee-beans")
assert snapshot.json()["quantity"] == 5
What does the update test prove?
- The POST request sends one complete inventory event.
- The response assertions verify that the event was applied once.
- The GET request checks the quantity stored in the temporary database.
- Save tests/test_main.py.
- Run the growing test suite with this command:
python -m pytest
The terminal reports two passing tests. The suite now proves that an accepted event changes the stored quantity.
Inventory update test failing?
- Check that the request path is exactly /inventory/events.
- Check that the snapshot path contains store-101 followed by coffee-beans.
- Check that every request uses the client fixture.
Help me debug the inventory update test.
- Add the duplicate-event test below test_event_updates_inventory by pasting this code:
def test_duplicate_event_is_ignored(client: TestClient) -> None:
event = {
"event_id": "event-2001",
"store_id": "store-202",
"sku": "oat-milk",
"quantity_change": 4,
}
first_response = client.post("/inventory/events", json=event)
second_response = client.post("/inventory/events", json=event)
assert first_response.json()["applied"] is True
assert second_response.json()["applied"] is False
assert second_response.json()["quantity"] == 4
What does the replay test prove?
- Both requests send the same event_id.
- The first response proves that a new event is applied.
- The second response proves that the replay becomes a safe no-op.
- The final assertion proves that the quantity remains unchanged.
- Save tests/test_main.py.
- Run the growing test suite with this command:
python -m pytest
The terminal reports three passing tests. Duplicate delivery is now protected by an automated regression check.
Replay test failing?
- Check that both POST requests reuse the same event dictionary.
- Check that the second assertion expects applied to be false.
- Check that the expected quantity remains 4.
Help me debug the duplicate-event test.
Request validation protects the API before malformed data reaches the database. The final test sends text where quantity_change requires an integer.
- Add the validation test below test_duplicate_event_is_ignored by pasting this code:
def test_invalid_quantity_is_rejected(client: TestClient) -> None:
response = client.post(
"/inventory/events",
json={
"event_id": "event-3001",
"store_id": "store-303",
"sku": "vanilla-syrup",
"quantity_change": "five",
},
)
assert response.status_code == 422
What does the validation test prove?
- The request uses the valid event shape.
- The quantity_change value has the wrong type.
- The 422 assertion proves that request validation rejects the payload.
- Save tests/test_main.py.
- Run the complete test file with this command:
python -m pytest
The terminal reports four passing tests. Health checks, updates, replay safety, and request validation now have repeatable proof.
Validation test failing?
- Check that quantity_change contains the string five.
- Check that the assertion expects status 422.
- Check that InventoryEvent still declares quantity_change as an integer.
Help me debug the request validation test.
✔️ Awesome, I've got everything!
Great. Save tests/test_main.py before moving on.
ⓧ I'd like to double check the full code
import pytest
from fastapi.testclient import TestClient
import database
from main import app
@pytest.fixture
def client(tmp_path, monkeypatch) -> TestClient:
test_database = tmp_path / "inventory.db"
monkeypatch.setattr(database, "DB_PATH", str(test_database))
database.initialize_database()
return TestClient(app)
def test_health(client: TestClient) -> None:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_event_updates_inventory(client: TestClient) -> None:
response = client.post(
"/inventory/events",
json={
"event_id": "event-1001",
"store_id": "store-101",
"sku": "coffee-beans",
"quantity_change": 5,
},
)
assert response.status_code == 200
assert response.json()["applied"] is True
assert response.json()["quantity"] == 5
snapshot = client.get("/inventory/store-101/coffee-beans")
assert snapshot.json()["quantity"] == 5
def test_duplicate_event_is_ignored(client: TestClient) -> None:
event = {
"event_id": "event-2001",
"store_id": "store-202",
"sku": "oat-milk",
"quantity_change": 4,
}
first_response = client.post("/inventory/events", json=event)
second_response = client.post("/inventory/events", json=event)
assert first_response.json()["applied"] is True
assert second_response.json()["applied"] is False
assert second_response.json()["quantity"] == 4
def test_invalid_quantity_is_rejected(client: TestClient) -> None:
response = client.post(
"/inventory/events",
json={
"event_id": "event-3001",
"store_id": "store-303",
"sku": "vanilla-syrup",
"quantity_change": "five",
},
)
assert response.status_code == 422
Document the finished API
Tests explain the API to the computer. A Markdown README explains the same system to the next developer who opens the repository.
- In the Explorer sidebar, create README.md beside main.py.
- Add the project overview through the run instructions by pasting this content:
# Reliable Inventory Sync API
A role-aligned application development project that demonstrates API contracts, durable storage, automated testing, and safe duplicate-event handling.
## Problem
Store systems can retry an inventory event when a response is delayed or lost. If the receiving service applies every retry, its stock count becomes incorrect. This API stores each event ID and applies that event only once.
## Architecture
- FastAPI exposes health, event ingestion, and inventory query endpoints.
- Pydantic validates request and response bodies.
- SQLite stores inventory quantities and processed event IDs.
- pytest and FastAPI TestClient exercise the API with isolated temporary databases.
## Setup on Windows
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
```
## Run
```powershell
fastapi dev
```
Open `http://127.0.0.1:8000/docs`.
What does this documentation cover?
- The problem section explains why duplicate events matter.
- The architecture section maps each tool to its responsibility.
- The setup section recreates the local environment.
- The run section points readers to the interactive API documentation.
- Save README.md.
- Open the Markdown preview from the editor toolbar.
- Confirm that the preview shows the project title through the run instructions.
README preview not rendering?
- Check that the file name ends with .md.
- Check that each section heading begins with the exact number of # characters shown.
- Check that every fenced command block has a closing fence.
Help me fix the first part of my README.
An endpoint table gives readers a compact map of the API. The duplicate-event demonstration then turns idempotency into something they can reproduce.
- Place the cursor at the end of README.md.
- Press Enter twice to leave one blank line.
- Add the endpoint table through the duplicate-event demonstration by pasting this content:
## Endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/health` | Confirm the service is responding |
| POST | `/inventory/events` | Apply one inventory change event |
| GET | `/inventory/{store_id}/{sku}` | Read the current quantity |
## Duplicate-event demo
Submit this body twice to `POST /inventory/events`:
```json
{
"event_id": "event-1001",
"store_id": "store-101",
"sku": "coffee-beans",
"quantity_change": 5
}
```
The first response has `"applied": true`. The replay has `"applied": false`, and the quantity remains 5.
Why include a replay demonstration?
- The endpoint table shows what each request does.
- The JSON body gives readers a repeatable event.
- The two response outcomes show how idempotency changes replay behavior.
- Save README.md.
- Return to the Markdown preview.
- Confirm that the endpoint table has three rows.
- Confirm that the duplicate-event body appears as formatted JSON.
Table or JSON formatting broken?
- Check that every table row begins with a vertical bar.
- Check that the JSON fence starts with ```json.
- Check that the JSON block ends with a closing fence.
Help me repair the endpoint table or JSON example.
The final README sections preserve the test command for future contributors. They also place this API in the wider project sequence.
- Place the cursor at the end of README.md.
- Press Enter twice to leave one blank line.
- Finish the README by pasting this content:
## Test
```powershell
python -m pytest
```
## Portfolio series
- Part 1: Application engineering and idempotent API integration
- Part 2: Automated testing, container builds, and release delivery
- Part 3: Monitoring, incident investigation, and production support
What do the final sections add?
- The test section gives contributors one command for checking the API.
- The portfolio series lists the engineering topics that follow this project.
- Save README.md.
- Return to the Markdown preview.
- Confirm that the preview ends with the test command followed by three portfolio parts.
Final README sections missing?
- Check that the new content was added after the duplicate-event paragraph.
- Check that the PowerShell fence surrounds only the test command.
- Check that each portfolio entry starts with a hyphen.
Help me check the completed README structure.
✔️ Awesome, I've got everything!
Great. Save README.md before creating the ignore file.
ⓧ I'd like to double check the full code
# Reliable Inventory Sync API
A role-aligned application development project that demonstrates API contracts, durable storage, automated testing, and safe duplicate-event handling.
## Problem
Store systems can retry an inventory event when a response is delayed or lost. If the receiving service applies every retry, its stock count becomes incorrect. This API stores each event ID and applies that event only once.
## Architecture
- FastAPI exposes health, event ingestion, and inventory query endpoints.
- Pydantic validates request and response bodies.
- SQLite stores inventory quantities and processed event IDs.
- pytest and FastAPI TestClient exercise the API with isolated temporary databases.
## Setup on Windows
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
```
## Run
```powershell
fastapi dev
```
Open `http://127.0.0.1:8000/docs`.
## Endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/health` | Confirm the service is responding |
| POST | `/inventory/events` | Apply one inventory change event |
| GET | `/inventory/{store_id}/{sku}` | Read the current quantity |
## Duplicate-event demo
Submit this body twice to `POST /inventory/events`:
```json
{
"event_id": "event-1001",
"store_id": "store-101",
"sku": "coffee-beans",
"quantity_change": 5
}
```
The first response has `"applied": true`. The replay has `"applied": false`, and the quantity remains 5.
## Test
```powershell
python -m pytest
```
## Portfolio series
- Part 1: Application engineering and idempotent API integration
- Part 2: Automated testing, container builds, and release delivery
- Part 3: Monitoring, incident investigation, and production support
Local environments, caches, bytecode, and database files are machine-specific artifacts. A .gitignore file keeps them outside the repository.
- In the Explorer sidebar, create .gitignore beside README.md.
- Add the ignore rules by pasting this content:
.venv/
__pycache__/
.pytest_cache/
*.pyc
inventory.db
What do these rules exclude?
- The .venv/ rule excludes the local virtual environment.
- The cache rules exclude generated Python test artifacts.
- The *.pyc rule excludes compiled Python bytecode.
- The inventory.db rule excludes local inventory data.
- Save .gitignore.
- Confirm that .gitignore is listed beside README.md in the Explorer sidebar.
Ignore file not visible?
- Check that the file name starts with a period.
- Check that the file has no extra extension after .gitignore.
- Refresh the Explorer sidebar if the file list has not updated.
Help me check my .gitignore file.
✔️ Awesome, I've got everything!
Great. Your local-only artifacts now have matching ignore rules.
ⓧ I'd like to double check the full code
.venv/
__pycache__/
.pytest_cache/
*.pyc
inventory.db
The source code, tests, documentation, and ignore rules now describe one reproducible project. The final test run checks that documentation work did not disturb the API behavior.
Before you run the final check, do you expect the documentation files to change the test result?
- Run the complete test suite from the PowerShell terminal with this command:
python -m pytest
The terminal reports four passing tests with no failures.
Tests do not all pass?
- Check that tests/test_main.py matches the complete reference above.
- Check that PowerShell is inside the reliable-inventory-sync-api folder.
- Check that the activated environment contains the dependencies from requirements.txt.
Help me diagnose the failing test suite.
That is a strong finish. Your repository now proves health, validation, persistence, and duplicate handling with repeatable tests.
Secret mission
Prevent Negative Inventory
Your API handles duplicate events safely. Add a rollback-safe business rule that rejects stock changes which would push quantity below zero.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your local project available, pause your work, or remove it completely. Everything runs on your Windows computer, so there are no cloud resources or ongoing charges.
Resources you used:
- The local reliable-inventory-sync-api project folder.
- The .venv virtual environment inside the project folder.
- The project files inside that folder, including requirements.txt, models.py, database.py, main.py, tests/test_main.py, README.md, and .gitignore.
- The local SQLite database named inventory.db, if the API created it.
Keep everything running
No cleanup action is needed. Choose this option if you want to keep developing or demonstrating the inventory API.
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C if the local API server is running.
- Retain the reliable-inventory-sync-api folder in its current location.
Pause - I'll come back to this later
Stop the local server to free its memory. Keep the project files so you can continue from the same state later.
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C if the local API server is running.
- Close Visual Studio Code.
- Leave the reliable-inventory-sync-api folder in its current location.
Your source files, virtual environment, tests, documentation, and local inventory data remain available when you return.
Delete - I don't want to use this again
Remove the entire local project if you do not plan to use it again. Deleting the project folder removes every resource listed above.
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C if the local API server is running.
- Close Visual Studio Code.
- Use Windows search to open File Explorer.
- Navigate to the location containing the reliable-inventory-sync-api folder.
- Select the reliable-inventory-sync-api folder.
- Press Shift+Delete.
- Approve the permanent deletion when Windows asks for confirmation.
- Confirm that the reliable-inventory-sync-api folder no longer appears in File Explorer.
Nice Work!
Nice Work!
Outstanding work! You built a documented FastAPI inventory service that handles repeated events safely.
You've learned how to:
- Design typed REST endpoints with Pydantic models. Malformed quantities now receive a structured validation response.
- Persist stock in SQLite using parameterized SQL. Idempotency turns a duplicate event into a safe no-op.
- Run isolated pytest integration tests against temporary databases. Five passing tests now prove the API's key behavior.
- Complete the optional Secret Mission by protecting stock from negative quantities. A rejected event now returns a rollback-safe 409 response.
Ready to quiz yourself?