عام المغامرة مع الجرافين بيثون

مرحباً بالجميع ، أنا مطور بيثون. في العام الماضي ، عملت مع الجرافين بيثون + django ORM وخلال هذه الفترة حاولت إنشاء نوع من الأدوات لجعل العمل مع الجرافين أكثر ملاءمة. كنتيجة لذلك ، حصلت على قاعدة صغيرة graphene-framework
كود graphene-framework
ومجموعة من بعض القواعد ، والتي أود مشاركتها.

ما هو الجرافين بيثون؟
وفقًا لـ graphene-python.org ، إذن:
Graphene-Python هي مكتبة لإنشاء واجهات برمجة تطبيقات GraphQL بسهولة باستخدام Python. وتتمثل مهمتها الرئيسية في توفير واجهة برمجة التطبيقات بسيطة ولكن في الوقت نفسه الموسعة لجعل حياة المبرمجين أسهل.
وتتمثل مهمتها الرئيسية في توفير واجهة برمجة التطبيقات بسيطة ولكن في الوقت نفسه الموسعة لجعل حياة المبرمجين أسهل.
نعم ، في الواقع الجرافين بسيط وقابل للتوسعة ، لكن يبدو لي بسيطًا جدًا بالنسبة للتطبيقات الكبيرة سريعة النمو. الوثائق القصيرة (استخدمت الشفرة المصدرية بدلاً من ذلك - إنها مطوّلة أكثر من ذلك بكثير) ، بالإضافة إلى أن الافتقار إلى معايير كتابة التعليمات البرمجية يجعل هذه المكتبة ليست الخيار الأفضل لواجهة برمجة التطبيقات التالية.
أيا كان الأمر ، فقد قررت استخدامه في المشروع وواجهت عددًا من المشاكل ، ولحسن الحظ ، فقد قمت بحل معظمها (بفضل الميزات الغنية غير الموثقة للجرافين). بعض حلولي معمارية بحتة ويمكن استخدامها خارج الصندوق ، بدون إطاري. ومع ذلك ، لا يزال يتطلب الباقي منها بعض قاعدة التعليمات البرمجية.
هذه المقالة ليست وثائق ، ولكن إلى حد ما وصف موجز للمسار الذي ذهبت إليه والمشكلات التي حلتها بطريقة أو بأخرى مع تبرير موجز لاختياري. في هذا الجزء ، لاحظت الطفرات والأشياء المتعلقة بها.
الغرض من هذا المقال هو الحصول على أي تعليقات ذات مغزى ، لذلك سوف أنتظر انتقادات في التعليقات!
ملاحظة: قبل متابعة قراءة المقال ، أوصي بشدة أن تتعرف على ماهية GraphQL.
الطفرات
تركز معظم المناقشات حول GraphQL على الحصول على البيانات ، ولكن أي نظام أساسي يحترم نفسه يتطلب أيضًا طريقة لتعديل البيانات المخزنة على الخادم.
لنبدأ مع الطفرات.
النظر في التعليمات البرمجية التالية:
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
بتغيير UpdatePostMutation
باستخدام id
المحدد ، وذلك باستخدام البيانات المنقولة وإرجاع الأخطاء إذا لم تتحقق بعض الشروط.
يحتاج المرء فقط إلى إلقاء نظرة على هذا الكود ، حيث يصبح مرئيًا عدم القابلية للتوسعة وعدم إمكانية الدعم بسبب:
- هناك العديد من الوسائط للدالة
mutate
، والتي قد يزيد عددها حتى إذا أردنا إضافة المزيد من الحقول لتحريرها. - لكي تبدو الطفرات متماثلة على جانب العميل ، يجب أن تُرجع
errors
، حتى يمكن فهم حالتها وما يمكن فهمه دائمًا. - بحث واسترداد كائن في وظيفة
mutate
. تعمل وظيفة الطفرة بالصوم ، وإذا لم تكن موجودة ، فلا ينبغي أن تحدث الطفرة. - التحقق من الأذونات في طفرة. يجب ألا يحدث التغيير إذا لم يكن لدى المستخدم الحق في القيام بذلك (تحرير بعض المنشورات).
- حجة أولى عديمة الفائدة ( جذر دائمًا
None
في حقول المستوى الأعلى ، وهو طفرة لدينا). - مجموعة من الأخطاء لا يمكن التنبؤ بها: إذا لم يكن لديك شفرة المصدر أو الوثائق ، فلن تعرف الأخطاء التي يمكن أن تعود إليها هذه الطفرة ، حيث لا تنعكس في المخطط.
- هناك العديد من عمليات التحقق من الأخطاء التي يتم إجراؤها على القالب مباشرةً في طريقة
mutate
، والتي تتضمن تغيير البيانات ، بدلاً من مجموعة متنوعة من عمليات التحقق. يجب أن يتكون mutate
المثالي من سطر واحد - دعوة إلى وظيفة تحرير النشر.
باختصار ، يجب على mutate
تعديل البيانات ، بدلاً من الاهتمام بمهام الطرف الثالث مثل الوصول إلى الكائنات والتحقق من صحة الإدخال. هدفنا هو الوصول إلى شيء مثل:
def mutate(post, info, input): post = Utils.update_post(post, **input) return UpdatePostMutation(post=post)
الآن دعونا نلقي نظرة على النقاط أعلاه.
أنواع مخصصة
يتم تمرير حقل email
كسلسلة ، في حين أنه عبارة عن سلسلة بتنسيق معين . في كل مرة يتلقى فيها API رسالة بريد إلكتروني ، يجب عليه التحقق من صحتها. لذا فإن أفضل حل هو إنشاء نوع مخصص.
class Email(graphene.String):
قد يبدو هذا واضحا ، ولكن جدير بالذكر.
أنواع المدخلات
استخدام أنواع المدخلات للطفرات الخاصة بك. حتى لو لم تخضع لإعادة الاستخدام في أماكن أخرى. بفضل أنواع المدخلات ، تصبح الاستعلامات أصغر مما يجعلها أسهل في القراءة وأسرع في الكتابة.
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)
إلى:
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 } }
بعد:
mutation($id: ID!, $input: UpdatePostInput!) { updatePost(id: $id, input: $input) { ok } }
يتغير رمز الطفرة إلى:
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):
فئة طفرة القاعدة
كما هو مذكور في الفقرة 2 ، يجب أن تُرجع الطفرات errors
ok
جيدًا حتى يمكن دائمًا فهم حالتها وأسبابها . إنها بسيطة بما فيه الكفاية ، فنحن ننشئ فئة أساسية:
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 {}
بعض الملاحظات:
يكون هذا مناسبًا جدًا عند قيام العميل بتحديث بعض البيانات بعد اكتمال الطفرة ولا يريد مطالبة المؤيد بإرجاع هذه المجموعة بأكملها. كلما قل الرمز الذي تكتبه ، أصبح الحفاظ عليه أسهل. أخذت هذه الفكرة من هنا .
مع فئة الطفرة الأساسية ، يتحول الرمز إلى:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) def mutate(_, info, input, id):
طفرات الجذر
يشبه طلب التحوّل الآن ما يلي:
mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok } }
احتواء جميع الطفرات في نطاق عالمي ليس ممارسة جيدة. فيما يلي بعض الأسباب وراء:
- مع تزايد عدد الطفرات ، يصبح العثور على الطفرة التي تحتاجها أكثر فأكثر.
- بسبب مساحة اسم واحدة ، من الضروري تضمين "اسم الوحدة النمطية" في اسم الطفرة ، على سبيل المثال
update
Post
. - يجب تمرير
id
كوسيطة إلى الطفرة.
أقترح استخدام طفرات الجذر . هدفهم هو حل هذه المشكلات عن طريق فصل الطفرات إلى نطاقات منفصلة وتحرير الطفرات من منطق الوصول إلى الكائنات وحقوق الوصول إليها.
يبدو الطلب الجديد كما يلي:
mutation($id: ID!, $input: PostUpdateInput!) { post(id: $id) { update(input: $input) { ok } } }
تظل وسيطات الطلب كما هي. الآن يتم استدعاء وظيفة التغيير داخل post
، مما يسمح بتنفيذ المنطق التالي:
- إذا لم
id
تمرير id
post
، فسيتم إرجاع {}
. هذا يسمح لك بمواصلة تنفيذ الطفرات داخل. يستخدم للطفرات التي لا تتطلب عنصرًا أساسيًا (على سبيل المثال ، لإنشاء كائنات). - إذا تم تمرير
id
، فسيتم استرداد العنصر المقابل. - إذا لم يتم العثور على الكائن ،
None
إرجاع أي ، وهذا يكمل الطلب ، فلن يتم استدعاء الطفرة. - إذا تم العثور على الكائن ، فتحقق من حقوق المستخدم في معالجته.
- إذا لم يكن لدى المستخدم حقوق ،
None
إرجاع أي منها واستكمل الطلب ، فلن يتم استدعاء الطفرة. - إذا كان لدى المستخدم حقوق ، فسيتم إرجاع الكائن الموجود ويستقبله الطفرة كجذر - الوسيطة الأولى.
وبالتالي ، يتغير رمز الطفرة إلى:
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)
- جذر الطفرة - الوسيطة الأولى - أصبح الآن كائنًا من النوع
Post
، يتم تنفيذ الطفرة فوقه. - انتقل التحقق من التفويض إلى رمز طفرة الجذر.
رمز طفرة الجذر:
class PostMutationRoot(MutationRoot): class Meta: model = Post has_permission = lambda post, user: post.owner == user update = UpdatePostMutation.Field()
واجهة خطأ
لجعل مجموعة من الأخطاء يمكن التنبؤ بها ، يجب أن تنعكس في التصميم.
- نظرًا لأن الطفرات يمكنها إرجاع العديد من الأخطاء ، يجب أن تكون الأخطاء قائمة.
- نظرًا لأن الأخطاء يتم تمثيلها من خلال أنواع مختلفة ، يجب أن يوجد
Union
معين للأخطاء Union
أجل حدوث طفرة معينة. - من أجل أن تظل الأخطاء متشابهة مع بعضها البعض ، يجب أن تطبق الواجهة ، دعنا نسميها
ErrorInterface
. دعها تحتوي على حقلين: ok
message
.
وبالتالي ، يجب أن تكون الأخطاء من النوع [SomeMutationErrorsUnion]!
. يجب أن SomeMutationErrorsUnion
كافة الأنواع الفرعية من SomeMutationErrorsUnion
بتطبيق ErrorInterface
.
نحصل على:
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, ]
تبدو جيدة ، ولكن هناك الكثير من التعليمات البرمجية. نستخدم علامة التعريف لإنشاء هذه الأخطاء أثناء التنقل:
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, ]
أضف إعلان الأخطاء التي تم إرجاعها إلى الطفرة:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() errors = graphene.List(UpdatePostMutationErrors, required=True) def mutate(post, info, input):
تحقق من وجود أخطاء
يبدو لي أن طريقة mutate
يجب ألا تهتم بأي شيء آخر غير تحوير البيانات . لتحقيق ذلك ، تحتاج إلى التحقق من وجود أخطاء في التعليمات البرمجية لهذه الوظيفة.
حذف التنفيذ ، وهنا هي النتيجة:
class UpdatePostMutation(DefaultMutation): class Arguments: input = UpdatePostInput() class Meta: root_required = True authentication_required = True
قبل بدء وظيفة mutate
، يتم استدعاء كل مدقق (العنصر الثاني من أعضاء مجموعة checks
). إذا تم إرجاع True
، فسيتم العثور على الخطأ المقابل. إذا لم يتم العثور على أخطاء ، mutate
استدعاء الدالة mutate
.
ساوضح:
- تحقق الدوال من نفس الوسيطات التي
mutate
الدالات mutate
- يجب أن ترجع وظائف التحقق من الصحة
True
إذا تم العثور على خطأ. - تعتبر عمليات التحقق من التفويض ووجود عنصر الجذر عامة جدًا ويتم سردها في إشارات التعريف.
authentication_required
يضيف التحقق من إذن إذا كان True
.- يضيف
root_required
" root is not None
". - لم يعد
UpdatePostMutationErrors
مطلوبًا. يتم إنشاء اتحاد الأخطاء المحتملة على الطاير اعتمادا على فئات الخطأ من مجموعة checks
.
الأدوية
يضيف DefaultMutation
المستخدم في القسم الأخير طريقة pre_mutate
، والتي تتيح لك تغيير وسيطات الإدخال قبل التحقق من الأخطاء ، وبالتالي ، استدعاء التغيير.
هناك أيضًا مجموعة أدوات بداية عامة تجعل الشفرة أقصر وتجعل الحياة أسهل.
ملاحظة: حاليًا الكود العام مخصص لـ django ORM
CreateMutation
يتطلب أحد create_function
model
أو create_function
. بشكل افتراضي ، يبدو create_function
كالتالي:
model._default_manager.create(**data, owner=user)
قد يبدو هذا غير آمن ، لكن لا تنس أن هناك نوعًا مدمجًا في التدقيق في graphql ، بالإضافة إلى عمليات التحقق في الطفرات.
كما يوفر طريقة post_mutate
التي يتم استدعاؤها بعد create_function
(instance_created, user)
create_function
(instance_created, user)
، وسيتم إرجاع النتيجة إلى العميل.
UpdateMutation
يسمح لك بتعيين update_function
. بشكل افتراضي:
def default_update_function(instance, user=None, **data): instance.__dict__.update(data) instance.save() return instance
root_required
True
افتراضيًا.
كما يوفر طريقة post_mutate
التي يتم استدعاؤها بعد update_function
(instance_updated, user)
update_function
(instance_updated, user)
، وسيتم إرجاع النتيجة إلى العميل.
وهذا ما نحتاجه!
الرمز النهائي:
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
يسمح لك بتعيين delete_function
. بشكل افتراضي:
def default_delete_function(instance, user=None, **data): instance.delete()
استنتاج
تتناول هذه المقالة جانبًا واحدًا فقط ، رغم أنه في رأيي هو الأكثر تعقيدًا. لدي بعض الأفكار حول محللات وأنواع ، وكذلك أشياء عامة في الجرافين بيثون.
من الصعب علي أن أسمي نفسي مطورًا ذو خبرة ، لذلك سأكون سعيدًا جدًا بأي ملاحظات ، وكذلك الاقتراحات.
شفرة المصدر يمكن العثور عليها هنا .