Python, Flask, MongoDB, Docker ile RestAPI Geliştirme¶
Bu eğitim serisinde python, flask, mongodb ve docker ile bir rest api geliştireceğiz.
Part 1¶
Kurulum ve Proje Yapısı¶
Uygulamayı çalıştırma ve ilk view fonksiyonu ve endpoint(uç noktayı) ekleme Requirements(Gereksinimler)
- python3.11
- Docker version 20.10.21
- Flask==2.2.2
- flask-mongoengine==1.0.0
- flask-marshmallow==0.14.0
- apifairy==1.3.0
Flask_MongoDB_Docker adında bir proje klasörü oluşturalım.
Yeni bir python projesine başlarken sanal bir ortam oluşturmak iyi bir pratiktir.
Proje klasörüne aşağıdaki komutu yazarak sanal ortamı oluşturalım.
$cd Flask_MongoDB_Docker/
# Sanal ortamımı env311 olarak adlandırdım
$ python3.11 -m venv env311
# Sanal ortamı etkinleştirin (macos/linux kullanıcıları)
$ source env311/bin/activate
# Windows kullanıcısıysanız bu şekilde etkinleştirebilirsiniz.
# $ env311\Scripts\activate
$ pip install flask
$ pip install flask-mongoengine
$ pip install python-dotenv
$ pip install flask-marshmallow
$ pip install apifairy
$ tree -I env311 Flask_MongoDB_Docker
Flask_MongoDB_Docker
├── Dockerfile
├── README.md
├── api
│ ├── __init__.py
│ ├── app.py
│ ├── models.py
│ ├── schemas.py
│ └── views.py
├── config.py
├── docker-compose.yml
├── main.py
└── requirements.txt
1 directory, 11 files
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
class Config:
# security options
SECRET_KEY = os.environ.get('SECRET_KEY', 'top-secret!')
# mongo db options eklenecek
# api/app.py
from flask import Flask
# internals
from config import Config
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
return app
# output of "flask run" command
* Serving Flask app 'main.py'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 854-344-911
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL
manually please check your spelling and try again.</p>
# api/views.py
from flask import Blueprint
books_bp = Blueprint("books", __name__)
@books_bp.route("/books/", methods=["GET"])
def books():
book_list = [
{"id":1, "name":"Python Flask & MongoDB", "author":"Adnan Kaya", "published_year": 2023},
{"id":2, "name":"Python for Absolute Beginners", "author":"Adnan Kaya", "published_year": 2025},
{"id":3, "name":"Learn Django by Developing Projects", "author":"Adnan Kaya", "published_year": 2024},
]
return book_list
# api/app.py
from flask import Flask
# internals
from config import Config
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# blueprints
API_URL_PREFIX = "/api/v1"
from api.views import books_bp
app.register_blueprint(books_bp, url_prefix=API_URL_PREFIX)
return app
[
{
"author": "Adnan Kaya",
"id": 1,
"name": "Python Flask & MongoDB",
"published_year": 2023
},
{
"author": "Adnan Kaya",
"id": 2,
"name": "Python for Absolute Beginners",
"published_year": 2025
},
{
"author": "Adnan Kaya",
"id": 3,
"name": "Learn Django by Developing Projects",
"published_year": 2024
}
]
Part 2¶
Mongoengine'i uygulamadan önce, mongodb'yi bir docker container(konteyner) olarak çalıştıralım. Terminalde aşağıdaki gibi çalıştırabilirsiniz.
docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=developer -e MONGO_INITDB_ROOT_PASSWORD=developer mongo
- Docker konteyneri kontrol edelim ve mongo kabuğunu/shell (mongosh) açalım:
-
Mongo shell
-
Şimdi config.py üzerinde flask uygulaması için mongodb konfigürasyonları ekleyelim
# config.py import os from dotenv import load_dotenv # .env yi yükler load_dotenv() BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class Config: # security options SECRET_KEY = os.environ.get('SECRET_KEY', 'top-secret!') # mongo db options MONGODB_SETTINGS = [ { "db": os.environ.get("MONGODB_DBNAME","mydb"), "host": os.environ.get("MONGODB_HOST","localhost"), "port": int(os.environ.get("MONGODB_PORT")) or 27017, "alias": "default", "username": os.environ.get("MONGODB_USERNAME","developer"), "password": os.environ.get("MONGODB_PASSWORD","developer") } ]
-
Ayrıca bu ortam değişkenlerini(environment variables) .env dosyasına ekleyebiliriz.
-
Şimdi api/app.py'de MongoEngine örneği oluşturuluyor(instantiating)
# api/app.py from flask import Flask from flask_mongoengine import MongoEngine # yeni # internals from config import Config db = MongoEngine() # yeni def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) # database mongodb db.init_app(app) # yeni # blueprints API_URL_PREFIX = "/api/v1" from api.views import books_bp app.register_blueprint(books_bp, url_prefix=API_URL_PREFIX) return app
-
api/init.py içine db'yi de import etmeliyiz.
-
Şimdi modellerimizi oluşturma zamanı. models.py'yi açın
-
Modelimizi oluşturduktan sonra flask shell üzerinde oynayalım, terminali açabilirsiniz
$ flask shell
# Python 3.11.0 (v3.11.0:deaf509e8f, Oct 24 2022, 14:43:23) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
# App: api.app
# Instance: /<my-path>/Flask_MongoDB_Docker/instance
>>> from api.models import *
>>> book = Book(name="Python Flask", author="Adnan Kaya", published_year=2023)
>>> book.save()
<Book: Python Flask>
- İşte bu! Veritabanında ilk kaydımızı oluşturduk. Mongo kabuğunda/shell kontrol edelim
test> show dbs
admin 100.00 KiB
config 92.00 KiB
local 72.00 KiB
mydb 8.00 KiB # our database is created
# Bizim veritabanı mydb olduğu icin onu seciyoruz
test> use mydb
switched to db mydb
# show collections(tablolari goster)
mydb> show collections
book
# book kayitlarini getir
mydb> db.book.find()
[
{
_id: ObjectId("638fb5df4653579e2ea4367c"),
name: 'Python Flask',
author: 'Adnan Kaya',
published_year: 2023
}
]
-
2 kayıt daha ekledik. Şimdi api'miz aracılığıyla veritabanından veri almak için api/views.py dosyasını düzenleyelim.
-
Şimdi http://localhost:5000/api/v1/books/ adresine bir GET isteği yapın. Yanıt durumu 200, ancak herhangi bir yanıt verimiz yok ve ayrıca terminalde hatamız var:
127.0.0.1 - - [07/Dec/2022 00:53:38] "GET /api/v1/books/ HTTP/1.1" 200 - Error on request: Traceback (most recent call last): File "/<mypath>/Flask_MongoDB_Docker/env311/lib/python3.11/site-packages/werkzeug/serving.py", line 335, in run_wsgi execute(self.server.app) File "/<mypath>/Flask_MongoDB_Docker/env311/lib/python3.11/site-packages/werkzeug/serving.py", line 325, in execute write(data) File "/<mypath>/Flask_MongoDB_Docker/env311/lib/python3.11/site-packages/werkzeug/serving.py", line 293, in write assert isinstance(data, bytes), "applications must write bytes" AssertionError: applications must write bytes
-
Hata mesajı özeti
-
Yani dönülecek veri byte türünde olmalıdır. views.py'deki book_list türü şu şekildedir:
-
book_list ise BaseQuerySet türündedir.
Bu sorunun üstesinden gelmek için flask.jsonify() kullanmalıyız. api/views.py dosyasını açın ve jsonify'ı içe aktarın ve jsonify'a ileterek book_list'i döndürün
from flask import Blueprint, jsonify # yeni
# internals
from .models import Book
books_bp = Blueprint("books", __name__)
@books_bp.route("/books/", methods=["GET"])
def books():
book_list = Book.objects.all()
return jsonify(book_list) # yeni
-
Şimdi http://localhost:5000/api/v1/books/ adresine bir GET isteği yapın ve aşağıdaki sonucu alacaksınız.
[ { "_id": { "$oid": "638fb5df4653579e2ea4367c" }, "author": "Adnan Kaya", "name": "Python Flask", "published_year": 2023 }, { "_id": { "$oid": "638fb80a4653579e2ea4367d" }, "author": "Adnan Kaya", "name": "Python Django", "published_year": 2025 }, { "_id": { "$oid": "638fb83b4653579e2ea4367e" }, "author": "Adnan Kaya", "name": "Python Tkinter", "published_year": 2026 } ]
-
_id ve $oid (object id) otomatik olarak oluşturulur.
Part 3¶
- Projemizi dockerize edip yeni modeller ekleyeceğiz. Başlayalım!
docker-compose.yml dosyamızı yazmaya başlayacağız.
# Flask_MongoDB_Docker/docker-compose.yml
version: '3.7'
services:
db:
image: mongo:latest
container_name: mymongodb
ports:
- 27017:27017
volumes:
- mongo_data:/data/db
volumes:
mongo_data:
- Yalnızca mongodb'nin en son imajını kullanan ve konteynerin adı mymongodb olan db servisini ekledik (bu container adına dikkat edin çünkü flask(web) servisini eklediğimizde bu container adını flask konteynerinden mongo konteynerine bağlanmak için kullanacağız).
Son olarak volumes mongo_data olarak adlandırdık. Aşağıdaki komutu yazarak mongodb container başlatalım:
# docker-compose.yml dosyasında db servisi bizim mongodb servisimizdir.
$ docker compose up -d --build db
# output
[+] Running 2/2
⠿ Network flask_mongodb_docker_default Created 0.0s
⠿ Container mymongodb Started
-
Konteyner statusunu kontrol edelim
-
.env dosyamıza bir göz atalım
-
Şu anda sadece mongodb olan 1 konteynerimiz var. Terminalimizden flaski çalıştıracağız. Mongodb'ye doğrudan flask'tan bağlandığımız için (herhangi bir konteynerden değil) , MONGODB_HOST'u localhost olarak kullanacağız. Flask'i docker compose ile başlattığımızda MONGODB_HOST değeri mongodb servisinin container adı olacaktır. Flask'i çalıştıralım:
-
http://127.0.0.1:5000/api/v1/books/ adresine bir GET isteği yapın
- HTTP Status 200 ile boş liste [ ] yanıtı almalısınız. Ancak part2'de 3 kayıt eklemiştik. Verilerimiz nerede?
- Part2'de container verilerini bağlamadık. Bu nedenle önceki konteyner kaldırıldığında 3 adet kayıt da kaybedildi.
- docker-compose.yml dosyasında, container kaldırılsa bile verilerimizi kalıcı olarak saklamak için birimimizi/volume belirledik.
Yeniden veri eklemeden şimdi flask'ı dockerize edelim ve mongodb konteynerine bağlanalım. docker-compose-yml dosyasını açın ve servislere web ekleyin.
# Flask_MongoDB_Docker/docker-compose.yml
version: '3.7'
services:
web:
container_name: flask_mongodb_docker_tutorial
build:
context: .
dockerfile: ./Dockerfile
volumes:
- .:/usr/src/app/
ports:
- 5000:5000
env_file:
- ./.env
depends_on:
- db
stdin_open: true
tty: true
db:
image: mongo:latest
container_name: mymongodb
ports:
- 27017:27017
volumes:
- mongo_data:/data/db
volumes:
mongo_data:
- Şimdi .env dosyasını aşağıdaki gibi düzenleyin
FLASK_APP=main.py
FLASK_DEBUG=1
MONGODB_DBNAME=mydb
# MONGODB_HOST=localhost
# docker compose kullandigimizda MONGODB_HOST=container_name olmali ki
# web ile db servisleri haberlesebilsin
MONGODB_HOST=mymongodb
MONGODB_PORT=27017
MONGODB_USERNAME=developer
MONGODB_PASSWORD=developer
-
Şimdi Dockerfile dosyamızı düzenleyeceğiz.
# Flask_MongoDB_Docker/Dockerfile # python3.11 resmi imajı indir FROM python:3.11.0-slim-buster # work directory belirle WORKDIR /usr/src/app # environment variables / ortam değişkenleri belirle ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 # sistem bagimliliklarini kur RUN apt-get update && apt-get install -y netcat RUN pip install --upgrade pip # requirements.txt yi container WORKDIR dizinine kopyala COPY ./requirements.txt /usr/src/app/requirements.txt # proje paketlerini kur RUN pip install -r requirements.txt # projeyi WORKDIR dizinine kopyala COPY . /usr/src/app/ # development modda çalıştır CMD ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=5000"]
-
Docker komutları için yorum satırları ekledim. Kolayca anlayabilirsiniz.
Şimdi terminalde çalışan flask uygulamasını durdurun. Ve flask(web) servisi oluşturun.
$ docker compose up -d --build web
# output
[+] Building 2.6s (12/12) FINISHED
# ...
# .....
[+] Running 2/2
⠿ Container mymongodb Running 0.0s
⠿ Container flask_mongodb_docker_tutorial Started
-
Container status kontrol edelim
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ccd7f1c0de54 flask_mongodb_docker-web "python -m flask run…" 4 minutes ago Up 3 minutes 0.0.0.0:5000->5000/tcp flask_mongodb_docker_tutorial dfa10167aa02 mongo:latest "docker-entrypoint.s…" 37 minutes ago Up 37 minutes 0.0.0.0:27017->27017/tcp mymongodb
-
Artık bir GET isteğini http://127.0.0.1:5000/api/v1/books/ adresine yapabilir ve yanıt olarak boş listeyi ve durumunu 200 olarak alabilirsiniz.
Yeni kayıtlar eklemeden önce, models.py'yi düzenleyelim ve modeller arasında ilişki kurmak için yeni model ekleyelim.
# Flask_MongoDB_Docker/api/models.py
# internals
from api import db
class Author(db.Document):
firstname = db.StringField(max_length=64)
lastname = db.StringField(max_length=64)
def __str__(self):
return f"{self.firstname} {self.lastname}"
class Book(db.Document):
name = db.StringField(max_length=64)
author = db.ReferenceField(document_type=Author)
published_year = db.IntField(min=610)
def __str__(self) -> str:
return self.name
- Her kitabın bir yazarı vardır. Basit ilişki (Birden Çoğa / One-To-Many)
Flask kabuğunda author ve book kayıtları oluşturalım. Öncelikle flask konteynerine girmemiz gerekiyor.
$ docker compose exec web sh
# ls
Dockerfile README.md __pycache__ api config.py docker-compose.yml env311 main.py requirements.txt
# pwd
/usr/src/app
-
Şimdi flask shell
-
Kayıtlar ekleyelim
>>> from api.models import Author, Book >>> a1 = Author.objects.create(firstname="Adnan", lastname="Kaya") >>> a2 = Author.objects.create(firstname="Recep", lastname="Alkurt") >>> a1 <Author: Adnan Kaya> >>> a2 <Author: Recep Alkurt> >>> b1 = Book.objects.create(author=a1, name="Flask 101", published_year=2023) >>> b2 = Book.objects.create(author=a2, name="Angular 101", published_year=2023) >>> b3 = Book.objects.create(author=a1, name="Django 101", published_year=2025) >>> b4 = Book.objects.create(author=a1, name="Scrapy 101", published_year=2024) >>> b1 <Book: Flask 101> >>> b1.author <Author: Adnan Kaya> >>> b2 <Book: Angular 101> >>> b2.author <Author: Recep Alkurt>
-
HARİKA! Veritabanına 2 yazar ve 4 kitap ekledik.
views.py dosyasına bir göz atalım (Şimdiye kadar hiçbir şeyi değiştirmedik)
from flask import Blueprint, jsonify
# internals
from .models import Book
books_bp = Blueprint("books", __name__)
@books_bp.route("/books/", methods=["GET"])
def books():
book_list = Book.objects.all()
return jsonify(book_list)
-
http://127.0.0.1:5000/api/v1/books/ adresine bir GET isteği yapın, yanıt şu olacaktır:
[ { "_id": { "$oid": "63b9d62bd1627fb9544b8833" }, "author": { "$oid": "63b9d5ded1627fb9544b8831" }, "name": "Flask 101", "published_year": 2023 }, { "_id": { "$oid": "63b9d63dd1627fb9544b8834" }, "author": { "$oid": "63b9d601d1627fb9544b8832" }, "name": "Angular 101", "published_year": 2023 }, { "_id": { "$oid": "63b9d64dd1627fb9544b8835" }, "author": { "$oid": "63b9d5ded1627fb9544b8831" }, "name": "Django 101", "published_year": 2025 }, { "_id": { "$oid": "63b9d65cd1627fb9544b8836" }, "author": { "$oid": "63b9d5ded1627fb9544b8831" }, "name": "Scrapy 101", "published_year": 2024 } ]
-
Sonraki partta serialization ve CRUD operasyonları yapacağız.
Part 4¶
Bu partta flask_marshmallow(bir nesne serileştirme kütüphanesi) extension/uzantısını uygulayacağız.
Ayrıca CRUD işlemleri de yapacağız.
Proje dosya yapısını tekrar hatırlayalım
Flask_MongoDB_Docker $ tree . -I env311
.
├── Dockerfile
├── README.md
├── __pycache__
├── api
│ ├── __init__.py
│ ├── __pycache__
│ ├── app.py
│ ├── models.py
│ ├── schemas.py
│ └── views.py
├── config.py
├── docker-compose.yml
├── main.py
└── requirements.txt
3 directories, 11 files
-
flask_marshmallow'u uygulayalım. api/app.py dosyasını açın ve flask_marshmallow'u import edin
from flask import Flask from flask_mongoengine import MongoEngine from flask_marshmallow import Marshmallow # yeni # internals from config import Config db = MongoEngine() marsh = Marshmallow() # yeni def create_app(config_class=Config): # existing code ... # serialization/deserialization Marshmallow marsh.init_app(app) # yeni # existing code ... return app
-
Ve api/init.py dosyasını aşağıdaki gibi düzenleyin
-
Şimdi api/schemas.py dosyasını açın ve AuthorSchema ve BookSchema sınıflarını oluşturun ve serialize/deserialize etmek için alanları ekleyin (bu alanları model sınıflarında da tanımladığımızı unutmayın). Bu yüzden tüm model alanlarını serialize etmek istiyoruz ve şema sınıflarında da tanımladık. Yalnızca istediğiniz alanları serialize etmek istiyorsanız, yalnızca istediğinizi tanımlayabilirsiniz.
# api/schemas.py from marshmallow import validate # internals from api import marsh as ma class AuthorSchema(ma.Schema): id = ma.String(unique=True, dump_only=True) firstname = ma.String(required=True) lastname = ma.String(required=True) class Meta: ordered = True class BookSchema(ma.Schema): id = ma.String(unique=True, dump_only=True) name = ma.String(required=True) author = ma.Nested(AuthorSchema, dump_only=True) published_year = ma.Integer(strict=True, required=True, validate=[validate.Range( min=610, error="Year must be min 610")] ) class Meta: ordered = True
-
AuthorSchema sınıfı, tümü string olan id, firstname ve lastname olmak üzere üç alan tanımlar. id alanı benzersiz olarak işaretlenir ve yalnızca serileştirilmiş çıktıya dahil edilir (dump_only=True).
- BookSchema sınıfı, id, name, author ve published_year olmak üzere dört alanı tanımlar. id alanı benzersiz olarak işaretlenir ve yalnızca serileştirilmiş çıktıya dahil edilir.
- Ad alanı zorunludur. author alanı nested(iç içedir), yani AuthorSchema sınıfının bir örneğidir(instance) ve ayrıca yalnızca serileştirilmiş çıktıya dahil edilir.
- published_year alanı bir tamsayıdır, zorunludur ve yalnızca tamsayıları kabul eder ve tamsayı olmayan bir değer iletilirse hata verir. Ayrıca, yılın en az 610 olması gereken bir aralık doğrulamasıyla doğrulanır.
- Her iki sınıfın da ordered(sıralı) olarak True, ayarlanan bir Meta sınıfı vardır. Bu, alanların bildirildiği sıraya göre sıralı serileştirme çıktısı anlamına gelir.
-
Şimdi bu şemaları api/views.py içinde kullanmanın zamanı geldi.
from flask import Blueprint, jsonify from apifairy import response # yeni # internals from .models import Book from .schemas import AuthorSchema, BookSchema # yeni books_bp = Blueprint("books", __name__) books_schema = BookSchema(many=True) # yeni @books_bp.route("/books/", methods=["GET"]) @response(books_schema) # yeni def books(): book_list = Book.objects.all() return book_list
-
Özel bir dekoratör olan apifairy'den bir response decorator import ediyoruz.
- Ayrıca daha önce tanımladığımız AuthorSchema ve BookSchema'yı import ediyoruz.
- Ve sonra, marshmallow'a bir nesne listesini serileştireceğini söyleyen many parametresi True olarak ayarlanan BookSchema sınıfının bir örneğini oluşturuyoruz.
- Yanıt dekoratörü, books_schema'yı bağımsız değişken olarak alan özel bir dekoratördür ve döndürülen verileri yanıt olarak göndermeden önce otomatik olarak serialize eder.
-
http://127.0.0.1:5000/api/v1/books/ adresine bir GET isteği yapalım ve yanıtı görelim. Yanıtı böyle görmelisiniz:
[ { "author": { "firstname": "Adnan", "id": "63b9d5ded1627fb9544b8831", "lastname": "Kaya" }, "id": "63b9d62bd1627fb9544b8833", "name": "Flask 101", "published_year": 2023 }, { "author": { "firstname": "Recep", "id": "63b9d601d1627fb9544b8832", "lastname": "Alkurt" }, "id": "63b9d63dd1627fb9544b8834", "name": "Angular 101", "published_year": 2023 }, { "author": { "firstname": "Adnan", "id": "63b9d5ded1627fb9544b8831", "lastname": "Kaya" }, "id": "63b9d64dd1627fb9544b8835", "name": "Django 101", "published_year": 2025 }, { "author": { "firstname": "Adnan", "id": "63b9d5ded1627fb9544b8831", "lastname": "Kaya" }, "id": "63b9d65cd1627fb9544b8836", "name": "Scrapy 101", "published_year": 2024 } ]
-
Dikkat ettiyseniz $oid yok ve ilişkili nesne alanları serialize edilerek döndürülür.
Yazar Oluşturmak Şimdi POST isteği yapmak için /authors/ endpoint ve onun view fonksiyonunu create_author olarak ekleyelim. api/views.py'yi açın:
from flask import Blueprint, jsonify
from apifairy import response, body # yeni
# internals
from .models import Book, Author # yeni
from .schemas import AuthorSchema, BookSchema
books_bp = Blueprint("books", __name__)
books_schema = BookSchema(many=True)
author_schema = AuthorSchema() # yeni
@books_bp.route("/authors/", methods=["POST"])
@body(author_schema)
@response(author_schema, status_code=201)
def create_author(payload):
author = Author(**payload)
author.save()
return author
- create_author fonksiyonu, üç dekoratörle dekore edilmiştir.
Birincisi @books_bp.route("/authors/", method=["POST"]) olup, /authors/ endpointini bu fonksiyona eşler ve bu endpointe yalnızca POST isteklerine izin verir. İkinci dekoratör, istek gövdesini author_schema'ya göre doğrulayan @body(author_schema) dekoratörüdür ve request body verisini ayrıştırıp fonksiyona bir payload argümanı olarak iletir. Üçüncü dekoratör @response(author_schema, status_code=201) dekoratöre author_schema kullanarak döndürülen verileri serialize edilmesi ve kaydın başarıyla oluşturulduğunu gösteren yanıtın durum kodunu 201 olarak ayarlamasını söyler. Şimdi request body ile http://127.0.0.1:5000/api/v1/authors/ için bir POST isteği yapalım. Request body:
- Aşağıdaki gibi yanıt alacağız
Authors Listesini Çekmek Yazar listesini almak için bir endpoint ve onun view fonksiyonunu oluşturalım. api/views.py açalım:
# öncekiler ...
author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True) # yeni
@books_bp.route("/authors/", methods=["GET"])
@response(authors_schema)
def authors():
author_list = Author.objects.all()
return author_list
- http://127.0.0.1:5000/api/v1/authors/ adresine bir GET isteği yapın ve yanıt şu şekilde olacaktır:
[ { "firstname": "Adnan", "id": "63b9d5ded1627fb9544b8831", "lastname": "Kaya" }, { "firstname": "Recep", "id": "63b9d601d1627fb9544b8832", "lastname": "Alkurt" }, { "firstname": "Adnan2", "id": "63c31e4110bf2407b4c2eb89", "lastname": "Kaya2" }, { "firstname": "Adnan3", "id": "63c31e9c33f15afb371e3144", "lastname": "Kaya3" }, { "firstname": "Adnan4", "id": "63c31f8dafd3ee10e7cc48da", "lastname": "Kaya4" } ]
Book Kaydı Oluşturmak Şimdi yeni kitap eklemek için POST isteği yapmak üzere /books/ endpointi oluşturalım.
Yeni fonksiyon oluşturmadan önce BookSchema'yı düzenlememiz gerekiyor. api/schemas.py açalım:
class BookSchema(ma.Schema):
# öncekiler ...
author_id = ma.String(load_only=True, required=True) # yeni
# öncekiler ...
-
Bir string olan yeni bir author_id alanı tanımlar ve load_only=True ve required=True olarak işaretlenir, bu, bu alanın yalnızca seri hale getirilmiş girdide bulunacağı ve gerekli olduğu anlamına gelir. Bu, sunucuya bir JSON payload gönderildiğinde, bu alanın bulunmasının beklendiği ve kitapla ilişkili yazarı tanımlamak için kullanılacağı anlamına gelir. Bu, senaryomuzda kullanışlıdır, çünkü giriş verileri üzerinde doğrulamayı zorunlu kılarak, müşterinin yeni bir kitap oluşturmadan önce geçerli bir author_id göndermesini sağlar. Ayrıca, bu alanın serileştirilmiş çıktıda bulunmadığını ve bu nedenle müşteriye verilen yanıta dahil edilmeyeceğini belirtmekte fayda var. api/views.py dosyasını açın
from flask import Blueprint, jsonify, abort # yeni # öncekiler ... books_schema = BookSchema(many=True) book_schema = BookSchema() # yeni # öncekiler ... @books_bp.route("/books/", methods=["POST"]) @body(book_schema) @response(book_schema, status_code=201) def create_book(payload): author_id = payload.pop("author_id") try: author = Author.objects.get(id=author_id) except Author.DoesNotExist as exc: return abort(code=404, description="Author does not exist, check your author_id again") except: return abort(code=400) else: book = Book(**payload, author=author) book.save() return book
-
create_book fonksiyonu, request body'den author_id ayıklayarak başlar ve ardından id numarasına göre ilgili Author nesnesini veritabanından almaya çalışır. Author mevcut değilse, 400 durum kodu ve "Yazar mevcut değil, yazar_kimliğinizi tekrar kontrol edin" şeklinde bir "açıklama" içeren bir yanıt döndürür. Author mevcutsa, fonksiyon kalan request body/payload verileriyle yeni bir Book nesnesi oluşturur ve author attribute alınan Author nesnesine atanır, ardından bunu veritabanına kaydeder ve yeni oluşturulan book nesnesini döndürür. Şimdi http://127.0.0.1:5000/api/v1/authors/ için bir GET isteği yapın ve author id'lerden birini alın. Adnan2'nin id'sini aldım.
Şimdi request body ile http://127.0.0.1:5000/api/v1/books/ adresine bir POST isteği yapın. Request body:
{
"name":"Rest API development with Flask, MongoDB, Docker",
"published_year":2023,
"author_id": "63c31e4110bf2407b4c2eb89"
}
- Kayıt başarılı bir şekilde oluşturulmalıdır.
ID Göre Author Silmek api/views.py dosyasını açın ve aşağıdakileri ekleyin
@books_bp.route("/authors/<string:id>", methods=["DELETE"])
def delete_author(id):
try:
author = Author.objects.get(id=id)
author.delete()
return jsonify(message=f"Deleted {author}!")
except Author.DoesNotExist as ex:
return abort(404, description=ex)
except:
return abort(400)
- Yazar id'lerinden birini alın ve http://127.0.0.1:5000/api/v1/authors/63c31e9c33f15afb371e3144 gibi HTTP DELETE isteği yapın
ID Göre Author Güncelle (PUT , PATCH isteği) api/views.py'yi açın ve aşağıdakileri ekleyin
# öncekiler ..
author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True)
update_author_schema = AuthorSchema(partial=True) # yeni
# öncekiler ...
@books_bp.route("/authors/<string:id>", methods=["PUT", "PATCH"])
@body(update_author_schema)
@response(author_schema, status_code=200)
def update_author(payload, id):
try:
author = Author.objects.get(id=id)
author.update(**payload)
author.save()
author.reload()
return author
except Author.DoesNotExist as ex:
return abort(404, description=ex)
except:
return abort(400)
-
update_author fonksiyonu, karşılık gelen Author nesnesini id ile veritabanından almaya çalışarak başlar. Author yoksa, 404 durum kodu ve exception mesajının "açıklaması" ile bir yanıt döndürür. Author varsa, payload verilerini kullanarak author nesnesinin alanlarını günceller ve bunu veritabanına kaydeder ve author kaydını veritabanından yeniden yükler ve güncellenen author nesnesini döndürür. Başka bir exception meydana gelirse, 400 durum koduyla bir cevap döndürür. İstek gövdesi(request body) ile http://127.0.0.1:5000/api/v1/authors/63c31e4110bf2407b4c2eb89 adresine PUT isteği yapalım.
-
Cevap aşağıdaki gibidir
-
İstek gövdesi ile PATCH isteğinde bulunun
- Cevap
Part 5¶
Unit testler bu yazımızda projeye eklenecektir.
Docker dışında testler yapacaksanız, pytest'i manuel olarak kurabilirsiniz.
-
Testleri çalıştırırken docker kullanacağız bu yüzden requirements.txt dosyasına pytest ekleyeceğim.
-
Şimdi api/ klasörü altında tests klasörü oluşturmaya başlıyoruz ve api/tests/ klasörü altında 3 adet test dosyası ve 1 adet base dosyası oluşturuyoruz. Klasör yapısı aşağıdaki gibi olacak
-
Temel sınıflara ortak ve hazırlık çalışmaları koymak iyi bir uygulamadır. api/tests/base.py dosyasını dolduralım
-
Bu kod, Config adlı başka bir sınıftan miras alan TestConfig adlı bir sınıfı tanımlar. TestConfig sınıfı, üst sınıfın çeşitli niteliklerini override eder. SERVER_NAME özniteliği(attribute), yerel ana bilgisayarın IP adresi ve bağlantı noktası numarası olan '127.0.0.1:5000' olarak ayarlanmıştır. TESTING özniteliği, kodun bir test ortamında çalıştığını gösteren True olarak ayarlanır. MONGODB_SETTINGS özniteliği, bir MongoDB veritabanına bağlanmak için bir ayarlar sözlüğü(dict) içeren bir dizidir. Bu TestConfig sınıfı, test ortamı için belirli yapılandırmalar ayarlayacaktır. api/tests/base.py'de temel test sınıfını yazalım
import unittest # internals from api import db, create_app from config import Config class TestConfig(Config): SERVER_NAME = '127.0.0.1:5000' TESTING = True MONGODB_SETTINGS = [ { "db": "test_db", "host": "mymongodb", # docker db host adi "port": 27017, "alias": "default", "username": "developer", "password": "developer", "connect": False, } ] class BaseTestCase(unittest.TestCase): ''' Run: docker compose exec web sh -d "pytest -s --disable-warnings" ''' config = TestConfig def setUp(self): db.disconnect("default") # create flask app self.app = create_app(self.config) self.app_context = self.app.app_context() self.app_context.push() # drop previous existing data db.connection["default"].drop_database("test_db") self.client = self.app.test_client() def tearDown(self): db.connection["default"].drop_database("test_db") db.disconnect() self.app_context.pop()
-
Bu sınıf, unittest.TestCase'den miras alınan BaseTestCase olarak adlandırılır. Bu, bir birim test paketindeki diğer test durumları için üst sınıf olarak kullanılması amaçlanan bir temel test durumu sınıfıdır. Sınıfın, önceki kod parçacığında tanımlanan TestConfig sınıfına ayarlanmış bir yapılandırma özniteliği vardır. setUp() fonksiyonu, test senaryosundaki her test yönteminden önce çalıştırılır ve test ortamını ayarlamak için kullanılır. Fonksiyon, db.disconnect() fonksiyonunu kullanarak MongoDB veritabanıyla bağlantıyı keserek başlar. Ardından, create_app() işlevini kullanarak, config özniteliğini bir argüman olarak ileten yeni bir Flask uygulaması oluşturur. Fonksiyon daha sonra uygulama bağlamını zorlar, böylece test durumu uygulamanın kaynaklarına erişebilir ve mevcut veritabanını kaldırır. tearDown() fonksiyonu, test senaryosundaki her test yönteminden sonra çalıştırılır ve testten sonra ortamı temizlemek için kullanılır. Mevcut test veritabanını kaldırarak, veritabanı bağlantısını keserek ve uygulama içeriğini açarak başlar. Bu kodun MongoDB veritabanını kullanan flask uygulamamızda entegrasyon testi için temel sınıf olarak kullanılması amaçlanmıştır, setUp fonksiyonu test ortamını kuracak, uygulamayı oluşturacak ve test veritabanına bağlanacak ve tearDown fonksiyonu test bittikten sonra temizlemek için çalışır. Modelleri Test Etme Şimdi api/tests/ klasörü altında test_models.py dosyasını oluşturalım ve 2 sınıfı dolduralım.
from api.models import Author, Book from api.tests.base import BaseTestCase class TestAuthorModel(BaseTestCase): ''' docker compose exec web sh -c "pytest -v api/tests/test_models.py -s --disable-warnings" ''' def setUp(self): super().setUp() self.author = Author(firstname='John', lastname='Doe') def test_author_str_representation(self): self.assertEqual(str(self.author), 'John Doe') def test_author_save(self): self.author.save() result = Author.objects(firstname='John', lastname='Doe') self.assertEqual(len(result), 1) self.assertEqual(result[0].firstname, 'John') self.assertEqual(result[0].lastname, 'Doe') class TestBookModel(BaseTestCase): def setUp(self): super().setUp() self.author = Author(firstname='John', lastname='Doe').save() self.book = Book(name='Test Book', author=self.author, published_year=2000) def test_book_str_representation(self): self.assertEqual(str(self.book), 'Test Book') def test_book_save(self): self.book.save() result = Book.objects(name='Test Book') self.assertEqual(len(result), 1) self.assertEqual(result[0].name, 'Test Book') self.assertEqual(str(result[0].author), 'John Doe') self.assertEqual(result[0].published_year, 2000)
Bu sınıfların her ikisi de, sırasıyla Author ve Book modellerinin işlevselliğini test etmeyi amaçlayan test senaryolarıdır.
TestAuthorModel sınıfının 3 fonksiyonu vardır:
super() kullanarak üst sınıfın setUp() yöntemini çağıran ve firstname='John' ve lastname='Doe' ile Author modelinin yeni bir örneğini oluşturan setUp() fonksiyonu vardır. Author modelinin string olarak temsilinin 'firstname lastname' biçiminde olduğunu test eden test_author_str_representation() fonksiyonu vardır. Author modelinin save() fonksiyonunun modeli veritabanına doğru bir şekilde kaydettiğini ve Author modelinin objects() fonksiyonunun kaydedilen modeli veritabanından alabildiğini test eden test_author_save() fonksiyonu. TestBookModel sınıfının 3 fonksiyonu vardır:
super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran setUp() fonksiyonu, Author modelinin yeni bir örneğini oluşturur ve bunu veritabanına kaydeder. daha sonra, name='Test Book', author=self.author ve published_year=2000 ile Book modelinin yeni bir örneğini oluşturur. Bir Book modelinin string temsilinin/representation kitap adı olduğunu test eden test_book_str_representation() fonksiyonu. Book modelinin save() fonksiyonunun modeli veritabanına doğru bir şekilde kaydettiğini ve Book modelinin objects() fonksiyonunun kaydedilen modeli veritabanından alabildiğini test eden test_book_save() fonksiyonu. Ayrıca, kitabın yazarının doğru bir şekilde kaydedilip kaydedilmediğini ve yazarın adının doğru bir şekilde alınıp alınmadığını kontrol eder. Test Şemaları (Test Schema Serializers) Şimdi api/tests/ klasörü altında test_schemas.py dosyası oluşturalım ve 2 sınıfı dolduralım.
from marshmallow import ValidationError
from api.models import Author, Book
from api.schemas import AuthorSchema, BookSchema
from api.tests.base import BaseTestCase
class TestAuthorSchema(BaseTestCase):
'''
docker compose exec web sh -c "pytest -v api/tests/test_schemas.py -s --disable-warnings"
'''
def setUp(self):
super().setUp()
self.author = Author(firstname='John', lastname='Doe')
self.schema = AuthorSchema()
def test_author_schema_dump(self):
result = self.schema.dump(self.author)
result.pop("id")
self.assertEqual(result, {"lastname": "Doe", "firstname": "John"})
def test_author_schema_load(self):
data = {"lastname": "Doe", "firstname": "John"}
result = self.schema.load(data)
self.assertEqual(result, {"lastname": "Doe", "firstname": "John"})
def test_author_schema_load_missing_field(self):
data = {"lastname": "Doe"}
with self.assertRaises(ValidationError):
self.schema.load(data)
class TestBookSchema(BaseTestCase):
def setUp(self):
super().setUp()
self.author = Author(firstname='John', lastname='Doe').save()
self.book = Book(name='Test Book', author=self.author,
published_year=2000)
self.schema = BookSchema()
def test_book_schema_dump(self):
result = self.schema.dump(self.book)
result.pop("id")
author = result.pop("author")
author.pop("id")
self.assertEqual(result, {"name": "Test Book", "published_year":2000})
self.assertEqual(author, {"lastname": "Doe", "firstname": "John"})
def test_book_schema_load(self):
data = {"name": "Test Book", "author_id": str(self.author.id),
"published_year": 2000}
result = self.schema.load(data)
self.assertEqual(result, {"name": "Test Book",
"author_id": str(self.author.id),
"published_year": 2000})
def test_book_schema_load_missing_field(self):
data = {"name": "Test Book", "published_year": 2000}
with self.assertRaises(ValidationError):
self.schema.load(data)
def test_book_schema_load_invalid_year(self):
data = {"name": "Test Book", "author_id": str(self.author.id),
"published_year": 600}
with self.assertRaises(ValidationError):
self.schema.load(data)
TestAuthorSchema sınıfının 3 fonksiyonu vardır:
super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran ve firstname='John' ve lastname='Doe' ile Author modelinin yeni bir örneğinin oluşturulması ve AuthorSchema'nın yeni bir örneğinin oluşturulması. AuthorSchema'nın dump() fonksiyonunun Author örneğini bir sözlüğe doğru şekilde seri hale getirdiğini test eden test_author_schema_dump() fonksiyonu. AuthorSchema'nın load() fonksiyonunun bir sözlüğü doğru bir şekilde bir Author örneğine serialize ettiğini test eden test_author_schema_load() fonksiyonu ve load() fonksiyonunun sözlükte gerekli bir değer eksik olduğunda bir ValidationError raise ettiğini test eden test_author_schema_load_missing_field() fonksiyonu alan. TestBookSchema sınıfının 5 fonksiyonu vardır:
super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran setUp() fonksiyonu, Author modelinin yeni bir örneğini oluşturur ve bunu veritabanına kaydeder, name='Test Book', author=self.author ve published_year=2000 ile ve BookSchema'nın yeni bir örneğini oluşturur. BookSchema'nın dump() fonksiyonunun Book örneğini doğru bir şekilde bir sözlüğe serialize ettiğini test eden test_book_schema_dump() fonksiyonu. BookSchema'nın load() fonksiyonunun bir sözlüğü bir Book örneğine doğru bir şekilde deserialize ettiğini test eden test_book_schema_load() fonksiyonu. Sözlükte gerekli bir alan eksik olduğunda load() yönteminin bir ValidationError oluşturup oluşturmadığını test eden test_book_schema_load_missing_field() fonksiyonu. load() fonksiyonunun yayınlanan yıl geçersiz olduğunda bir ValidationError oluşturup oluşturmadığını test eden test_book_schema_load_invalid_year() fonksiyonu. View Fonksiyonları Test Etme Şimdi api/tests/ klasörü altında test_views.py dosyası oluşturalım ve bir sınıf ve daha fazla test fonksiyonu dolduralım.
from flask import url_for
from api.models import Author, Book
from api.tests.base import BaseTestCase
class ApiViewsTestCase(BaseTestCase):
'''
docker compose exec web sh -c "pytest -v api/tests/test_views.py -s --disable-warnings"
'''
def setUp(self):
super().setUp()
self.author = Author(firstname='John', lastname='Doe').save()
self.book = Book(name='Test Book', author=self.author, published_year=2000).save()
def test_create_author(self):
data = {"firstname": "Jane", "lastname": "Doe"}
response = self.client.post(url_for("books.create_author"), json=data)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.get_json()["firstname"], data["firstname"])
self.assertEqual(response.get_json()["lastname"], data["lastname"])
def test_create_book(self):
data = {"name": "Test Book 2", "author_id": str(self.author.id), "published_year": 2000}
response = self.client.post(url_for("books.create_book"), json=data)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.get_json()["author"]["id"], data["author_id"])
self.assertEqual(response.get_json()["name"], data["name"])
self.assertEqual(response.get_json()["published_year"], data["published_year"])
def test_create_book_invalid_author(self):
data = {"name": "Test Book 2", "author_id": "invalid_id", "published_year": 2000}
response = self.client.post(url_for("books.create_book"), json=data)
self.assertEqual(response.status_code, 400)
def test_books(self):
response = self.client.get(url_for("books.books"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.get_json()), 1)
self.assertEqual(response.get_json()[0]["name"], self.book.name)
self.assertEqual(response.get_json()[0]["published_year"], self.book.published_year)
self.assertEqual(response.get_json()[0]["author"]["firstname"], self.book.author.firstname)
self.assertEqual(response.get_json()[0]["author"]["lastname"], self.book.author.lastname)
def test_authors(self):
response = self.client.get(url_for("books.authors"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.get_json()), 1)
self.assertEqual(response.get_json()[0]["firstname"], self.author.firstname)
self.assertEqual(response.get_json()[0]["lastname"], self.author.lastname)
def test_update_author(self):
data = {"firstname": "Jane"}
response = self.client.put(url_for("books.update_author", id=self.author.id), json=data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get_json()["firstname"], data["firstname"])
def test_update_author_invalid_id(self):
data = {"firstname": "Jane"}
# 'invalid_id' is not a valid ObjectId, it must be a 12-byte input or a 24-character hex string
response = self.client.put(url_for("books.update_author", id="invalid_id"), json=data)
self.assertEqual(response.status_code, 400)
def test_delete_author(self):
response = self.client.delete(url_for("books.delete_author", id=self.author.id))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get_json(), {"message": "Deleted John Doe!"})
def test_delete_author_invalid_id(self):
# 'invalid_id' is not a valid ObjectId, it must be a 12-byte input or a 24-character hex string
response = self.client.delete('/api/v1/authors/invalid_id')
self.assertEqual(response.status_code, 400)
ApiViewsTestCase sınıfının 10 fonksiyonu vardır:
super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran setUp() fonksiyonu, Author modelinin yeni bir örneğini oluşturur, onu veritabanına kaydeder, Book modelinin yeni bir örneğini oluşturur, onu veritabanına kaydeder. create_author endpointe yapılan bir POST isteğinin doğru bir şekilde yeni bir Author modeli oluşturduğunu ve yanıtın doğru verileri içerdiğini test eden test_create_author() create_book uç noktasına yapılan bir POST isteğinin doğru bir şekilde yeni bir Book modeli oluşturduğunu ve yanıtın doğru verileri içerdiğini test eden test_create_book() Geçersiz bir author_id ile create_book uç noktasına yapılan bir POST isteğinin başarısız olduğunu test eden ve 400 durum kodu döndüren test_create_book_invalid_author() /books/ uç noktasına yapılan bir GET isteğinin veritabanındaki tüm books listesini doğru bir şekilde döndürdüğünü ve yanıtın doğru verileri içerdiğini test eden test_books() Author uç noktasına yapılan bir GET isteğinin veritabanındaki tüm authors listesini doğru bir şekilde döndürdüğünü ve yanıtın doğru verileri içerdiğini test eden test_authors() update_author uç noktasına yapılan bir PUT isteğinin belirtilen Author modelini doğru bir şekilde güncelleyip güncellemediğini ve yanıtın doğru verileri içerdiğini test eden test_update_author() Geçersiz bir id'ye sahip update_author uç noktasına yapılan bir PUT isteğinin başarısız olduğunu test eden ve bir 400 durum kodu döndüren test_update_author_invalid_id() delete_author uç noktasına yapılan bir DELETE isteğinin belirtilen Author modelini doğru bir şekilde sildiğini ve yanıtın doğru verileri içerdiğini test eden test_delete_author() Geçersiz bir id sahip delete_author uç noktasına yapılan bir DELETE isteğinin başarısız olduğunu test eden ve 400 durum kodu döndüren test_delete_author_invalid_id() Tüm Testleri Çalıştırma Aşağıdaki komutu yazarak tüm testleri çalıştırabiliriz.
pytest komutu, test paketini çalıştırır ve varsayılan olarak geçerli dizinde ve alt dizinlerinde test_.py veya *_test.py kalıbıyla eşleşen tüm dosyaları keşfeder. -s seçeneği, Pytest'e testleri çalıştırmasını ve çıktıyı konsola yazdırmasını söyler. Bu seçenek olmadan, Pytest çıktıyı yazdırmayacak ve yalnızca test çalışmasının sonucunu gösterecektir. --disable-warnings seçeneği, Pytest'e testleri çalıştırmasını ve tüm uyarıları devre dışı bırakmasını söyler. **Test Komutunun Çıktısı*
$ docker compose exec web sh -c "pytest -s --disable-warnings"
========================================================= test session starts =========================================================
platform linux -- Python 3.11.0, pytest-7.2.1, pluggy-1.0.0
rootdir: /usr/src/app
collected 20 items
api/tests/test_models.py ....
api/tests/test_schemas.py .......
api/tests/test_views.py .........
================================================== 20 passed, 112 warnings in 50.99s ==================================================