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 main with 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 to main.

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.

  1. Write one route and its tests, and run the tests.

  2. 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 push
    
  3. Go 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.

  1. 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.

  2. 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 /books and GET /books/<id>;
    • issue #2, Student B: PUT /books/<id>, DELETE /books/<id> and GET /healthz.
  3. Each works on a branch named for the issue, as The Plan names them: feature/1-create-and-read and feature/2-update-and-delete.

    1$ git switch main
    2$ git pull
    3$ git switch -c feature/1-create-and-read
    
  4. Commit 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.py or tests/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
    
  5. Merge into main when 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 main unchanged since step 1: git moves main forward, with no conflict.
  6. The second to merge runs the same commands, and git pull brings the partner's routes first.

    • Both of you added routes in the same file, so git merge may 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
    
  7. Both pull main, and run the tests: each now has the whole API.

  • If git push to main is refused because your partner pushed first, run git 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

FieldTypeWho sets itRule
idintegerthe API, on createunique, never reused, never changed
titlestringthe clientrequired, 1–200 characters
authorstringthe clientrequired, 1–100 characters
yearintegerthe clientoptional; if sent, 0 to the current year
readbooleanthe clientoptional; 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 pathDoesSentReceived
POST /bookscreates a booka book without id201 and the new book, with its id
GET /bookslists every booknothing200 and a list of books, oldest first
GET /books/<id>reads one booknothing200 and the book
PUT /books/<id>replaces one booka whole book without id200 and the book as stored
DELETE /books/<id>deletes one booknothing204, and no body
GET /healthzsays the app is upnothing200 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

  • PUT replaces the whole book: every required field is sent again, and a missing year or read returns to its default.
  • The id comes from the URL, and an id in 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.

CaseStatus
No book with that id: read, update or delete404 Not Found
The body is not JSON, or a field is missing, of the wrong type or out of its range422 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 422 only when you return it, with the error body, from your own checks.
  • A body that is not JSON makes Flask answer 400 by itself: read the body in the mode that returns nothing instead, and answer 422.
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 422 by itself.
    • A body that is not JSON is a 422 as well.
    • Its error body is {"detail": [...]}, listing every failing field; keep it.
  • 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 /healthz answers 200 and {"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.

Translations: