-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathfastapi_example.py
More file actions
executable file
·289 lines (229 loc) · 10.1 KB
/
fastapi_example.py
File metadata and controls
executable file
·289 lines (229 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#!/usr/bin/python
'''
In this example, we will use FastAPI as a gateway into a MongoDB database. We will use a REST style
interface that allows users to initiate GET, POST, PUT, and DELETE requests.
Specifically, we are creating an app that tracks entering marvel characters from a user.
Then the user can ask for a list of various characters by their name.
The swift code for interacting with the interface is also available through the SMU MSLC class repository.
Look for the https://github.com/SMU-MSLC/SwiftHTTPExample with branches marked for FastAPI
To run this example in localhost mode only use the command:
fastapi dev fastapi_example.py
Otherwise, to run the app in deployment mode (allowing for external connections), use:
fastapi run fastapi_example.py
External connections will use your public facing IP, which you can find from the inet.
A useful command to find the right public facing ip is:
ifconfig |grep "inet "
which will return the ip for various network interfaces from your card.
which will return the ip for various network interfaces from your card. If you get something like this:
inet 10.9.181.129 netmask 0xffffc000 broadcast 10.9.191.255
then your app needs to connect to the netmask (the first ip), 10.9.181.129
'''
# For this to run properly, MongoDB should be running
# To start mongo use this: brew services start mongodb-community@6.0
# To stop it use this: brew services stop mongodb-community@6.0
# This App uses a combination of FastAPI and Motor (combining tornado/mongodb) which have documentation here:
# FastAPI: https://fastapi.tiangolo.com
# Motor: https://motor.readthedocs.io/en/stable/api-tornado/index.html
# Moreover, the following code was manipulated from a Mongo+FastAPI tutorial available here:
# There are quite a few changes from that code, so it will be quite different.
# Motor allows us to asynchronously interact with the database, allowing FastAPI to service
# requests while mongo commands are performed.
# https://github.com/mongodb-developer/mongodb-with-fastapi/tree/master
import os
from typing import Optional, List
from enum import Enum
# FastAPI imports
from fastapi import FastAPI, Body, HTTPException, status
from fastapi.responses import Response
from pydantic import ConfigDict, BaseModel, Field, EmailStr
from pydantic.functional_validators import BeforeValidator
from typing_extensions import Annotated
# Motor imports
from bson import ObjectId
import motor.motor_asyncio
from pymongo import ReturnDocument
# Create the FastAPI app
app = FastAPI(
title="Marvel Character API",
summary="A sample application showing how to use FastAPI to add a ReST API to a MongoDB collection.",
)
# Motor API allows us to directly interact with a hosted MongoDB server
# In this example, we assume that there is a single client
# First let's get access to the Mongo client that allows interactions locally
client = motor.motor_asyncio.AsyncIOMotorClient()
# new we need to create a database and a collection. These will create the db and the
# collection if they haven't been created yet. They are stored upon the first insert.
db = client.mcu
character_collection = db.get_collection("character")
# Represents an ObjectId field in the database.
# It will be represented as a `str` on the model so that it can be serialized to JSON.
PyObjectId = Annotated[str, BeforeValidator(str)]
# for storing the kind of character they are
class KindEnum(str, Enum):
hero = 'hero'
villain = 'villain'
grey = 'gray'
# create a quick hello world for connecting
@app.get("/")
def read_root():
return {"Hello:": "Mobile Sensing World"}
#========================================
# Data store objects from pydantic
#----------------------------------------
# These allow us to create a schema for our database and access it easily with FastAPI
# That might seem odd for a document DB, but its not! Mongo works faster when objects
# have a similar schema.
'''Create the MCU character model and use strong typing. This alos helps with the use of intellisense.
'''
class CharacterModel(BaseModel):
"""
Container for a single marvel character record.
"""
# The primary key for the CharacterModel, stored as a `str` on the instance.
# This will be aliased to `_id` when sent to MongoDB,
# but provided as `id` in the API requests and responses.
id: Optional[PyObjectId] = Field(alias="_id", default=None)
name: str = Field(...) # our character's name, no restrictions
power: str = Field(...) # a super power, if they have one
level: int = Field(..., le=5) # class of character (1 to 5 limit)
kind: str = Field(...) # Enum for good, bad, mixed characters
model_config = ConfigDict(
populate_by_name=True,
arbitrary_types_allowed=True,
json_schema_extra={ # provide an example for FastAPI to show users
"example": {
"name": "Natash Romanov",
"power": "None",
"level": 1,
"kind": 'hero',
}
},
)
class UpdateCharacterModel(BaseModel):
"""
A set of optional updates to be made to a document in the database.
"""
name: Optional[str] = None
power: Optional[str] = None
level: Optional[int] = None
kind: Optional[str] = None
model_config = ConfigDict(
arbitrary_types_allowed=True,
json_encoders={ObjectId: str},
json_schema_extra={
"example": {
"name": "Natash Romanov",
"power": "None",
"level": 1,
"kind": 'hero',
}
},
)
class CharacterCollection(BaseModel):
"""
A container holding a list of `CharacterModel` instances.
This exists because providing a top-level array in a JSON response can be a [vulnerability](https://haacked.com/archive/2009/06/25/json-hijacking.aspx/)
"""
characters: List[CharacterModel]
#========================================
# FastAPI methods, for interacting with db
#----------------------------------------
# These allow us to interact with the REST server. All interactions with mongo should be
# async, allowing the API to remain responsive even when servicing longer queries.
@app.post(
"/characters/",
response_description="Add new character",
response_model=CharacterModel,
status_code=status.HTTP_201_CREATED,
response_model_by_alias=False,
)
async def create_character(character: CharacterModel = Body(...)):
"""
Insert a new character record.
Update the character if a character by that name already exists.
Return the newly created character to the connected client
A unique `id` will be created and provided in the response.
"""
new_character = await character_collection.find_one_and_update(
{"name":character.name},
{"$set":character.model_dump(by_alias=True, exclude=["id"])}, # set all fields except id
upsert=True, # insert if nothing found.
return_document=ReturnDocument.AFTER)
return new_character
@app.get(
"/characters/",
response_description="List all characters",
response_model=CharacterCollection,
response_model_by_alias=False,
)
async def list_characters():
"""
List all of the characters data in the database.
The response is unpaginated and limited to 1000 results.
"""
return CharacterCollection(characters=await character_collection.find().to_list(1000))
@app.get(
"/characters/{name}",
response_description="Get a single mcu character",
response_model=CharacterModel,
response_model_by_alias=False,
)
async def show_character(name: str):
"""
Get the record for a specific character, looked up by `name`.
Any spaces in the name should be replaced by underscores.
"""
# replace any underscores with spaces
name_query = name.replace("_"," ")
if (
character := await character_collection.find_one({"name": name_query})
) is not None:
return character
raise HTTPException(status_code=404, detail=f"Character {name_query} not found")
@app.put(
"/characters/{name}",
response_description="Update a character",
response_model=CharacterModel,
response_model_by_alias=False,
)
async def update_character(name: str, character: UpdateCharacterModel = Body(...)):
"""
Update individual fields of an existing character record.
Only the provided fields will be updated.
Any missing or `null` fields will be ignored.
"""
# replace any underscores with spaces
name_query = name.replace("_"," ")
# only get fields that exist (from the put request)
character = {
k: v for k, v in character.model_dump(by_alias=True).items() if v is not None
}
# if we still have some fields to update
if len(character) >= 1:
# this is similar to the insert, but the user speifically
# wants to update a character. We will query, but NOT insert if not found.
update_result = await character_collection.find_one_and_update(
{"name": name_query},
{"$set": character},
return_document=ReturnDocument.AFTER,
)
if update_result is not None:
return update_result
else:
# return a common sense error to them
raise HTTPException(status_code=404, detail=f"Character {name_query} not found")
# The update is empty, but we should still return the matching document:
if (existing_character := await character_collection.find_one({"name": name_query})) is not None:
return existing_character
raise HTTPException(status_code=404, detail=f"Character {name_query} not found")
# @app.delete("/characters/{name}", response_description="Delete a character")
# async def delete_character(name: str):
# """
# Remove a single character record from the database.
# """
# # replace any underscores with spaces (to help support others)
# name_query = name.replace("_"," ")
# delete_result = await character_collection.delete_one({"name": name_query})
# if delete_result.deleted_count == 1:
# return Response(status_code=status.HTTP_204_NO_CONTENT)
# raise HTTPException(status_code=404, detail=f"Character {name_query} not found")