Task 1: The Application and Its Repository
In this task you create the repository and the app the pipeline will deliver.
Create the repository
- On GitHub, a new public repository:
<you>/devops-project.- It is your portfolio: a potential employer can read the code, the pipeline and every run.
- Task 3 protects
mainwith a branch protection rule, and on GitHub's free plans that works only in a public repository. - A private one needs a paid plan, GitHub Pro or Team.
- In a pair: one of you creates it, then adds the other in Settings → Collaborators with write access.
- Clone it, and push a first commit to
main: until Task 3, both of you push straight tomain.
Share the work in git
Choose the block for how you work, and follow its steps only.
Alone: commit to main
Until Task 3 you work on main itself: one commit per route, pushed as you go.
Write one route and its tests, and run the tests.
Commit it, and push. Add the files by name, the ones this commit is about, never
git add .: it would sweep in whatever else lies in the directory, a.venv/or a__pycache__/.1$ git status 2$ git add app/main.py tests/test_books.py 3$ git commit -m "Add POST /books" 4$ git pushGo on to the next route.
In a pair: a branch each, merged into main
You split the routes, each writes theirs on a branch of their own, and each merges their branch into main
by hand. No pull requests yet: they come with Task 3.
Write the common part together, at one keyboard: the app, the storage and the checks of the book's fields, which every route uses. Push it to
main.Split the routes into two GitHub issues, each assigned to one of you, each route with its tests:
- issue #1, Student A:
POST /books,GET /booksandGET /books/<id>; - issue #2, Student B:
PUT /books/<id>,DELETE /books/<id>andGET /healthz.
- issue #1, Student A:
Each works on a branch named for the issue, as The Plan names them:
feature/1-create-and-readandfeature/2-update-and-delete.1$ git switch main 2$ git pull 3$ git switch -c feature/1-create-and-readCommit as you go, and push the branch, so your partner can see it.
- Add the files by name, never
git add .: it would sweep in whatever else lies in the directory. - Keep your tests in a file of your own,
tests/test_create_read.pyortests/test_update_delete.py, so the two of you change only one file in common.
1$ git status 2$ git add app/main.py tests/test_create_read.py 3$ git commit -m "Add POST /books" 4$ git push -u origin feature/1-create-and-read- Add the files by name, never
Merge into
mainwhen your routes and their tests pass:1$ git switch main 2$ git pull 3$ git merge feature/1-create-and-read 4$ git push- The first to merge finds
mainunchanged since step 1: git movesmainforward, with no conflict.
- The first to merge finds
The second to merge runs the same commands, and
git pullbrings the partner's routes first.- Both of you added routes in the same file, so
git mergemay stop with a conflict there. - Open the file, keep both sides' routes, and delete the conflict markers (
<<<<<<<,=======,>>>>>>>). - Run every test, both students', then mark the file resolved by adding it, finish the merge and push:
1$ git add app/main.py 2$ git commit 3$ git push- Both of you added routes in the same file, so
Both pull
main, and run the tests: each now has the whole API.
- If
git pushtomainis refused because your partner pushed first, rungit pull, then push again.
Write the API
The API keeps a reading list: the books you have read or mean to read.
- One resource, the book, and the five operations on it: create, list, read, update, delete.
- Every request and response body is JSON.
- Storage: TODO: decide — in memory, SQLite, or PostgreSQL from Task 8 on.
The book
| Field | Type | Who sets it | Rule |
|---|---|---|---|
id | integer | the API, on create | unique, never reused, never changed |
title | string | the client | required, 1–200 characters |
author | string | the client | required, 1–100 characters |
year | integer | the client | optional; if sent, 0 to the current year |
read | boolean | the client | optional; false when not sent |
A book as the API sends it:
1{"id": 1, "title": "Dune", "author": "Frank Herbert", "year": 1965, "read": false}
The routes
| Method and path | Does | Sent | Received |
|---|---|---|---|
POST /books | creates a book | a book without id | 201 and the new book, with its id |
GET /books | lists every book | nothing | 200 and a list of books, oldest first |
GET /books/<id> | reads one book | nothing | 200 and the book |
PUT /books/<id> | replaces one book | a whole book without id | 200 and the book as stored |
DELETE /books/<id> | deletes one book | nothing | 204, and no body |
GET /healthz | says the app is up | nothing | 200 and {"status": "ok"} |
Create a book
Sent: the client's fields; the API assigns id.
1POST /books HTTP/1.1
2Content-Type: application/json
3
4{"title": "Dune", "author": "Frank Herbert", "year": 1965}
Received: 201 Created, a Location header naming the new book's URL, and the book as stored, read filled in.
1HTTP/1.1 201 Created
2Location: /books/1
3Content-Type: application/json
4
5{"id": 1, "title": "Dune", "author": "Frank Herbert", "year": 1965, "read": false}
List the books
Received: 200 OK and a JSON list, oldest first; an empty list [] when there are no books, never a 404.
1HTTP/1.1 200 OK
2Content-Type: application/json
3
4[{"id": 1, "title": "Dune", "author": "Frank Herbert", "year": 1965, "read": false},
5 {"id": 2, "title": "Emma", "author": "Jane Austen", "year": 1815, "read": true}]
Read one book
Sent: GET /books/1. Received: 200 OK and that one book, in the same shape as on create.
Update a book
PUTreplaces the whole book: every required field is sent again, and a missingyearorreadreturns to its default.- The
idcomes from the URL, and anidin the body is ignored.
1PUT /books/1 HTTP/1.1
2Content-Type: application/json
3
4{"title": "Dune", "author": "Frank Herbert", "year": 1965, "read": true}
Received: 200 OK and the book as now stored.
Delete a book
Sent: DELETE /books/1. Received: 204 No Content, with no body. The book's id is never given to another book.
The errors
Every error is a status code and a JSON body saying what went wrong.
| Case | Status |
|---|---|
No book with that id: read, update or delete | 404 Not Found |
| The body is not JSON, or a field is missing, of the wrong type or out of its range | 422 Unprocessable Content |
1HTTP/1.1 404 Not Found
2Content-Type: application/json
3
4{"detail": "no book with id 7"}
- The status codes are the contract: the tests, the pipeline's integration tests in Task 8 and the smoke test in Task 6 check them, so both tracks keep to them.
- The body's one key is
detail, as FastAPI writes it; its text is yours.
Option 1: Flask
- A route is a plain function per method and path.
- The checks of the field table are yours to write: Flask hands you the parsed JSON and nothing more.
- Flask answers
422only when you return it, with the error body, from your own checks. - A body that is not JSON makes Flask answer
400by itself: read the body in the mode that returns nothing instead, and answer422.
Option 2: FastAPI
- The book is a Pydantic model: the field table, written as types and limits.
- FastAPI checks each body against the model and answers
422by itself.- A body that is not JSON is a
422as well. - Its error body is
{"detail": [...]}, listing every failing field; keep it.
- A body that is not JSON is a
- A missing book is yours to answer: raise FastAPI's HTTP exception with
404. - The model also draws the OpenAPI page, at
/docs: try every route there.
Add a health endpoint
GET /healthzanswers200and{"status": "ok"}.- The deployment asks it whether the app is up, from Task 6 on: it must answer fast, and touch no storage.
Write the tests
- pytest, with the framework's test client, which sends requests to the app without a server.
- One test for each row of the routes table, and one for each row of the errors table.
- A layout:
app/,tests/,requirements.txt,requirements-dev.txt.
Run it on your machine
- Run the API, and call each route with
curl. - Run the tests.