كراكس! اخترع جيل الألفية إطار بايثون

مقدمة



مرحبا هبر! هذه المقالة مخصصة لتحليل إيجابيات وسلبيات إطار عمل Python التالي ، والذي تم إصداره منذ حوالي أسبوع.



لذا ، القليل من الاستطراد الغنائي. خلال الأحداث المعروفة ، عندما كنا منعزلين عن أنفسنا قليلاً ، كان لدينا المزيد من وقت الفراغ. وصل شخص ما إلى قائمة الأدب المخصص للقراءة ، وبدأ شخص ما في دراسة لغة أجنبية أخرى ، واستمر شخص ما في الضغط على Dotan ولم ينتبه إلى التغييرات. لكنني (آسف ، هذا المقال سوف يحتوي على الكثير من "أنا" ، وأنا أشعر بالخجل قليلاً) قررت وحاولت القيام بشيء مفيد. ومع ذلك ، فإن الفائدة قابلة للنقاش. الأسئلة الواضحة التي سيطرحها القارئ على الأرجح في المقام الأول:"أم ، إطار بايثون؟ آخر؟ اعذروني ولكن لماذا؟ نحن لسنا جافا سكريبت ، بعد كل شيء! "



في الواقع ، هذا بالضبط ما سيتم مناقشته في هذا المقال: هل هو ضروري؟ إذا لزم الأمر ، لمن؟ ما هو الفرق مما هو موجود بالفعل؟ كيف يمكن أن تكون جذابة ولماذا ، على سبيل المثال ، يمكن دفنها دون انتظار عيد الميلاد الأول. لا تخطط المقالة للكثير من التعليمات البرمجية - يمكن العثور على أمثلة لكتابة تطبيق واستخدام الأجزاء الفردية في الوثائق (يوجد الكثير من التعليمات البرمجية هناك ؛)). هذه المقالة هي أكثر من نظرة عامة.



من يحتاجها؟



إجابة أنانية إلى حد ما على هذا السؤال - أولاً وقبل كل شيء ، بالطبع ، أنا. لدي بعض الخبرة في إنشاء تطبيقات الويب باستخدام الأطر الحالية وأجد نفسي أفكر بانتظام: "نعم ، كل شيء رائع ، ولكن فقط إذا كان هكذا …. وهنا إعلان ... "... يجد معظمنا ، بطريقة أو بأخرى ، عاجلاً أم آجلاً حقيقة أن بعض الأشياء لا تحبها وتريد (أو حتى تضطر) إلى تغييرها. حاولت تجميع ما أحبه من الأدوات التي استخدمتها. آمل ألا أكون وحدي في تفضيلاتي ، وأن يكون هناك أشخاص سيجدون هذه الأفكار قريبة. الفكرة الرئيسية وراء Crax هي أنها لا تفرض أي نمط معين من التطوير قدر الإمكان. على سبيل المثال ، لسنا بحاجة إلى مساحات أسماء ، ولا نريد تقسيم المنطق إلى تطبيقات ، بل نريد نشر مسارين بسرعة وتوجيه الطلبات والاستجابات. حسنًا ، في هذه الحالة يمكننا فقط إنشاء تطبيق ملف واحد والحصول على ما نريد. لكن الوضع المعاكس ممكن أيضًا ، ولن يكون هذا مشكلة أيضًا. الشيء الثاني الذي يدافع عنه كراكس هو البساطة. الحد الأدنى من التعليمات البرمجية والحد الأدنى من قراءة الوثائق للبدء.إذا كان الشخص الذي بدأ لتوه في تعلم Python يخطط للعمل مع إطار العمل ، فيجب أن يكون قادرًا على التغلب دون ألم على عتبة الدخول.



إذا نظرت إلى عدد سطور التعليمات البرمجية المطلوبة لاجتياز جميع اختبارات

TechEmpower (المزيد عن ذلك أدناه) ، فإن Crax في تطبيق يتكون من ملف واحد يكون أكثر إحكاما من جميع المشاركين الآخرين ، ولم يكن هناك أي غرض "لضغط" هذا الملف. لا يوجد شيء آخر للكتابة. بتلخيص ما سبق ، يمكننا القول أن Crax مناسبة لمجموعة مختلفة جدًا من المهام ومجموعة واسعة جدًا من المبرمجين بدرجات متفاوتة من التدريب.



لماذا لا تستخدم الأدوات الموجودة؟



لما لا؟ إذا كنت تعرف بالضبط الأداة التي يجب استخدامها ، والأكثر ملاءمة لمهمتك الحالية ، علاوة على ذلك ، فقد عملت باستخدام هذه الأداة وتعرف كل الفروق الدقيقة. بالطبع ، ستختار ما تعرفه وما يناسبك. لا يوجد (ولن يكون) هدف لوضع Crax باعتباره "٪ framework_name٪ Killer". لن يكون هناك أي نوع الإثارة: "رمي على وجه السرعة٪ framework_name٪، إعادة كتابة كل شيء على Crax والملاحظ على الفور زيادة عضو في كمية المبيعات" . لا شيء من هذا القبيل. يمكنك فقط أن تلاحظ لنفسك أنه كان لديك واحد آخر في صندوق الأدوات الخاص بك قبل أسبوع. لاستخدامه أم لا ، الأمر متروك لك. ومع ذلك ، لماذا يستحق المحاولة.



أولاً ، إنه سريع بما فيه الكفاية. تمت كتابته باستخدام واجهة ASGI (اقرأ المواصفات هنا) وهو أسرع بكثير من Flask أو Django 1. * ، 2. *. لكن Crax بالطبع ليس إطار عمل Python الوحيد الذي يستخدم ASGI ، وتظهر الاختبارات الأولية أنه يتنافس جيدًا مع الأطر الأخرى التي تستخدم هذه التقنية. للمقارنة ، استخدمنا اختبارات تقييم الأداء TechEmpower . لسوء الحظ ، فإن Crax ، مثل الأطر الأخرى التي تمت إضافتها في منتصف الجولة الحالية ، ستدخل فقط في المرحلة التالية ، وبعد ذلك يمكنك رؤية النتائج في المشكلة الرسومية. ومع ذلك ، بعد كل طلب سحب ، يقوم Travis بإجراء اختبارات ويمكنك رؤية الخصائص المقارنة لأطر العمل في سجل Travis. يوجد أسفل الرابط قطعة قماش طويلة من سجل Travis لأطر Python بأسماء بالترتيب الأبجدي من A إلى F هنا... يمكنك محاولة قراءة السجل ومقارنة Crax ، على سبيل المثال ، مع apidaora ، سيظهر جيدًا. يوجد أدناه على الرسم البياني الوضع الحالي في اختبارات الجولة الـ19.







بالطبع سنكون قادرين على رؤية النتائج الحقيقية والنتائج الحقيقية فقط في الجولة القادمة ، ولكن مع ذلك.



ومع ذلك ، كما ذكرنا أعلاه ، ليس لدينا أدوات أقل سرعة ومثبتة بالفعل.

نفس المتزامن ، مع دعم أصلي لمقابس الويب وغيرها من أفراح.



لنفترض Starlette أو FastApi. إنها أطر عمل مذهلة للغاية مع مجتمع كبير مهتم بتطوير هذه المنتجات. من الجدير بالذكر أن Crax تشبه إلى حد كبير Starlette أو FastAPI في أيديولوجيتها ، وقد سُرقت بعض الأفكارتجسست على Starlette (على سبيل المثال Response Middleware). ومع ذلك ، هناك عدد من الأشياء التي قد تعجبك في Crax وتجعلك تفكر: "ربما جربها للمشروع التالي."... على سبيل المثال ملف التكوين. بالطبع ، لدى Starlette أيضًا القدرة على إنشاء ملف تكوين ، ولكنه معقد إلى حد ما بالنسبة للمبتدئين ، وفي النهاية يتلخص جوهره في حقيقة أن جميع متغيرات التكوين يتم تمريرها في النهاية إلى مُهيئ فئة التطبيق على أي حال. إذا قمت بجمع كل المتغيرات الممكنة ، على سبيل المثال ، إعداد المسجل ، والبرمجيات الوسيطة ، و CORS ، وما إلى ذلك ، فسوف يتضح أن الأمر أكثر من اللازم. في Crax ، يتم التصريح عن جميع المتغيرات في ملف (config) الرئيسي (مثل Django) ، ولا تحتاج إلى تمريرها في أي مكان. علاوة على ذلك ، يمكن دائمًا الوصول إلى جميع المتغيرات المعلنة في ملف التكوين في وقت التشغيل (سواء من التطبيق قيد التشغيل أو من الخارج ، مرحبًا Django ).



from crax.utils import get_settings_variable
base_url = get_settings_variable('BASE_URL')


ومع ذلك ، قد تبدو ميزة مشكوك فيها ، عندما يبدأ ملف التكوين في النمو متضخمًا مع المتغيرات والإعدادات ، ونود الوصول إليها ، يصبح هذا مهمًا.



التفاصيل المهمة التالية التي أود التحدث عنها هي تنظيم هيكل التطبيق. عندما يكون لديك مشروع صغير ، يمكن وضع كل منطقه في ملف واحد ، فهذا شيء واحد. ولكن عندما تكتب شيئًا أكثر شمولية ، فقد ترغب في فصل وجهات النظر والنماذج وأوصاف المسارات وما إلى ذلك ، وفقًا لمنطقهم. في هذا السياق ، تتبادر إلى الذهن مخططات Flask الرائعة أو تطبيقات Django. يتحدث Crax عن مساحات الأسماء بهذا المعنى. في البداية ، يهدف طلبك إلى أن يكون



مجموعة من حزم python المضمنة في ملف المشروع الرئيسي. بالمناسبة ، يمكن تداخل مساحات الأسماء (الأجزاء الخاصة بك من التطبيق) بشكل متكرر (hello Flask) ، ولا تهم أسماء الملفات الموجودة فيها. لماذا تفعل ذلك؟ وماذا تعطينا؟



أولا ، التوجيه. ستنشئ مساحات الأسماء uri بناءً على موقع مساحة الاسم تلقائيًا (ولكن يمكن التحكم في هذا بالطبع). على سبيل المثال:



from crax.urls import Route, Url, include

url_list = [
    Route(Url('/'), Home),
    Route(Url('/guest_book'), guest_view_coroutine),
    include('second_app.urls'),
    include('second_app.nested.urls'),
    include('third_app.urls')
]


استبدل النقاط بشرطة مائلة وستحصل على uri في مساحة الاسم (بالطبع ، عن طريق إضافة معالج نهائي). نظرًا لأننا ذكرنا بالفعل التوجيه ، فسوف نتناوله بمزيد من التفاصيل.

تقدم Crax عدة إمكانيات مثيرة للاهتمام ، إلى جانب العمل المعتاد بالتعبيرات العادية أو العمل عبر مسار Django.



# URL defined as regex with one floating (optional) parameter
Url(r"/cabinet/(?P<username>\w{0,30})/(?:(?P<optional>\w+))?", type="re_path")
# General way to define URL
Url("/v1/customer/<customer_id>/<discount_name>/")


ومع ذلك ، من الممكن ربط عدة عناوين Urls بمعالج واحد.



from crax.urls import Route, Url

class APIView(TemplateView):
    template = "index.html"

urls = [
    Route(
        urls=(
            Url("/"),
            Url("/v1/customers"),
            Url("/v1/discounts"),
            Url("/v1/cart"),
            Url("/v1/customer/<customer_id:int>"),
            Url("/v1/discount/<discount_id:int>/<optional:str>/"),
        ),
        handler=APIView)
    ]


يمكنك أن تفكر بنفسك في المكان الذي يمكن أن يكون مفيدًا لك فيه. وأيضًا ، هناك طريقة تشغيل لوحدة الحل في وضع "التنكر". على سبيل المثال ، تريد فقط توزيع نوع من الدليل باستخدام القوالب ، ولا تريد أي شيء آخر. ربما هذا هو توثيق أبو الهول ، أو شيء مشابه. يمكنك دائمًا القيام بذلك:



import os
from crax.urls import Url, Route

class Docs(TemplateView):
    template = 'index.html'
    scope = os.listdir('docs/templates')

URL_PATTERNS = [
    Route(urls=(
        Url('/documentation', masquerade=True),
        handler=Docs),
]


رائع ، سيتم الآن عرض جميع القوالب الموجودة في دليل المستندات / القوالب بنجاح باستخدام معالج واحد. سيقول القارئ الفضولي أنه ليس هناك حاجة إلى استخدام بايثون هنا على الإطلاق ، وكل هذا لا يمكن القيام به إلا بمساعدة Nginx الشرطي. أوافق تمامًا ، تمامًا إلى أن يصبح من الضروري ، على سبيل المثال ، توزيع هذه القوالب حسب الدور أو في مكان ما على الجانب ، لا يلزم وجود منطق إضافي.



ومع ذلك ، نعود إلى مساحات أسماء الكباش لدينا . سيكون من المحزن جدًا أن تكون مساحات الأسماء (وإن كانت متداخلة) مطلوبة فقط لتنظيم تحليل عنوان url. بالطبع ، الغرض من مساحات الأسماء أوسع قليلاً. على سبيل المثال ، العمل مع نماذج قاعدة البيانات وعمليات الترحيل.



لا يوجد ORM في Crax. وليس من المفترض. على أي حال ، حتى تقدم SQLAlchemy حلولاً غير متزامنة. ومع ذلك ، تم الإعلان عن العمل مع قواعد البيانات (Postgres و MySQL و SQLite). هذا يعني أنه من الممكن كتابة النماذج الخاصة بك بناءً على Crax BaseTable . تحت الغطاء ، يعد هذا غلافًا رقيقًا للغاية فوق SQLAlchemy Core Table ، ويمكنه القيام بكل ما يمكن أن يفعله Core Table . لما قد تكون هناك حاجة إليه. ربما للقيام بشيء مماثل.



from crax.database.model import BaseTable
import sqlalchemy as sa

class BaseModelOne(BaseTable):
    # This model just passes it's fields to the child
    # Will not be created in database because the abstract is defined
    parent_one = sa.Column(sa.String(length=50), nullable=False)

    class Meta:
        abstract = True

class BaseModelTwo(BaseTable):
    # Also passes it's fields to the child
    # Will be created in database
    parent_two = sa.Column(sa.String(length=50), nullable=False)

class MyModel(BaseModelOne, BaseModelTwo):
    name = sa.Column(sa.String(length=50), nullable=False)

print([y.name for x in MyModel.metadata.sorted_tables for y in x._columns])
# Let's check our fields ['name', 'id', 'parent_one', 'parent_two']


ولكي نكون قادرين على العمل مع الهجرات. تعتبر عمليات ترحيل Crax جزءًا صغيرًا من التعليمات البرمجية أعلى SQLAlchemy Alembic. نظرًا لأننا نتحدث عن مساحات الأسماء وفصل المنطق ، إذن ، من

الواضح أننا نود تخزين التهجيرات في نفس الحزمة مثل المنطق الآخر لمساحة الاسم هذه. هذه هي الطريقة التي تعمل بها هجرات الكراكس. سيتم توزيع جميع عمليات الترحيل وفقًا لمساحة الاسم الخاصة بهم ، وإذا كانت مساحة الاسم هذه تعني العمل مع قواعد بيانات مختلفة ، فسيكون هناك تقسيم داخل دليل الترحيل إلى أدلة قواعد البيانات المقابلة. الأمر نفسه ينطبق على عمليات الترحيل دون اتصال - سيتم تقسيم جميع ملفات * .sql وفقًا لمساحة الاسم وقاعدة البيانات النموذجية. لن أرسم هنا حول كتابة الاستفسارات - إنه موجود في الوثائق ، سأقول فقط أنك لا تزال تعمل مع SQLAlchemy Core.



مرة أخرى ، تتضمن مساحات الأسماء تخزينًا مناسبًا للقوالب (يتم دعم الميراث وميزات Jinja2 الأخرى + بعض وسائل الراحة في شكل رموز CSRF جاهزة أو إنشاء عنوان url). وهذا يعني أن جميع القوالب الخاصة بك منظمة. حسنًا ، بالطبع ، لست عالقًا في عام 2007 المجيد ، فأنا أفهم أن القوالب (حتى لو تم تقديمها بشكل غير متزامن) ستكون مطلوبة قليلاً في عام 2020. وهذا ، على الأرجح ، يسعدك فصل منطق الواجهة الأمامية والخلفية. يقوم Crax بعمل ممتاز في هذا الأمر ، ويمكن عرض النتائج على Github.

هنايتم استخدام VueJs كواجهة أمامية. ونظرًا لأن لدينا نوعًا من واجهة برمجة التطبيقات ، فربما نرغب في عمل وثائق تفاعلية. يمكن لـ Crax إنشاء وثائق OpenAPI (Swagger) خارج الصندوق استنادًا إلى قوائم المسار الخاصة بك وسلاسل مستندات المعالج. كل الأمثلة ، بالطبع ، موجودة في الوثائق.



قبل أن ننتقل إلى الجزء الأكثر إثارة للاهتمام من عرضنا الموجز ، يجدر التحدث قليلاً عن البطاريات المفيدة التي يتم توفيرها بالفعل مع Crax.



وبطبيعة الحال ، يكون وضع التصحيح عندما يمكن قراءة الخطأ والتتبع الكامل مباشرة في المتصفح ، على الصفحة التي حدثت فيها المصيبة. يمكن إيقاف تشغيل وضع التصحيح وتخصيصه بخلفيات مملةمع معالجيهم. اطبع طريقة عرض فريدة لكل رمز حالة http. يتم ذلك بكل بساطة ، مثل كل شيء في Crax.



المسجل المدمج مع القدرة على الكتابة في نفس الوقت إلى الملف المحدد وإرسال السجلات إلى وحدة التحكم (أو القيام بشيء واحد). القدرة على تعيين المسجل الخاص بك بدلا من الافتراضي. دعم الحراسة بإضافة سطرين إلى التكوين (وإذا لزم الأمر ، التخصيص).



نوعان من البرامج الوسيطة المثبتة مسبقًا. تتم معالجة الأول قبل معالجة الطلب من قبل التطبيق ، والثاني بعد.



دعم مدمج لرؤوس CORS. ما عليك سوى الإعلان عن قواعد CORS في ملف التكوين.

القدرة على تحديد الطرق المتاحة لكل معالج مباشرة في الموقع. سيعمل كل معالج بقائمة طرق HTTP المحددة (+ HEAD و OPTIONS) ، أو فقط مع GET و HEAD و OPTIONS.



القدرة على تحديد أن هذا المعالج متاح فقط للمستخدمين المصرح لهم ، أو فقط للمستخدمين من مجموعة المسؤولين ، أو فقط لأعضاء دور المستخدم المتميز.

هناك ترخيص لجلسات HMAC الموقعة ، والتي لا تحتاج إلى الدخول إلى قاعدة البيانات وعدد من الأدوات لإنشاء وإدارة المستخدمين. يمكنك تمكين دعم الواجهة الخلفية للترخيص والحصول على مستخدم محدد مسبقًا وعدد من الأدوات للعمل معها. ومع ذلك ، مثل معظم أدوات Crax ، يمكنك تركها واستخدامها وكتابة ما تريد. لا يمكنك استخدام التخويل وقواعد البيانات والنماذج والترحيلات وطرق العرض وكتابة الحلول المخصصة الخاصة بك بالكامل. لا تحتاج إلى بذل أي جهد للقيام بذلك ، فأنت لم تقم بتمكينه - إنه ليس كذلك.



هناك عدة أنواع من الاستجابة وأنواع عديدة من المعالجات المستندة إلى الفئة التي ستساعدك على كتابة التطبيقات بشكل أسرع وأكثر إيجازًا. في هذه الحالة ، سوف تعمل أيضًا ، والتي لا ترث من تلك المدمجة.



from crax.views import BaseView

# Written your own stuff
class CustomView:
    methods = ['GET', 'POST']
    def __init__(self, request):
        self.request = request
    async def __call__(self, scope, receive, send):
        if self.request.method == 'GET':
            response = TextResponse(self.request, "Hello world")
            await response(scope, receive, send)
        elif self.request.method == 'POST':
            response = JSONResponse(self.request, {"Hello": "world"})
            await response(scope, receive, send)

# Crax based stuff
class CustomView(BaseView):
    methods = ['GET', 'POST']
    async def get(self):
        response = TextResponse(self.request, "Hello world")
        return response

    async def post(self):
        response = JSONResponse(self.request, {"Hello": "world"})
        return response

class CustomersList(TemplateView):
    template = 'second.html'

    # No need return anything in case if it is TemplateView.
    # Template will be rendered with params
    async def get(self):
        self.context['params'] = self.request.params


دعم حماية CSRF. إنشاء الرموز ، والتحقق من وجود رمز مميز في نص الطلب ،

وتعطيل التحقق لمعالجات محددة.



دعم حماية ClickJacking (الإطار ، iframe ، التضمين ... سياسات التقديم)



دعم للتحقق من الحجم الأقصى المسموح به لجسم الطلب قبل أن يبدأ التطبيق في معالجته.



دعم websocket الأصلي. لنأخذ مثالاً من الوثائق ونكتب تطبيقًا بسيطًا يمكنه إرسال رسائل websocket بالبث أو لكل مجموعة مستخدمين أو رسائل إلى مستخدم معين. لنفترض أن لدينا مجموعات "بنين" و "بنات" (من الممكن إضافة مجموعة "آباء"). يمكننا كتابة شيء مشابه لمثال (بالطبع ، هذا ليس رمز منتج).



#app.py

import asyncio
import json
import os
from base64 import b64decode
from functools import reduce

from crax.auth import login
from crax.auth.authentication import create_session_signer
from crax.auth.models import Group, UserGroup
from crax.response_types import JSONResponse
from crax.urls import Route, Url
from crax.views import TemplateView, WsView
from sqlalchemy import and_, select
from websockets import ConnectionClosedOK

BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = "SuperSecret"
MIDDLEWARE = [
    "crax.auth.middleware.AuthMiddleware",
    "crax.auth.middleware.SessionMiddleware",
]

APPLICATIONS = ["ws_app"]
CLIENTS = {'boys': [], 'girls': []}


class Home(TemplateView):
    template = "index.html"
    login_required = True


class Login(TemplateView):
    template = "login.html"
    methods = ["GET", "POST"]

    async def post(self):
        credentials = json.loads(self.request.post)
        try:
            await login(self.request, **credentials)
            if hasattr(self.request.user, "first_name"):
                context = {'success': f"Welcome back, {self.request.user.username}"}
                status_code = 200
            else:
                context = {'error': f"User or password wrong"}
                status_code = 401
        except Exception as e:
            context = {'error': str(e)}
            status_code = 500
        response = JSONResponse(self.request, context)
        response.status_code = status_code
        return response


class WebSocketsHome(WsView):

    def __init__(self, request):
        super(WebSocketsHome, self).__init__(request)
        self.group_name = None

    async def on_connect(self, scope, receive, send):
        # This coroutine will be called every time a client connects.
        # So at this point we can do some useful things when we find a new connection.

        await super(WebSocketsHome, self).on_connect(scope, receive, send)
        if self.request.user.username:
            cookies = self.request.cookies
            # In our example, we want to check a group and store the user in the desired location.

            query = select([Group.c.name]).where(
                and_(UserGroup.c.user_id == self.request.user.pk, Group.c.id == UserGroup.c.group_id)
            )
            group = await Group.query.fetch_one(query=query)
            self.group_name = group['name']

            # We also want to get the username from the user's session key for future access via direct messaging

            exists = any(x for x in CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0])
            signer, max_age, _, _ = create_session_signer()
            session_cookie = b64decode(cookies['session_id'])
            user = signer.unsign(session_cookie, max_age=max_age)
            user = user.decode("utf-8")
            username = user.split(":")[0]
            val = {f"{cookies['session_id']}:{cookies['ws_secret']}:{username}": receive.__self__}

            # Since we have all the information we need, we can save the user
            # The key will be session: ws_cookie: username and the value will be an instance of uvicorn.WebSocketProtocol

            if not exists:
                CLIENTS[self.group_name].append(val)
            else:
                # We should clean up our storage to prevent existence of the same clients.
                # For example due to page reloading
                [
                    CLIENTS[self.group_name].remove(x) for x in
                    CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0]
                ]
                CLIENTS[self.group_name].append(val)

    async def on_disconnect(self, scope, receive, send):
        # This coroutine will be called every time a client disconnects.
        # So at this point we can do some useful things when we find a client disconnects.
        # We remove the client from the storage

        cookies = self.request.cookies
        if self.group_name:
            try:
                [
                    CLIENTS[self.group_name].remove(x) for x in
                    CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0]
                ]
            except ValueError:
                pass

    async def on_receive(self, scope, receive, send):
        # This coroutine will be called every time we receive a new incoming websocket message.
        # Check the type of message received and send a response according to the message type.

        if "text" in self.kwargs:
            message = json.loads(self.kwargs["text"])
            message_text = message["text"]
            clients = []
            if message["type"] == 'BroadCast':
                clients = reduce(lambda x, y: x + y, CLIENTS.values())

            elif message["type"] == 'Group':
                clients = CLIENTS[message['group']]

            elif message["type"] == 'Direct':
                username = message["user_name"]
                client_list = reduce(lambda x, y: x + y, CLIENTS.values())
                clients = [client for client in client_list if username.lower() in list(client)[0]]
            for client in clients:
                if isinstance(client, dict):
                    client = list(client.values())[0]
                    try:
                        await client.send(message_text)
                    except (ConnectionClosedOK, asyncio.streams.IncompleteReadError):
                        await client.close()
                        clients.remove(client)


URL_PATTERNS = [Route(Url("/"), Home), Route(Url("/", scheme="websocket"), WebSocketsHome), Route(Url("/login"), Login)]
DATABASES = {
        "default": {
            "driver": "sqlite",
            "name": f"/{BASE_URL}/ws_crax.sqlite",
        },
    }
app = Crax('ws_app.app')

if __name__ == "__main__":
    if sys.argv:
        from_shell(sys.argv, app.settings)




<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Crax Websockets</title>
    </head>
    <body>
        <div id="wsText"></div>
        <form>
            <input id="messageText"><br>
            <select id="targetGroup">
                <option>boys</option>
                <option>girls</option>
            </select>
            <select id="messageType">
                <option>BroadCast</option>
                <option>Group</option>
                <option>Direct</option>
            </select>
            <select id="userNames">
                <option>Greg</option>
                <option>Chuck</option>
                <option>Mike</option>
                <option>Amanda</option>
                <option>Lisa</option>
                <option>Anny</option>
            </select>
        </form>
        <a href="#" id="sendWs">Send Message</a>
        <script>
            var wsText = document.getElementById("wsText")
            var messageType = document.getElementById("messageType")
            var messageText = document.getElementById("messageText")
            var targetGroup = document.getElementById("targetGroup")
            var userName = document.getElementById("userNames")
            var sendButton = document.getElementById("sendWs")
            ws = new WebSocket("ws://127.0.0.1:8000")
            ws.onmessage = function(e){
                wsText.innerHTML+=e.data
            }

            sendButton.addEventListener("click", function (e) {
                e.preventDefault()
                var message = {type: messageType.value, text: messageText.value}
                var data
                if (messageText.value !== "") {
                    if (messageType.value === "BroadCast"){
                        // send broadcast message
                        data = message
                    }
                    else if (messageType.value === "Group"){
                        // send message to group
                        data = Object.assign(message, {group: targetGroup.value})
                    }
                    else if (messageType.value === "Direct"){
                        // send message to certain user
                        data = Object.assign(message, {user_name: userName.value})
                    }
                    ws.send(JSON.stringify(data))
                }
            })
        </script>
    </body>
    </html>


<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Crax Websockets</title>
</head>
<body>
    <form>
        <input id="username">
        <input id="password" type="password">
    </form>
    <div id="loginResults"></div>
    <a href="#" id="sendLogin">Login</a>

    <script>
        var loginButton = document.getElementById("sendLogin")
        var loginResults = document.getElementById("loginResults")
        var username = document.getElementById("username")
        var password = document.getElementById("password")
        loginButton.addEventListener("click", function (e) {
            e.preventDefault()
            if (username.value !== "" && password.value !== "") {
                var xhr = new XMLHttpRequest()
                xhr.overrideMimeType("application/json")
                xhr.open("POST", "/login")
                xhr.send(JSON.stringify({username: username.value, password: password.value}))
                xhr.onload = function () {
                    var result = JSON.parse(xhr.responseText)
                    if ("success" in result){
                        loginResults.innerHTML+="<h5 style='color: green'>"+result.success+ "</h5>"
                    }
                    else if ("error" in result) {
                        loginResults.innerHTML+="<h5 style='color: red'>"+result.error+ "</h5>"
                    }
                }
            }
        })
    </script>
</body>
</html>


يمكن الاطلاع على الكود الكامل في وثائق Crax.



حسنًا ، لقد حان الوقت الأكثر إثارة للاهتمام في هذه المقالة.



لماذا هو غير ضروري؟



أولاً ، كما ذكر أعلاه ، هناك العديد من الأطر التي تفعل الشيء نفسه ، ولديها مجتمع تم تشكيله بالفعل. في حين أن Crax هو طفل عمره أسبوع واحد. يكاد يكون جيش الرجل الواحد ضمانًا للتخلي عن المشروع عاجلاً أم آجلاً. إنه لأمر محزن ، لكن حقيقة أن العمل على الطاولة ، وإصدار الإصدارات والتحديثات فقط لنفسك و Vasily من Syktyvkar ، يعد وقتًا أطول بكثير مما كان عليه عندما يعمل المجتمع في المشروع. في غضون ذلك ، لا يحتوي المشروع على عدد من الميزات التي يجب توفرها في عام 2020. على سبيل المثال: لا يوجد دعم JWT (JOSE). لا يوجد دعم خارج الصندوق لأدوات OAuth2. لا يدعم GraphQL. من الواضح أنه يمكنك كتابة هذا بنفسك لمشروعك ، لكن Starlette أو FastAPI يمتلكانه بالفعل. علي أن أكتب هذا فقط (نعم ، إنه موجود في الخطط). سيكون هناك القليل عن الخطط في الختام.



يكتب مطورو Netflix و Microsoft عن FastAPI. حول Crax يكتب بدون اسم ، ولا يُعرف مكان ظهوره ، ومن يعرف أين يمكن أن يكون بالضبط بعد يوم من هاوية الغد.



لن يطلقوا على باخرة اسمي الغبي ،

والدتي تبكي في الليل ، لأنها أنجبت غريبًا ...

(ج)


هذا مهم. إنها تسمى السمعة والنظام البيئي. لا يمتلك Crax أيًا منهما. بدون هذه الأشياء المهمة ، يضمن المشروع أن يذهب إلى مكب النفايات دون أن يولد.



الأمر يستحق الفهم. ما هو مكتوب أعلاه ليس محاولة لطباعة الطبقات وليس نصًا لشخص بلا مأوى في القطار. هذا تقييم رصين وتحذير من أن "الحلول الجاهزة للإنتاج" ليست فقط نتائج تغطية الكود المصدري عن طريق الاختبارات ، بل هي تقييم عام لنضج التقنيات والنهج والحلول المستخدمة في المشروع.



إذا كنت قد بدأت للتو في التعرف على Python وتجربة أطر العمل ، فأنت في خطر: على الأرجح ، لن تجد إجابات على السؤال عن SO ، وربما يساعدك الرفاق الأكثر خبرة الذين ، للأسف ، قد لا يكونون هناك.



الأهداف



أول شيء أخطط للقيام به هو ، بالطبع ، إضافة بعض الأشياء الضرورية مثل JWT (JOSE) و OAuth2 ودعم GraphQL. هذا ما سيسهل علي وعلى الأشخاص المهتمين العمل. وهذا ، في الواقع ، هو الهدف الرئيسي لـ Crax - جعل عمل شخص ما أسهل قليلاً. ربما بحلول ذلك الوقت ستبدأ جولة جديدة في TechEmpower وستصبح المعايير أكثر وضوحًا. بل إنه من الممكن بعد ذلك أن يكون هناك اهتمام معين بالمجتمع.

هناك فكرة لكتابة CMS بناءً على Crax.

إذا لم أكن مخطئًا (إذا كنت مخطئًا - صححها) ، فليس لدينا حتى الآن أي CMS غير متزامن في Python في مجموعة أدواتنا. قد أغير رأيي وأقرر كتابة نوع من حلول التجارة الإلكترونية. ولكن ، من الواضح ، من أجل منع Crax من الغرق قبل الوصول إلى العوامات ، يجب القيام بشيء مثير للاهتمام على قاعدته. ربما سيكون المتحمسون مهتمين بهذا. المتحمسون موجودون عندما يكون مجانيًا. لأنه لا يوجد مال هنا ، وعلى الأرجح لن يكون هناك. Crax مجاني تمامًا للجميع ولم أحصل على عشرة سنتات لهذه الوظيفة. وبالتالي ، تم التخطيط للتطوير في "أمسيات شتوية طويلة" ، وربما في العام المقبل ، سيولد شيء مثير للاهتمام.



خاتمة



كنت أفكر في المجموعة التي يجب تضمينها في هذا المقال (بالمناسبة ، هذا هو أول منشور لي عن المورد). ربما كان الأمر يستحق وضعه تحت علامة "I'm PR". ما جعلني أغير رأيي: أولاً وقبل كل شيء ، حقيقة أنه لا يحتوي على طابع إعلاني لأي شيء.



لا توجد دعوة "الأولاد ، الاشتراك على وجه السرعة لطلبات السحب" . لا توجد فكرة هنا للعثور على راع. لا توجد حتى فكرة أنني أحضرت لك شيئًا لم تره من قبل (بالطبع ، رأيت). يمكنك استخلاص فكرة أنني مؤلف كل من المقالات وهذه الأداة ، وإدراك ما تمت كتابته كمراجعة. ونعم ، هذه هي أفضل طريقة. ستكون نتيجة ممتازة بالنسبة لي إذا تذكرت أنها كذلك.

على هذا ربما كل شيء.



"لذا ... حان الوقت لأخذ قضبان الصيد.

- لماذا ا؟

- قبعة هاريس الحمراء تخيف كل الأسماك.

(ج)


كود على وثائق جيثب




All Articles