├── .gitignore ├── Dockerfile ├── README.md ├── app ├── __init__.py ├── main.py └── restaurants.json ├── docker-compose.yml ├── requirements.txt └── tests ├── __init__.py └── test_dummy.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .python-version -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | WORKDIR /code 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install -r requirements.txt 7 | 8 | COPY app /code/app/ 9 | COPY tests tests 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python FastAPI workshop 2 | Let's build Restaurant API using modern Python and [FastAPI](https://fastapi.tiangolo.com/) as the web framework. 3 | 4 | ## Restaurant API - specification 5 | This is what we are going to implement today! 6 | 7 | A simple backend service which stores information about restaurants and has two endpoints for accessing that information: 8 | 1. GET _/restaurants_ which returns data for all the restaurants.The response payload format should be: 9 | ```json 10 | [ 11 | { 12 | "name": "Example restaurant", 13 | "description": "Example description", 14 | "id": "unique-id-for-the-restaurant", 15 | "location": { 16 | "city": "Example city", 17 | "coordinates": { 18 | "lat": 60.169938852212965, 19 | "lon": 24.941325187683105 20 | } 21 | } 22 | } 23 | ] 24 | ``` 25 | 26 | 2. GET _/restaurants/(restaurant-id)_ which returns data for a single restaurant. The response payload format should be: 27 | ```json 28 | { 29 | "name": "Example restaurant", 30 | "description": "Example description", 31 | "id": "unique-id-for-the-restaurant", 32 | "location": { 33 | "city": "Example city", 34 | "coordinates": { 35 | "lat": 60.169938852212965, 36 | "lon": 24.941325187683105 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | 43 | ## Restaurant data 44 | The restaurant data is stored in the [restaurants.json](app/restaurants.json) file which contains information about 20 imaginary restaurants which are located in Helsinki area. 45 | 46 | Example data for a single restaurant: 47 | ```json 48 | { 49 | "blurhash": "UUKJMXv|x]oz0gtRM{V@AHRQwvxZXSs9s;o0", 50 | "city": "Helsinki", 51 | "description": "Burgers with attitude!", 52 | "id": "456162c4-8dab-4959-8cdd-3777c7ede20d", 53 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5b348b31fe8992000bbec771/2be8c7738b220df2f9a0974da5c90d90", 54 | "location": [ 55 | 24.941325187683105, 56 | 60.169938852212965 57 | ], 58 | "name": "Burger plaza", 59 | "tags": [ 60 | "hamburger", 61 | "fries" 62 | ] 63 | } 64 | ``` 65 | 66 | Fields: 67 | 68 | * blurhash: A compact representation of a placeholder for an image (type: string), see https://blurha.sh/. 69 | * city: A city where the restaurant is located (type: string) 70 | * description: More information about what kind of restaurant it is (type: string) 71 | * id: Unique identifier for the restaurant (type: string) 72 | * image: A link to restaurant's image (type: string) 73 | * location: Restaurant's location in latitude & longitude coordinates. First element in the list is the longitude (type: a list containing two numbers) 74 | * name: The name of the restaurant (type: string) 75 | * tags: A list of tags describing what kind of food the restaurant sells, e.g. pizza / burger (type: a list of strings, max. 3 elements) 76 | 77 | ## Development 78 | 79 | ### With Docker 80 | 81 | #### Running the app 82 | Run the app: 83 | ``` 84 | docker compose up 85 | ``` 86 | 87 | The API documentation is available in http://127.0.0.1:8000/docs. 88 | 89 | #### Tests 90 | ``` 91 | docker compose run restaurant-api pytest 92 | ``` 93 | 94 | ### Without Docker 95 | **Prerequisites** 96 | * Python 3.10 or later: [https://www.python.org/downloads/](https://www.python.org/downloads/) 97 | 98 | #### Setting things up 99 | Create a virtual environment: 100 | ``` 101 | python -m venv .venv 102 | ``` 103 | 104 | Activate the virtual environment: 105 | 106 | * Linux / MacOS: 107 | ``` 108 | source .venv/bin/activate 109 | ``` 110 | * Windows (CMD): 111 | ``` 112 | .venv/Scripts/activate.bat 113 | ``` 114 | 115 | * Windows (Powershell) 116 | ``` 117 | .venv/Scripts/Activate.ps1 118 | ``` 119 | 120 | Install the dependencies 121 | ``` 122 | pip install -r requirements.txt 123 | ``` 124 | 125 | #### Running the app 126 | 127 | Run the server (`--reload` automatically restarts the server when there are changes in the code): 128 | ``` 129 | uvicorn app.main:app --reload 130 | ``` 131 | 132 | The API documentation is available in http://127.0.0.1:8000/docs. 133 | 134 | #### Tests 135 | ``` 136 | pytest 137 | ``` 138 | 139 | ## Additional features 140 | 141 | __You can code these on your own after the workshop 😉__ 142 | 1. Implement an endpoint which lists all the restaurants for a given tag. For example: GET /restaurants/tags/hamburger 143 | 2. Implement a search endpoint which takes a search string as a query parameter and returns the restaurants which have a full or a partial match for the search string in their name, description or tags fields. For example: GET /restaurants/search?q=sushi 144 | 3. Implement an endpoint which takes the customer location as a query parameter and returns the restaurants which are within 500 meters radius from the customer's location. For example: GET /restaurants/nearby?lat=60.17106&lon=24.934434 145 | 4. Instead of storing the restaurants in a static json file, store them in a database instead (hint: have a look at [SQLModel](https://sqlmodel.tiangolo.com/)). Additionally add endpoints for creating, updating, and deleting restaurants. 146 | 5. Implement automated tests for all the functionality you've built. Familiarise yourself with [pytest](https://docs.pytest.org/en/latest/) and read [how to test FastAPI applications](https://fastapi.tiangolo.com/tutorial/testing/). 147 | 148 | ## Tools to make your life easier with Python applications 149 | * [Poetry](https://python-poetry.org/docs/) for dependency management 150 | * [Ruff](https://docs.astral.sh/ruff/) for linting 151 | * [Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for automatic formatting 152 | * [Mypy](https://mypy.readthedocs.io/en/stable/) for static type checking 153 | * [Pre-commit](https://pre-commit.com/) for automatically running all the code quality related tools during commits/pushes 154 | * [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) for measuring the test coverage 155 | * [GitHub Actions](https://github.com/features/actions) for continuous integration 156 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/python-fastapi-workshop/9192e0bb2e3d27ab448da306f7093881cc6c965d/app/__init__.py -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI(title="Restaurant API") 4 | 5 | 6 | @app.get("/") 7 | def hello_world(): 8 | return {"Hello": "World"} 9 | -------------------------------------------------------------------------------- /app/restaurants.json: -------------------------------------------------------------------------------- 1 | { 2 | "restaurants": [ 3 | { 4 | "blurhash": "UUKJMXv|x]oz0gtRM{V@AHRQwvxZXSs9s;o0", 5 | "city": "Helsinki", 6 | "description": "Burgers with attitude!", 7 | "id": "456162c4-8dab-4959-8cdd-3777c7ede20d", 8 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5b348b31fe8992000bbec771/2be8c7738b220df2f9a0974da5c90d90", 9 | "location": [ 10 | 24.941325187683105, 11 | 60.169938852212965 12 | ], 13 | "name": "Burger plaza", 14 | "tags": [ 15 | "hamburger", 16 | "fries" 17 | ] 18 | }, 19 | { 20 | "blurhash": "U8INy*D+KjIW%3pZ$yx[5T0Lv|_1.3m,0z9h", 21 | "city": "Helsinki", 22 | "description": "Japanese ramen", 23 | "id": "99fe5fa3-16aa-44fa-8ee2-33bad84ef3c8", 24 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5d108aa82e757db3f4946ca9/d88ebd36611a5e56bfc6a60264fe3f81", 25 | "location": [ 26 | 24.941786527633663, 27 | 60.169934599421396 28 | ], 29 | "name": "Ramen house", 30 | "tags": [ 31 | "ramen", 32 | "risotto" 33 | ] 34 | }, 35 | { 36 | "blurhash": "UXJHswsy-n%0~T%Ka%NLNFjFxvNe%MRjM|ax", 37 | "city": "Helsinki", 38 | "description": "Gourmet kebab", 39 | "id": "c534233a-dc48-4624-b82c-7be09b481130", 40 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5abcf2d270aae6000deacff0/9c21840f97e01f5c293eae0993b4faa2", 41 | "location": [ 42 | 24.94184732393478, 43 | 60.16993168083865 44 | ], 45 | "name": "Doner house", 46 | "tags": [ 47 | "kebab", 48 | "doner" 49 | ] 50 | }, 51 | { 52 | "blurhash": "ULL;EN%4?c-Oys?wxuTJ?ujERQMxw[M{aeR4", 53 | "city": "Helsinki", 54 | "description": "Comfy Chinese restaurant", 55 | "id": "08c04d4e-fbe5-4619-bf11-1b02852fdb53", 56 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5cfa497902b535ee1c620fca/2c451d88fe96dc04d228dc4cd3dd23a5", 57 | "location": [ 58 | 24.94148075580597, 59 | 60.16990257838493 60 | ], 61 | "name": "Chinatown", 62 | "tags": [ 63 | "chinese", 64 | "asian" 65 | ] 66 | }, 67 | { 68 | "blurhash": "UELWIZ-n~pxt?a9tbwIpx^o#OoRkbsD%tP^P", 69 | "city": "Helsinki", 70 | "description": "Trendy pokebowls", 71 | "id": "66d2c74b-4e1b-47cd-bffc-db1485b6b0ca", 72 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5b8e46f4c9d2f5000b8216fa/4c97474360f98231b7bb1f90e430855d", 73 | "location": [ 74 | 24.941325187683105, 75 | 60.16988548380842 76 | ], 77 | "name": "Poke Bowling", 78 | "tags": [ 79 | "poke", 80 | "Hawaii" 81 | ] 82 | }, 83 | { 84 | "blurhash": "UZP$~|R*o~tRyEM{V[f7?wRjVsV@IAozfhae", 85 | "city": "Helsinki", 86 | "description": "Fancy sushi burritos", 87 | "id": "af895b0d-ca1e-43d3-8912-d1858365c695", 88 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/57d7af84b7426f114912b2a1/75742ebe3c411262325c647a3acbd5b2", 89 | "location": [ 90 | 24.9400752782822, 91 | 60.1701494897883 92 | ], 93 | "name": "Sushi Burrito", 94 | "tags": [ 95 | "sushi", 96 | "burrito" 97 | ] 98 | }, 99 | { 100 | "blurhash": "UdI3~;Ip0}Rk={bH9]oJW.RjnlbvNeV@xtbI", 101 | "city": "Helsinki", 102 | "description": "High quality sushi", 103 | "id": "c4497ff5-072a-4419-bfd5-11c31ee6cb89", 104 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5ad7050b879256000b477e16/705b6f09e5e4b33779b45afffb994cac", 105 | "location": [ 106 | 24.94049370288849, 107 | 60.16995648878383 108 | ], 109 | "name": "SushiFun", 110 | "tags": [ 111 | "sushi", 112 | "japanese" 113 | ] 114 | }, 115 | { 116 | "blurhash": "U8Q9.m.TDitlysx]9u%2?bVX-TIn_2gP?bxu", 117 | "city": "Helsinki", 118 | "description": "Urban sushi", 119 | "id": "10d356da-12f6-4921-9dbd-bb8364fa2b62", 120 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5afebf92317288000b8e4c17/24797b40bc5f136cb5605f4de79c0a42", 121 | "location": [ 122 | 24.941813349723816, 123 | 60.16974835162762 124 | ], 125 | "name": "Sushibar", 126 | "tags": [ 127 | "sushi", 128 | "japanese" 129 | ] 130 | }, 131 | { 132 | "blurhash": "UEKTMO_N@XpINfXn%Mxu_2kV9FtSi{V@RORj", 133 | "city": "Helsinki", 134 | "description": "Delicious pitas, salads and more", 135 | "id": "749beb15-6228-40be-beb3-5584badfa364", 136 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5de0e07c8995dc9c25304c9f/7680cf2cffae35b7302e3a91b65ce8f5", 137 | "location": [ 138 | 24.93900239467621, 139 | 60.1707249837842 140 | ], 141 | "name": "Fafa's Sokos", 142 | "tags": [ 143 | "street food", 144 | "pita" 145 | ] 146 | }, 147 | { 148 | "blurhash": "UEKTMO_N@XpINfXn%Mxu^+kV9FtSi{V@RORj", 149 | "city": "Helsinki", 150 | "description": "Delicious pitas and falafels", 151 | "id": "7b7385a7-e3e5-4bd5-ab45-aefc445857f4", 152 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5996b207ac09660afae98373/7680cf2cffae35b7302e3a91b65ce8f5", 153 | "location": [ 154 | 24.9417033791542, 155 | 60.169590913672 156 | ], 157 | "name": "Pita House", 158 | "tags": [ 159 | "pita", 160 | "falafel" 161 | ] 162 | }, 163 | { 164 | "blurhash": "UGGI7M9Y.T-Q_N4:.8W,-;xC-:o3Rkjvn$$$", 165 | "city": "Helsinki", 166 | "description": "Fresh ingredients", 167 | "id": "c55196e1-93cf-4aea-bb68-b84e4dec1121", 168 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5d63b2f947e0db48e4c19e46/f790340eaf6cce48712fd7a0887ba65c", 169 | "location": [ 170 | 24.941448569297787, 171 | 60.16931552051862 172 | ], 173 | "name": "Little East", 174 | "tags": [ 175 | "middle eastern", 176 | "north-african" 177 | ] 178 | }, 179 | { 180 | "blurhash": "UTE.Oqxu0fxu-:soWCSgNIWXxZs:NJW=kAs.", 181 | "city": "Helsinki", 182 | "description": "Meaty steaks", 183 | "id": "e9a29f1c-5014-48a2-9dec-940b5fa69b4a", 184 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/568f89054f2a1574b0bf0d21/ba4c91a913aa4e7bee2157d26800f9d3", 185 | "location": [ 186 | 24.94346022605896, 187 | 60.16959412414628 188 | ], 189 | "name": "Steak House", 190 | "tags": [ 191 | "steak", 192 | "bar" 193 | ] 194 | }, 195 | { 196 | "blurhash": "UULXA2%1%%a#oIxZM{kC.TofROWCxuRkogWU", 197 | "city": "Helsinki", 198 | "description": "Pizza everywhere", 199 | "id": "a97855fc-f9ea-4beb-86c0-d321d8d153b9", 200 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/55fff28cdef54a6c86e71bd5/dbe320a61e265a1a85293db4d4f962ae", 201 | "location": [ 202 | 24.94167387485504, 203 | 60.16923254771562 204 | ], 205 | "name": "Pizza everyday", 206 | "tags": [] 207 | }, 208 | { 209 | "blurhash": "UFCsQ;M|slM~$kfloLsA02xsR.xWxuslW:W=", 210 | "city": "Helsinki", 211 | "description": "Smoothies swiftly", 212 | "id": "00a0263b-d168-4699-bee8-3b69b06377ee", 213 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/56653ef5c26b4c6830ede645/7ebc624172ee732134538bebde1d0f20", 214 | "location": [ 215 | 24.94018793106079, 216 | 60.17241188234262 217 | ], 218 | "name": "Fresh & Juicy", 219 | "tags": [ 220 | "coffee" 221 | ] 222 | }, 223 | { 224 | "blurhash": "UOGuj,=_xt01nz%2$jR64nRjj[nkS$M|bFxu", 225 | "city": "Helsinki", 226 | "description": "Japanese food with a twist", 227 | "id": "eb63fde4-16fb-477f-88cd-90df607c5e71", 228 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5caf4751b216dd000dd73660/bb2cdb3942e414c2a07b153d6949c0c4", 229 | "location": [ 230 | 24.938106536865234, 231 | 60.171354167342905 232 | ], 233 | "name": "Japanese Fusion", 234 | "tags": [ 235 | "japanese", 236 | "modern" 237 | ] 238 | }, 239 | { 240 | "blurhash": "UHKKK5^Qo|%3*0GZXUR44n$fx9s;Z$M{xas,", 241 | "city": "Helsinki", 242 | "description": "Fresh, always fresh", 243 | "id": "bec1f38d-8429-40d7-8b0a-86cae8ae3cdf", 244 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/5c1bb3c58d5781000abfd079/0ff324c4df574ddc1a6699cf26992b55", 245 | "location": [ 246 | 24.945595264434814, 247 | 60.17096459277974 248 | ], 249 | "name": "Little Italy", 250 | "tags": [ 251 | "pizza", 252 | "pasta" 253 | ] 254 | }, 255 | { 256 | "blurhash": "UMLNV:rqJ7Mc~pkrNf%NyE-AVYt6J.oKRPx^", 257 | "city": "Helsinki", 258 | "description": "Wings", 259 | "id": "07c000e0-4ec5-4d2a-a5c1-de56f2f37339", 260 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/590c53172b10d62f9ac14830/2b416f8e941ceebb6e5d2e2efc8c81ce", 261 | "location": [ 262 | 24.9454665184021, 263 | 60.17155216317508 264 | ], 265 | "name": "Wing Buddies", 266 | "tags": [ 267 | "wings", 268 | "hotdog" 269 | ] 270 | }, 271 | { 272 | "blurhash": "UYLgb6ad*0%24nf6DiRjI]RjMxSgMxx[kBRQ", 273 | "city": "Helsinki", 274 | "description": "Fresh sushi", 275 | "id": "9efb5c15-64ba-4697-99ef-31ecfca76d46", 276 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/56a1d69c38115153ce516be7/b55be8561839f5bf03cf1e78933a8295", 277 | "location": [ 278 | 24.938321113586426, 279 | 60.16940733239114 280 | ], 281 | "name": "Sushi and Friends", 282 | "tags": [ 283 | "sushi", 284 | "japanese" 285 | ] 286 | }, 287 | { 288 | "blurhash": "UUG8TIIAnO%2~AspRkRP^iaJwwxZV@oLr?n%", 289 | "city": "Helsinki", 290 | "description": "Quality sushi", 291 | "id": "dd285292-1cfa-4bb2-98b3-7e928b457fe0", 292 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/54fed4b8747daa39fff94ff2/57606ccce2a2ed4ce98d4043934e013e", 293 | "location": [ 294 | 24.939225018024445, 295 | 60.16898783926865 296 | ], 297 | "name": "Tokyo house", 298 | "tags": [ 299 | "sushi", 300 | "japanese" 301 | ] 302 | }, 303 | { 304 | "blurhash": "UPF5N}I7WBNfoff6oejY+[JCNZS2jdawofWY", 305 | "city": "Helsinki", 306 | "description": "Vegan friendly", 307 | "id": "def85f15-ac5d-4f43-b2c6-7c058b963e53", 308 | "image": "https://prod-wolt-venue-images-cdn.wolt.com/568f828c4f2a1574b22f31f6/25d4cb0845abef6d0375f69df874bbb4", 309 | "location": [ 310 | 24.944517016410828, 311 | 60.16923121347787 312 | ], 313 | "name": "VegGrill", 314 | "tags": [ 315 | "vegan", 316 | "vegetarian" 317 | ] 318 | } 319 | ] 320 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | restaurant-api: 4 | build: . 5 | ports: 6 | - 127.0.0.1:8000:8000 7 | command: uvicorn --host 0.0.0.0 --port 8000 app.main:app --reload 8 | volumes: 9 | - ./app:/code/app 10 | - ./tests:/code/tests 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.108.0 2 | httpx==0.26.0 3 | pydantic==2.5.3 4 | pytest==7.4.4 5 | uvicorn==0.25.0 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woltapp/python-fastapi-workshop/9192e0bb2e3d27ab448da306f7093881cc6c965d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_dummy.py: -------------------------------------------------------------------------------- 1 | def test_dummy(): 2 | assert 1 == 1 3 | --------------------------------------------------------------------------------