1. مقدمة
في هذا الدرس التطبيقي حول الترميز، ستستخدم gRPC-Python لإنشاء عميل وخادم يشكّلان الأساس لتطبيق يحدّد المسارات مكتوب بلغة Python.
في نهاية هذا الدليل التعليمي، سيكون لديك عميل يتصل بخادم بعيد باستخدام gRPC للحصول على معلومات حول الميزات على مسار العميل، وإنشاء ملخّص لمسار العميل، وتبادل معلومات المسار، مثل آخر الأخبار عن حركة المرور، مع الخادم والعملاء الآخرين.
يتم تحديد الخدمة في ملف بتنسيق Protocol Buffers، وسيتم استخدام هذا الملف لإنشاء رمز نص نموذجي للعميل والخادم حتى يتمكّنا من التواصل مع بعضهما البعض، ما يوفّر عليك الوقت والجهد في تنفيذ هذه الوظيفة.
لا يهتم هذا الرمز الذي تم إنشاؤه بتعقيدات الاتصال بين الخادم والعميل فحسب، بل أيضًا بتسلسل البيانات وإلغاء تسلسلها.
ماذا ستتعلّم؟
- كيفية استخدام "مخازن البروتوكولات المؤقتة" (Protocol Buffers) لتحديد واجهة برمجة تطبيقات الخدمة
- كيفية إنشاء عميل وخادم يستندان إلى gRPC استنادًا إلى "مخازن البروتوكولات المؤقتة" من خلال إنشاء الرموز البرمجية آليًا
- فهم عملية التواصل بين العميل والخادم باستخدام gRPC
هذا الدرس التطبيقي حول الترميز موجّه لمطوّري Python المبتدئين في gRPC أو الذين يريدون تجديد معلوماتهم في المجال، أو أي شخص آخر مهتم بتطوير أنظمة موزّعة. لا يُشترط توفّر خبرة سابقة في gRPC.
2. قبل البدء
المتطلبات
- الإصدار 3.9 أو الإصدارات الأحدث من Python ننصحك باستخدام الإصدار 3.13 من Python. للحصول على تعليمات التثبيت الخاصة بكل نظام أساسي، يُرجى الاطّلاع على إعداد Python واستخدامه. بدلاً من ذلك، يمكنك تثبيت إصدار غير تابع للنظام من Python باستخدام أدوات مثل uv أو pyenv.
- pip لتثبيت حِزم Python
- venv لإنشاء بيئات Python افتراضية
تُعدّ الحزمتان ensurepip وvenv جزءًا من مكتبة Python العادية، وتتوفّران عادةً تلقائيًا.
ومع ذلك، تختار بعض التوزيعات المستندة إلى Debian (بما في ذلك Ubuntu) استبعادها عند إعادة توزيع Python. لتثبيت الحِزم، شغِّل الأمر التالي:
sudo apt install python3-pip python3-venv
الحصول على الشفرة
لتبسيط عملية التعلّم، يقدّم لك هذا الدرس التطبيقي حول الترميز بنية أساسية مُعدّة مسبقًا لرمز المصدر لمساعدتك على البدء. ستوضّح لك الخطوات التالية كيفية إكمال التطبيق، بما في ذلك إنشاء رمز gRPC باستخدام مكوّن برنامج تجميع مخازن البروتوكولات المؤقتة grpc_tools.protoc.
grpc-codelabs
يتوفّر رمز المصدر الخاص بالبنية الأساسية لهذا الدرس التطبيقي حول الترميز في الدليل codelabs/grpc-python-streaming/start_here. إذا كنت تفضّل عدم تنفيذ الرمز بنفسك، يتوفّر الرمز المصدر المكتمل في الدليل completed.
أولاً، أنشئ دليل عمل الدرس التطبيقي وادخله:
mkdir grpc-python-streaming && cd grpc-python-streaming
نزِّل الدرس التطبيقي حول الترميز واستخرِجه:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-1/codelabs/grpc-python-streaming/start_here
بدلاً من ذلك، يمكنك تنزيل ملف .zip الذي يحتوي على دليل الدرس العملي فقط وفك ضغطه يدويًا.
3- تحديد الرسائل والخدمات
تتمثّل خطوتك الأولى في تحديد خدمة gRPC للتطبيق وطريقة استدعاء إجراء عن بُعد (RPC) وأنواع رسائل الطلبات والردود باستخدام مخازن البروتوكولات المؤقتة. ستوفّر خدمتك ما يلي:
- طُرق استدعاء الإجراء عن بُعد التي تحمل الأسماء
ListFeaturesوRecordRouteوRouteChatوالتي ينفّذها الخادم ويستدعيها العميل - أنواع الرسائل
PointوFeatureوRectangleوRouteNoteوRouteSummary، وهي عبارة عن بنى بيانات يتم تبادلها بين العميل والخادم عند استدعاء طرق RPC.
سيتم تحديد طرق "استدعاء الإجراء عن بُعد" هذه وأنواع الرسائل الخاصة بها في ملف protos/route_guide.proto الخاص بالرمز المصدر المقدَّم.
يُشار إلى "مخازن البروتوكولات المؤقتة" عادةً باسم protobufs. لمزيد من المعلومات عن مصطلحات gRPC، يُرجى الاطّلاع على المفاهيم الأساسية والبنية ودورة الحياة في gRPC.
تحديد أنواع الرسائل
في ملف protos/route_guide.proto الخاص بالرمز المصدر، حدِّد أولاً نوع رسالة Point. يمثّل Point زوج إحداثيات خط العرض وخط الطول على الخريطة. في هذا الدرس التطبيقي، استخدِم أعدادًا صحيحة للإحداثيات:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
الرقمان 1 و2 هما رقما تعريف فريدان لكل حقل من الحقول في بنية message.
بعد ذلك، حدِّد نوع رسالة Feature. يستخدم Feature الحقل string للاسم أو العنوان البريدي الخاص بمكان محدّد من خلال Point:
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
لكي يتم بث نقاط متعددة ضمن منطقة إلى أحد العملاء، ستحتاج إلى رسالة Rectangle تمثّل مستطيلاً لخطوط الطول والعرض، ويتم تمثيله كنقطتَين متقابلتَين قطريًا lo وhi:
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
بالإضافة إلى ذلك، رسالة RouteNote تمثّل رسالة تم إرسالها في وقت محدّد:
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
أخيرًا، ستحتاج إلى رسالة RouteSummary. يتم تلقّي هذه الرسالة ردًا على طلب إجراء RecordRoute RPC، ويتم توضيح ذلك في القسم التالي. يحتوي هذا الملف على عدد النقاط الفردية التي تم تلقّيها، وعدد الميزات التي تم رصدها، وإجمالي المسافة المقطوعة كمجموع تراكمي للمسافة بين كل نقطة.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
تحديد طرق الخدمة
لتحديد خدمة، عليك تحديد خدمة مسماة في ملف .proto. يحتوي الملف route_guide.proto على بنية service باسم RouteGuide تحدّد طريقة واحدة أو أكثر توفّرها خدمة التطبيق.
عند تحديد طرق RPC داخل تعريف الخدمة، عليك تحديد أنواع الطلبات والردود. في هذا القسم من الدرس العملي، لنحدّد ما يلي:
ListFeatures
يحصل على عناصر Feature المتوفّرة ضمن Rectangle المحدّد. يتم بث النتائج بدلاً من عرضها دفعة واحدة لأنّ المستطيل قد يغطي مساحة كبيرة ويحتوي على عدد كبير من العناصر.
بالنسبة إلى هذا التطبيق، ستستخدم عملية بث من جهة الخادم لاستدعاء إجراء عن بُعد (RPC): يرسل العميل طلبًا إلى الخادم ويتلقّى بثًا لقراءة سلسلة من الرسائل. يقرأ العميل من البث الذي تم إرجاعه إلى أن لا تتبقى أي رسائل. كما هو موضّح في مثالنا، يمكنك تحديد طريقة البث من جهة الخادم من خلال وضع الكلمة الرئيسية stream قبل نوع الاستجابة.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
تقبل هذه السمة مجموعة من النقاط على مسار يتم اجتيازه، وتعرض RouteSummary عند اكتمال عملية الاجتياز.
في هذه الحالة، يكون طلب إجراء مكالمة RPC ببث من جهة العميل مناسبًا: يكتب العميل سلسلة من الرسائل ويرسلها إلى الخادم، وذلك باستخدام بث يتم توفيره مرة أخرى. بعد أن ينتهي العميل من كتابة الرسائل، ينتظر أن يقرأها الخادم كلها ويرسل رده. يمكنك تحديد طريقة البث من جهة العميل عن طريق وضع الكلمة الرئيسية stream قبل نوع الطلب.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
يقبل هذا النوع من الطلبات بثًا من RouteNotes يتم إرساله أثناء التنقّل في مسار، مع تلقّي RouteNotes أخرى (مثل تلك الواردة من مستخدمين آخرين).
هذه هي بالضبط حالة الاستخدام للبث الثنائي الاتجاه. هي استدعاء إجراء عن بُعد (RPC) لبث ثنائي الاتجاه، حيث يرسل كلا الجانبين سلسلة من الرسائل باستخدام بث للقراءة والكتابة. يعمل كل من هذين التدفقين بشكل مستقل، لذا يمكن للعملاء والخوادم القراءة والكتابة بأي ترتيب يريدونه: على سبيل المثال، يمكن للخادم الانتظار لتلقّي جميع رسائل العميل قبل كتابة ردوده، أو يمكنه بدلاً من ذلك قراءة رسالة ثم كتابة رسالة، أو أي مجموعة أخرى من عمليات القراءة والكتابة. يتم الحفاظ على ترتيب الرسائل في كل بث. يمكنك تحديد هذا النوع من الطرق من خلال وضع الكلمة الرئيسية stream قبل كلّ من الطلب والاستجابة.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. إنشاء رمز العميل والخادم
بعد ذلك، أنشئ رمز gRPC النموذجي لكل من العميل والخادم من ملف .proto باستخدام برنامج تجميع مخازن البروتوكولات المؤقتة.
لإنشاء رموز برمجية بلغة Python في gRPC، أنشأنا grpcio-tools. ويشمل ذلك:
- مترجم protoc العادي الذي ينشئ رموز Python البرمجية من تعريفات
message - إضافة gRPC protobuf التي تنشئ رمز Python (طرق كعب العميل والخادم) من تعريفات
service
سنثبّت حزمة grpcio-tools Python باستخدام pip. لننشئ بيئة افتراضية جديدة بلغة Python (venv) لعزل التبعيات الخاصة بمشروعك عن حِزم النظام:
python3 -m venv --upgrade-deps .venv
لتفعيل البيئة الافتراضية في shell bash/zsh، اتّبِع الخطوات التالية:
source .venv/bin/activate
بالنسبة إلى Windows والأصداف غير العادية، يمكنك الاطّلاع على الجدول في https://docs.python.org/3/library/venv.html#how-venvs-work.
بعد ذلك، ثبِّت grpcio-tools (يؤدي ذلك أيضًا إلى تثبيت حزمة grpcio):
pip install grpcio-tools
استخدِم الأمر التالي لإنشاء رمز النص النموذجي لـ Python:
python -m grpc_tools.protoc --proto_path=./protos \
--python_out=. --pyi_out=. --grpc_python_out=. \
./protos/route_guide.proto
سيؤدي ذلك إلى إنشاء الملفات التالية للواجهات التي حدّدناها في route_guide.proto:
- يحتوي
route_guide_pb2.pyعلى الرمز الذي ينشئ الفئات بشكل ديناميكي من تعريفاتmessage. route_guide_pb2.pyiهو "ملف رمز بديل" أو "ملف تلميح نوع" تم إنشاؤه من تعريفاتmessage. يحتوي فقط على التواقيع بدون تنفيذ. يمكن أن تستخدم بيئات التطوير المتكاملة ملفات Stub لتوفير ميزة الإكمال التلقائي ورصد الأخطاء بشكل أفضل.- يتم إنشاء
route_guide_pb2_grpc.pyمن تعريفاتserviceويتضمّن فئات ودوال خاصة بـ gRPC.
يتضمّن الرمز البرمجي الخاص بـ gRPC ما يلي:
5- إنشاء الخادم
لنلقِ نظرة أولاً على كيفية إنشاء خادم RouteGuide. ينقسم إنشاء خادم RouteGuide وتشغيله إلى عنصرَي عمل:
- تنفيذ واجهة مقدّم الخدمة التي تم إنشاؤها من تعريف الخدمة باستخدام دوال تنفّذ "العمل" الفعلي للخدمة
- تشغيل خادم gRPC للاستماع إلى الطلبات من العملاء وإرسال الردود
لنلقِ نظرة على route_guide_server.py.
تنفيذ RouteGuide
يحتوي route_guide_server.py على فئة RouteGuideServicer فرعية من الفئة route_guide_pb2_grpc.RouteGuideServicer التي تم إنشاؤها:
# RouteGuideServicer provides an implementation of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
تنفّذ RouteGuideServicer جميع طرق خدمة RouteGuide.
Server-side streaming RPC
ListFeatures هي عملية RPC لبث الردود ترسل عدة Feature إلى العميل:
def ListFeatures(self, request, context):
"""List all features contained within the given Rectangle."""
left = min(request.lo.longitude, request.hi.longitude)
right = max(request.lo.longitude, request.hi.longitude)
top = max(request.lo.latitude, request.hi.latitude)
bottom = min(request.lo.latitude, request.hi.latitude)
for feature in self.db:
lat, lng = feature.location.latitude, feature.location.longitude
if left <= lng <= right and bottom <= lat <= top:
yield feature
في هذا المثال، رسالة الطلب هي route_guide_pb2.Rectangle التي يريد العميل العثور على Feature ضمنها. بدلاً من عرض ردّ واحد، تعرض الطريقة صفر ردود أو أكثر.
Client-side streaming RPC
تستخدم طريقة بث الطلبات RecordRoute مكرّرًا لقيم الطلبات وتعرض قيمة استجابة واحدة.
def RecordRoute(self, request_iterator, context):
"""Calculate statistics about the trip composed of Points."""
point_count = 0
feature_count = 0
distance = 0.0
prev_point = None
start_time = time.time()
for point in request_iterator:
point_count += 1
if get_feature(self.db, point):
feature_count += 1
if prev_point:
distance += get_distance(prev_point, point)
prev_point = point
elapsed_time = time.time() - start_time
return route_guide_pb2.RouteSummary(
point_count=point_count,
feature_count=feature_count,
distance=int(distance),
elapsed_time=int(elapsed_time),
)
استدعاء إجراء عن بُعد (RPC) ثنائي الاتجاه
أخيرًا، لنلقِ نظرة على RPC RouteChat() للبث الثنائي الاتجاه:
def RouteChat(self, request_iterator, context):
"""
Receive a stream of message/location pairs, and responds with
a stream of all previous messages for the given location.
"""
prev_notes = []
for new_note in request_iterator:
for prev_note in prev_notes:
if prev_note.location == new_note.location:
yield prev_note
prev_notes.append(new_note)
تجمع دلالات هذه الطريقة بين دلالات طريقة بث الطلبات وطريقة بث الردود. يتم تمرير مكرّر لقيم الطلبات إليها، وهي نفسها مكرّر لقيم الردود.
بدء الخادم
بعد تنفيذ جميع طرق RouteGuide، تتمثّل الخطوة التالية في بدء تشغيل خادم gRPC ليتمكّن العملاء من استخدام خدمتك فعليًا:
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
RouteGuideServicer(),
server,
)
listen_addr = "localhost:50051"
server.add_insecure_port(listen_addr)
print(f"Starting server on {listen_addr}")
server.start()
server.wait_for_termination()
طريقة الخادم start() غير حظر. سيتم إنشاء سلسلة محادثات جديدة للتعامل مع الطلبات. في كثير من الأحيان، لن يكون لدى سلسلة التعليمات التي تستدعي server.start() أي عمل آخر لتنفيذه في الوقت الحالي. في هذه الحالة، يمكنك استدعاء server.wait_for_termination() لحظر سلسلة استدعاء الدوال البرمجية بشكل كامل إلى أن يتم إيقاف الخادم.
6. إنشاء العميل
لنلقِ نظرة على route_guide_client.py.
إنشاء مقالة موجزة
لاستدعاء طرق الخدمة، علينا أولاً إنشاء رمز بديل.
ننشئ مثيلاً لفئة RouteGuideStub من وحدة route_guide_pb2_grpc، تم إنشاؤها من طريقة .proto. In run():
with grpc.insecure_channel("localhost:50051") as channel:
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
يُرجى العِلم أنّه يتم استخدام channel هنا كأداة لإدارة السياق، وسيتم إغلاقها تلقائيًا عند خروج المترجم من كتلة with.
طُرق خدمات الاستدعاء
بالنسبة إلى طرق طلبات استدعاء الإجراء عن بُعد (RPC) التي تعرض استجابة واحدة (طرق "response-unary")، يتيح gRPC Python دلالات التحكّم المتزامن (الحظر) وغير المتزامن (عدم الحظر) في سير العمل. بالنسبة إلى طرق RPC التي تتضمّن بث الاستجابة، تُرجع الطلبات على الفور مكرّرًا لقيم الاستجابة. يتم حظر استدعاء كتلة طريقة next() الخاصة بهذا المكرّر إلى أن يصبح الردّ الذي سيتم إنشاؤه من المكرّر متاحًا.
Server-side streaming RPC
يشبه استدعاء ListFeatures لبث الردود العمل مع أنواع التسلسلات:
def guide_list_features(stub):
_lo = route_guide_pb2.Point(latitude=400000000, longitude=-750000000)
_hi = route_guide_pb2.Point(latitude=420000000, longitude=-730000000)
rectangle = route_guide_pb2.Rectangle(
lo=_lo,
hi=_hi,
)
print("Looking for features between 40, -75 and 42, -73")
features = stub.ListFeatures(rectangle)
for feature in features:
print(
f"Feature called '{feature.name}'"
f" at {format_point(feature.location)}"
)
Client-side streaming RPC
يشبه استدعاء RecordRoute لطلب بث البيانات تمرير مكرّر إلى طريقة محلية. مثل طلب RPC البسيط أعلاه الذي يعرض أيضًا ردًا واحدًا، يمكن استدعاؤه بشكل متزامن:
def guide_record_route(stub):
feature_list = route_guide_resources.read_route_guide_database()
route_iterator = generate_route(feature_list)
route_summary = stub.RecordRoute(route_iterator)
print(f"Finished trip with {route_summary.point_count} points")
print(f"Passed {route_summary.feature_count} features")
print(f"Traveled {route_summary.distance} meters")
print(f"It took {route_summary.elapsed_time} seconds")
استدعاء إجراء عن بُعد (RPC) ثنائي الاتجاه
يحتوي استدعاء RouteChat ثنائي الاتجاه (كما هو الحال على جهة الخادم) على مجموعة من دلالات بث الطلب وبث الرد.
أنشئ رسائل الطلبات وأرسِلها واحدةً تلو الأخرى باستخدام yield.
def generate_notes():
home = route_guide_pb2.Point(latitude=1, longitude=1)
work = route_guide_pb2.Point(latitude=2, longitude=2)
notes = [
make_route_note("Departing from home", home),
make_route_note("Arrived at work", work),
make_route_note("Having lunch at work", work),
make_route_note("Departing from work", work),
make_route_note("Arrived home", home),
]
for note in notes:
print(
f"Sending RouteNote for {format_point(note.location)}:"
f" {note.message}"
)
yield note
# Sleep to simulate moving from one point to another.
# Only for demonstrating the order of the messages.
time.sleep(0.1)
تلقّي استجابات الخادم ومعالجتها:
def guide_route_chat(stub):
responses = stub.RouteChat(generate_notes())
for response in responses:
print(
"< Found previous note at"
f" {format_point(response.location)}: {response.message}"
)
استدعاء الطرق المساعدة
في run، نفِّذ الطرق التي أنشأناها للتو، ومرِّر إليها stub.
print("-------------- ListFeatures --------------")
guide_list_features(stub)
print("-------------- RecordRoute --------------")
guide_record_route(stub)
print("-------------- RouteChat --------------")
guide_route_chat(stub)
7. للتجربة:
شغِّل الخادم:
python route_guide_server.py
من وحدة طرفية مختلفة، فعِّل البيئة الافتراضية مرة أخرى (source .venv/bin/activate))، ثم شغِّل العميل:
python route_guide_client.py
لنلقِ نظرة على الناتج.
ListFeatures
أولاً، ستجد قائمة الميزات. يتم بث كل ميزة من الخادم (RPC للبث من جهة الخادم) عند اكتشاف أنّها تقع ضمن المستطيل المطلوب:
-------------- ListFeatures -------------- Looking for features between 40, -75 and 42, -73 Feature called 'Patriots Path, Mendham, NJ 07945, USA' at (lat=407838351, lng=-746143763) Feature called '101 New Jersey 10, Whippany, NJ 07981, USA' at (lat=408122808, lng=-743999179) Feature called 'U.S. 6, Shohola, PA 18458, USA' at (lat=413628156, lng=-749015468) Feature called '5 Conners Road, Kingston, NY 12401, USA' at (lat=419999544, lng=-740371136) ...
RecordRoute
ثانيًا، يعرض الرمز RecordRoute قائمة بالنقاط التي تمت زيارتها بشكل عشوائي والتي يتم بثها من العميل إلى الخادم (بث RPC من جهة العميل):
-------------- RecordRoute -------------- Visiting point (lat=410395868, lng=-744972325) Visiting point (lat=404310607, lng=-740282632) Visiting point (lat=403966326, lng=-748519297) Visiting point (lat=407586880, lng=-741670168) Visiting point (lat=406589790, lng=-743560121) Visiting point (lat=410322033, lng=-747871659) Visiting point (lat=415464475, lng=-747175374) Visiting point (lat=407586880, lng=-741670168) Visiting point (lat=402647019, lng=-747071791) Visiting point (lat=414638017, lng=-745957854)
بعد أن ينتهي العميل من بث جميع النقاط التي تمت زيارتها، سيتلقّى استجابة غير متدفقة (طلب إجراء عن بُعد أحادي) من الخادم. سيتضمّن هذا الردّ ملخّصًا للحسابات التي تم إجراؤها على المسار الكامل للعميل.
Finished trip with 10 points Passed 10 features Traveled 654743 meters It took 0 seconds
RouteChat
أخيرًا، يوضّح الناتج RouteChat البث ثنائي الاتجاه. عندما "يزور" العميل النقطتين home أو work، يسجّل ملاحظة للنقطة عن طريق إرسال RouteNote إلى الخادم. عندما تتم زيارة نقطة سبقًا، يعرض الخادم جميع الملاحظات السابقة لهذه النقطة.
-------------- RouteChat -------------- Sending RouteNote for (lat=1, lng=1): Departing from home Sending RouteNote for (lat=2, lng=2): Arrived at work Sending RouteNote for (lat=2, lng=2): Having lunch at work < Found previous note at (lat=2, lng=2): Arrived at work Sending RouteNote for (lat=2, lng=2): Departing from work < Found previous note at (lat=2, lng=2): Arrived at work < Found previous note at (lat=2, lng=2): Having lunch at work Sending RouteNote for (lat=1, lng=1): Arrived home < Found previous note at (lat=1, lng=1): Departing from home
8. الخطوات التالية
- تعرَّف على طريقة عمل gRPC في مقدمة عن gRPC والمفاهيم الأساسية
- اتّبِع الخطوات الواردة في البرنامج التعليمي الخاص بالأساسيات
- استكشاف مرجع واجهة برمجة تطبيقات Python