Tahun petualangan dengan graphene-python

Tahun petualangan dengan graphene-python


gambar


Hai semuanya, saya adalah pengembang python. Tahun lalu saya bekerja dengan graphene-python + django ORM dan selama ini saya mencoba membuat beberapa jenis alat untuk membuat bekerja dengan graphene lebih nyaman. Sebagai hasilnya, saya mendapat basis kode graphene-framework kecil dan seperangkat aturan, yang ingin saya bagikan.


gambar


Apa itu graphene-python?


Menurut graphene-python.org , maka:


Graphene-Python adalah perpustakaan untuk membuat API GraphQL dengan mudah menggunakan Python. Tugas utamanya adalah untuk menyediakan API yang sederhana tetapi pada saat yang sama dapat diperpanjang untuk membuat kehidupan programmer lebih mudah.

Tugas utamanya adalah untuk menyediakan API yang sederhana tetapi pada saat yang sama dapat diperpanjang untuk membuat kehidupan programmer lebih mudah.


Ya, pada kenyataannya graphene sederhana dan dapat dikembangkan, tetapi bagi saya sepertinya terlalu sederhana untuk aplikasi besar dan cepat berkembang. Dokumentasi pendek (saya menggunakan kode sumber sebagai gantinya - itu jauh lebih verbose), serta kurangnya standar untuk menulis kode membuat perpustakaan ini bukan pilihan terbaik untuk API Anda berikutnya.


Bagaimanapun, saya memutuskan untuk menggunakannya dalam proyek dan mengalami sejumlah masalah, untungnya, setelah memecahkan sebagian besar dari mereka (berkat fitur graphene yang tidak terdokumentasi). Beberapa solusi saya murni arsitektur dan dapat digunakan di luar kotak, tanpa kerangka kerja saya. Namun, sisanya masih memerlukan beberapa basis kode.


Artikel ini bukan dokumentasi, tetapi dalam arti deskripsi singkat tentang jalan yang saya tempuh dan masalah yang saya selesaikan dengan satu atau lain cara dengan alasan singkat untuk pilihan saya. Pada bagian ini, saya memperhatikan mutasi dan hal-hal yang berkaitan dengannya.


Tujuan artikel ini adalah untuk mendapatkan umpan balik yang berarti , jadi saya akan menunggu kritik di komentar!


Catatan: sebelum melanjutkan membaca artikel, saya sangat menyarankan Anda membiasakan diri dengan apa itu GraphQL.




Mutasi


Sebagian besar diskusi tentang GraphQL fokus pada mendapatkan data, tetapi platform apa pun yang menghargai diri sendiri juga membutuhkan cara untuk memodifikasi data yang disimpan di server.

Mari kita mulai dengan mutasi.


Pertimbangkan kode berikut:


 class UpdatePostMutation(graphene.Mutation): class Arguments: post_id = graphene.ID(required=True) title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, post_id, title, content, image_urls, allow_comments, contact_email): errors = [] try: post = get_post_by_id(post_id) except PostNotFound: return UpdatePostMutation(ok=False, errors=['post_not_found']) if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if not is_email(contact_email): errors.append('contact_email_not_valid') if post.owner != info.context.user: errors.append('not_post_owner') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, title, content, image_urls, allow_comments, contact_email) return UpdatePostMutation(ok=bool(errors), errors=errors) 

UpdatePostMutation mengubah pos dengan id diberikan, menggunakan data yang ditransfer dan mengembalikan kesalahan jika beberapa kondisi tidak terpenuhi.


Satu hanya perlu melihat kode ini, karena menjadi non-ekstensibilitas dan tidak didukung karena:


  1. Terlalu banyak argumen dari fungsi yang mutate , yang jumlahnya mungkin bertambah bahkan jika kita ingin menambahkan lebih banyak bidang yang akan diedit.
  2. Agar mutasi terlihat sama di sisi klien, mereka harus mengembalikan errors dan ok , sehingga status mereka dan apa yang selalu mungkin untuk dipahami.
  3. Cari dan ambil objek dalam fungsi mutate . Fungsi mutasi beroperasi dengan puasa, dan jika tidak ada, maka mutasi tidak boleh terjadi.
  4. Memeriksa izin dalam mutasi. Mutasi tidak boleh terjadi jika pengguna tidak memiliki hak untuk melakukan ini (edit beberapa posting).
  5. Argumen pertama yang tidak berguna ( root yang selalu None untuk bidang tingkat atas, yang merupakan mutasi kami).
  6. Serangkaian kesalahan yang tidak dapat diprediksi: jika Anda tidak memiliki kode sumber atau dokumentasi, maka Anda tidak akan tahu kesalahan apa yang dapat dikembalikan mutasi ini, karena mereka tidak tercermin dalam skema.
  7. Ada terlalu banyak pengecekan kesalahan templat yang dilakukan secara langsung dalam metode mutate , yang melibatkan perubahan data, dan bukannya berbagai pengecekan. mutate ideal harus terdiri dari satu baris - panggilan ke fungsi pengeditan posting.

Singkatnya, mutate harus memodifikasi data , daripada menangani tugas pihak ketiga seperti mengakses objek dan memvalidasi input. Tujuan kami adalah mencapai sesuatu seperti:


  def mutate(post, info, input): post = Utils.update_post(post, **input) return UpdatePostMutation(post=post) 

Sekarang mari kita lihat poin-poin di atas.




Jenis khusus


Bidang email dilewatkan sebagai string, sementara itu adalah string format tertentu . Setiap kali API menerima email, itu harus memeriksa kebenarannya. Jadi solusi terbaik adalah membuat tipe kustom.


 class Email(graphene.String): # ... 

Ini mungkin tampak jelas, tetapi perlu disebutkan.




Jenis input


Gunakan tipe input untuk mutasi Anda. Bahkan jika mereka tidak dapat digunakan kembali di tempat lain. Berkat tipe input, kueri menjadi lebih kecil, yang membuatnya lebih mudah dibaca dan lebih cepat untuk menulis.


 class UpdatePostInput(graphene.InputObjectType): title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True) 

Kepada:


 mutation( $post_id: ID!, $title: String!, $content: String!, $image_urls: String!, $allow_comments: Boolean!, $contact_email: Email! ) { updatePost( post_id: $post_id, title: $title, content: $content, image_urls: $image_urls, allow_comments: $allow_comments, contact_email: $contact_email, ) { ok } } 

Setelah:


 mutation($id: ID!, $input: UpdatePostInput!) { updatePost(id: $id, input: $input) { ok } } 

Kode mutasi berubah menjadi:


 class UpdatePostMutation(graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, input, id): # ... if not errors: post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation(errors=errors) 



Kelas mutasi dasar


Seperti disebutkan dalam paragraf 2, mutasi harus mengembalikan errors dan ok sehingga status mereka dan apa penyebabnya selalu dapat dipahami . Cukup sederhana, kami membuat kelas dasar:


 class MutationPayload(graphene.ObjectType): ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) query = graphene.Field('main.schema.Query', required=True) def resolve_ok(self, info): return len(self.errors or []) == 0 def resolve_errors(self, info): return self.errors or [] def resolve_query(self, info): return {} 

Beberapa catatan:


  • Metode resolve_ok , jadi kami tidak harus menghitung sendiri.
  • Bidang query adalah Query root, yang memungkinkan Anda untuk meminta data langsung di dalam permintaan mutasi (data akan diminta setelah mutasi selesai).
     mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok query { profile { totalPosts } } } } 

Ini sangat nyaman ketika klien memperbarui beberapa data setelah mutasi selesai dan tidak ingin meminta pendukung untuk mengembalikan seluruh rangkaian ini. Semakin sedikit kode yang Anda tulis, semakin mudah untuk mempertahankannya. Saya mengambil ide ini dari sini .


Dengan kelas mutasi dasar, kode berubah menjadi:


 class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) def mutate(_, info, input, id): # ... 



Mutasi akar


Permintaan mutasi kami sekarang terlihat seperti ini:


 mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok } } 

Mengandung semua mutasi dalam lingkup global bukanlah praktik yang baik. Berikut beberapa alasannya:


  1. Dengan semakin banyaknya mutasi, semakin sulit menemukan mutasi yang Anda butuhkan.
  2. Karena satu namespace, perlu untuk memasukkan "nama modulnya" dalam nama mutasi, misalnya update Post .
  3. Anda harus memberikan id sebagai argumen untuk mutasi.

Saya sarankan menggunakan mutasi root . Tujuan mereka adalah untuk memecahkan masalah-masalah ini dengan memisahkan mutasi ke dalam lingkup yang terpisah dan membebaskan mutasi dari logika akses ke objek dan hak akses kepada mereka.


Permintaan baru terlihat seperti ini:


 mutation($id: ID!, $input: PostUpdateInput!) { post(id: $id) { update(input: $input) { ok } } } 

Argumen permintaan tetap sama. Sekarang fungsi perubahan "dipanggil" di dalam post , yang memungkinkan logika berikut untuk diimplementasikan:


  1. Jika id tidak diteruskan ke post , maka ia mengembalikan {} . Ini memungkinkan Anda untuk terus melakukan mutasi di dalam. Digunakan untuk mutasi yang tidak memerlukan elemen root (misalnya, untuk membuat objek).
  2. Jika id dilewatkan, elemen yang sesuai diambil.
  3. Jika objek tidak ditemukan, None dikembalikan, dan ini melengkapi permintaan, mutasi tidak dipanggil.
  4. Jika objek ditemukan, maka periksa hak pengguna untuk memanipulasinya.
  5. Jika pengguna tidak memiliki hak, None dikembalikan dan permintaan selesai, mutasi tidak dipanggil.
  6. Jika pengguna memiliki hak, maka objek yang ditemukan dikembalikan dan mutasi menerimanya sebagai root - argumen pertama.

Dengan demikian, kode mutasi berubah menjadi:


 class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() def mutate(post, info, input): if post is None: return None errors = [] if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation(errors=errors) 

  • Akar dari mutasi - argumen pertama - sekarang menjadi objek tipe Post , di mana mutasi dilakukan.
  • Pemeriksaan otorisasi dipindahkan ke root mutation code.

Kode Mutasi Root:


 class PostMutationRoot(MutationRoot): class Meta: model = Post has_permission = lambda post, user: post.owner == user update = UpdatePostMutation.Field() 



Antarmuka galat


Untuk membuat satu set kesalahan dapat diprediksi, mereka harus tercermin dalam desain.


  • Karena mutasi dapat mengembalikan beberapa kesalahan, kesalahan harus menjadi daftar.
  • Karena kesalahan diwakili oleh tipe yang berbeda, Union kesalahan tertentu harus ada untuk mutasi tertentu.
  • Agar kesalahan tetap serupa satu sama lain, mereka harus mengimplementasikan antarmuka, sebut saja itu ErrorInterface . Biarkan berisi dua bidang: ok dan message .

Jadi, kesalahan harus bertipe [SomeMutationErrorsUnion]! . Semua subtipe SomeMutationErrorsUnion harus menerapkan ErrorInterface .


Kami mendapatkan:


 class NotAuthenticated(graphene.ObjectType): message = graphene.String(required=True, default_value='not_authenticated') class Meta: interfaces = [ErrorInterface, ] class TitleTooShort(graphene.ObjectType): message = graphene.String(required=True, default_value='title_too_short') class Meta: interfaces = [ErrorInterface, ] class TitleAlreadyTaken(graphene.ObjectType): message = graphene.String(required=True, default_value='title_already_taken') class Meta: interfaces = [ErrorInterface, ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [NotAuthenticated, TitleIsTooShort, TitleAlreadyTaken, ] 

Kelihatannya bagus, tetapi ada terlalu banyak kode. Kami menggunakan metaclass untuk menghasilkan kesalahan ini dengan cepat:


 class PostErrors(metaclass=ErrorMetaclass): errors = [ 'not_authenticated', 'title_too_short', 'title_already_taken', ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [PostErrors.not_authenticated, PostErrors.title_too_short, PostErrors.title_already_taken, ] 

Tambahkan deklarasi kesalahan yang dikembalikan ke mutasi:


 class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() errors = graphene.List(UpdatePostMutationErrors, required=True) def mutate(post, info, input): # ... 



Periksa kesalahan


Sepertinya saya bahwa metode mutate tidak boleh peduli tentang apa pun selain bermutasi data . Untuk mencapai ini, Anda perlu memeriksa kesalahan dalam kode mereka untuk fungsi ini.


Menghilangkan implementasi, berikut hasilnya:


 class UpdatePostMutation(DefaultMutation): class Arguments: input = UpdatePostInput() class Meta: root_required = True authentication_required = True #   ,    True   # An iterable of tuples (error_class, checker) checks = [ ( PostErrors.title_too_short, lambda post, input: len(input.title) < TITLE_MIN_LENGTH ), ( PostErrors.title_already_taken, lambda post, input: Post.objects.filter(title=input.title).exists() ), ] def mutate(post, info, input): post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation() 

Sebelum memulai fungsi mutate , setiap pemeriksa (elemen kedua dari anggota array checks ) dipanggil. Jika True dikembalikan, kesalahan yang sesuai ditemukan. Jika tidak ada kesalahan ditemukan, fungsi mutate dipanggil.


Saya akan menjelaskan:


  • Periksa fungsi, ambil argumen yang sama dengan fungsi yang mutate .
  • Fungsi validasi harus mengembalikan True jika ditemukan kesalahan.
  • Pemeriksaan otorisasi dan keberadaan elemen root cukup umum dan terdaftar di bendera Meta .
  • authentication_required menambahkan pemeriksaan otorisasi jika True .
  • root_required menambahkan root_required " root is not None ".
  • UpdatePostMutationErrors tidak lagi diperlukan. Gabungan kemungkinan kesalahan dibuat dengan cepat tergantung pada kelas kesalahan array checks .



Generik


DefaultMutation digunakan di bagian terakhir menambahkan metode pre_mutate , yang memungkinkan Anda untuk mengubah argumen input sebelum memeriksa kesalahan, dan, dengan demikian, memanggil mutasi.


Ada juga kit starter generik yang membuat kode lebih pendek dan membuat hidup lebih mudah.
Catatan: saat ini kode generik khusus untuk django ORM


Penciptaan


Membutuhkan salah satu create_function model atau create_function . Secara default, create_function terlihat seperti ini:


 model._default_manager.create(**data, owner=user) 

Ini mungkin terlihat tidak aman, tetapi jangan lupa bahwa ada tipe bawaan memeriksa graphql, serta memeriksa mutasi.


Ini juga menyediakan metode post_mutate yang dipanggil setelah create_function dengan argumen (instance_created, user) , yang hasilnya akan dikembalikan ke klien.


Pembaruan pembaruan


Memungkinkan Anda untuk mengatur update_function . Secara default:


 def default_update_function(instance, user=None, **data): instance.__dict__.update(data) instance.save() return instance 

root_required True secara default.


Ini juga menyediakan metode post_mutate , yang disebut after update_function dengan argumen (instance_updated, user) , yang hasilnya akan dikembalikan ke klien.


Dan inilah yang kami butuhkan!


Kode terakhir:


 class UpdatePostMutation(UpdateMutation): class Arguments: input = UpdatePostInput() class Meta: checks = [ ( PostErrors.title_too_short, lambda post, input: len(input.title) < TITLE_MIN_LENGTH ), ( PostErrors.title_already_taken, lambda post, input: Post.objects.filter(title=input.title).exists() ), ] 

DeleteMutation


Memungkinkan Anda menyetel delete_function . Secara default:


 def default_delete_function(instance, user=None, **data): instance.delete() 



Kesimpulan


Artikel ini hanya mempertimbangkan satu aspek, meskipun menurut saya itu yang paling rumit. Saya memiliki beberapa pemikiran tentang resolvers dan jenis, serta hal-hal umum dalam graphene-python.


Sulit bagi saya untuk menyebut diri saya seorang pengembang yang berpengalaman, jadi saya akan sangat senang atas umpan balik, serta saran.


Kode sumber dapat ditemukan di sini .

Source: https://habr.com/ru/post/id461939/


All Articles