未验证 提交 85e01b9a 编写于 作者: J Jacob Schmitt 提交者: GitHub

Merge branch 'master' into consuming-apis-python

......@@ -12,7 +12,7 @@ connex_app = connexion.App(__name__, specification_dir=basedir)
app = connex_app.app
# Build the Sqlite ULR for SqlAlchemy
sqlite_url = "sqlite:////" + os.path.join(basedir, "people.db")
sqlite_url = "sqlite:///" + os.path.join(basedir, "people.db")
# Configure the SqlAlchemy part of the app instance
app.config["SQLALCHEMY_ECHO"] = True
......
......@@ -4,7 +4,7 @@
float cmult(int int_param, float float_param) {
float return_value = int_param * float_param;
printf(" In cmult : int: %d float %.1f returning %.1f\n", int_param,
printf(" In cmult : int %d float %.1f returning %.1f\n", int_param,
float_param, return_value);
return return_value;
}
......
......@@ -6,7 +6,7 @@
float cppmult(int int_param, float float_param) {
float return_value = int_param * float_param;
std::cout << std::setprecision(1) << std::fixed
<< " In cppmul: int: " << int_param
<< " In cppmul: int " << int_param
<< " float " << float_param
<< " returning " << return_value
<< std::endl;
......
......@@ -199,7 +199,7 @@ class Window(QMainWindow):
def pasteContent(self):
# Logic for pasting content goes here...
self.centralWidget.setText("<b>Edit > Pate</b> clicked")
self.centralWidget.setText("<b>Edit > Paste</b> clicked")
def cutContent(self):
# Logic for cutting content goes here...
......
# Running the Example
1. Install Docker if you haven't already.
2. Run `./build_and_run.sh`.
#!/bin/bash
./gen_certs.sh
DOCKER_BUILDKIT=1 docker build . -f marketplace/Dockerfile -t marketplace --secret id=ca.key,src=ca.key
DOCKER_BUILDKIT=1 docker build . -f recommendations/Dockerfile -t recommendations --secret id=ca.key,src=ca.key
docker-compose up
version: "3.8"
services:
marketplace:
environment:
RECOMMENDATIONS_HOST: recommendations
# DOCKER_BUILDKIT=1 docker build . -f marketplace/Dockerfile \
# -t marketplace --secret id=ca.key,src=ca.key
image: marketplace
networks:
- microservices
ports:
- 5000:5000
recommendations:
# DOCKER_BUILDKIT=1 docker build . -f recommendations/Dockerfile \
# -t recommendations --secret id=ca.key,src=ca.key
image: recommendations
networks:
- microservices
networks:
microservices:
#!/bin/bash
# Generate CA key and self-signed cert
openssl req -x509 -nodes -newkey rsa:4096 -keyout ca.key -out ca.pem -subj /O=me
# Generate a private key and certificate signing request for the client and server
openssl req -nodes -newkey rsa:4096 -keyout client.key -out client.csr -subj /CN=marketplace
openssl req -nodes -newkey rsa:4096 -keyout server.key -out server.csr -subj /CN=recommendations
# Sign the client and server certs with the CA cert
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -set_serial 1 -out client.pem
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -set_serial 1 -out server.pem
#!/bin/bash
set -euo pipefail
docker-compose up -d
trap "docker-compose down" EXIT
sleep 5 # Give the services time to warm up
docker-compose exec marketplace pytest
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: marketplace
labels:
app: marketplace
spec:
replicas: 1
selector:
matchLabels:
app: marketplace
template:
metadata:
labels:
app: marketplace
spec:
containers:
- name: marketplace
image: hidan/python-microservices-article-marketplace:0.1
env:
- name: RECOMMENDATIONS_HOST
value: recommendations
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: recommendations
labels:
app: recommendations
spec:
replicas: 1
selector:
matchLabels:
app: recommendations
template:
metadata:
labels:
app: recommendations
spec:
containers:
- name: recommendations
image: hidan/python-microservices-article-recommendations:0.1
---
apiVersion: v1
kind: Service
metadata:
name: recommendations
spec:
selector:
app: recommendations
ports:
- protocol: TCP
port: 50051
targetPort: 50051
---
apiVersion: v1
kind: Service
metadata:
name: marketplace
spec:
type: LoadBalancer
selector:
app: marketplace
ports:
- protocol: TCP
port: 5000
targetPort: 5000
# syntax = docker/dockerfile:1.0-experimental
# DOCKER_BUILDKIT=1 docker build . -f marketplace/Dockerfile -t marketplace --secret id=ca.key,src=ca.key
FROM python
RUN mkdir /service
COPY protobufs/ /service/protobufs/
COPY marketplace/ /service/marketplace/
COPY ca.pem /service/marketplace/
WORKDIR /service/marketplace
RUN pip install -r requirements.txt
RUN python -m grpc_tools.protoc -I ../protobufs --python_out=. \
--grpc_python_out=. ../protobufs/recommendations.proto
RUN openssl req -nodes -newkey rsa:4096 -subj /CN=marketplace \
-keyout client.key -out client.csr
RUN --mount=type=secret,id=ca.key \
openssl x509 -req -in client.csr -CA ca.pem -CAkey /run/secrets/ca.key \
-set_serial 1 -out client.pem
EXPOSE 5000
ENV FLASK_APP=marketplace.py
ENTRYPOINT [ "flask", "run", "--host=0.0.0.0"]
import os
from flask import Flask, render_template
import grpc
from recommendations_pb2 import BookCategory, RecommendationRequest
from recommendations_pb2_grpc import RecommendationsStub
app = Flask(__name__)
recommendations_host = os.getenv("RECOMMENDATIONS_HOST", "localhost")
with open("client.key", "rb") as fp:
client_key = fp.read()
with open("client.pem", "rb") as fp:
client_cert = fp.read()
with open("ca.pem", "rb") as fp:
ca_cert = fp.read()
creds = grpc.ssl_channel_credentials(ca_cert, client_key, client_cert)
recommendations_channel = grpc.secure_channel(
f"{recommendations_host}:443", creds
)
recommendations_client = RecommendationsStub(recommendations_channel)
@app.route("/")
def render_homepage():
recommendations_request = RecommendationRequest(
user_id=1, category=BookCategory.MYSTERY, max_results=3
)
recommendations_response = recommendations_client.Recommend(
recommendations_request
)
return render_template(
"homepage.html",
recommendations=recommendations_response.recommendations,
)
from urllib.request import urlopen
def test_render_homepage():
homepage_html = urlopen("http://localhost:5000").read().decode("utf-8")
assert "<title>Online Books For You</title>" in homepage_html
assert homepage_html.count("<li>") == 3
# -*- coding: utf-8 -*-
# flake8: noqa
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: recommendations.proto
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name="recommendations.proto",
package="",
syntax="proto3",
serialized_options=None,
serialized_pb=b'\n\x15recommendations.proto"^\n\x15RecommendationRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\x05\x12\x1f\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32\r.BookCategory\x12\x13\n\x0bmax_results\x18\x03 \x01(\x05"/\n\x12\x42ookRecommendation\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05title\x18\x02 \x01(\t"F\n\x16RecommendationResponse\x12,\n\x0frecommendations\x18\x01 \x03(\x0b\x32\x13.BookRecommendation*?\n\x0c\x42ookCategory\x12\x0b\n\x07MYSTERY\x10\x00\x12\x13\n\x0fSCIENCE_FICTION\x10\x01\x12\r\n\tSELF_HELP\x10\x02\x32O\n\x0fRecommendations\x12<\n\tRecommend\x12\x16.RecommendationRequest\x1a\x17.RecommendationResponseb\x06proto3',
)
_BOOKCATEGORY = _descriptor.EnumDescriptor(
name="BookCategory",
full_name="BookCategory",
filename=None,
file=DESCRIPTOR,
values=[
_descriptor.EnumValueDescriptor(
name="MYSTERY",
index=0,
number=0,
serialized_options=None,
type=None,
),
_descriptor.EnumValueDescriptor(
name="SCIENCE_FICTION",
index=1,
number=1,
serialized_options=None,
type=None,
),
_descriptor.EnumValueDescriptor(
name="SELF_HELP",
index=2,
number=2,
serialized_options=None,
type=None,
),
],
containing_type=None,
serialized_options=None,
serialized_start=242,
serialized_end=305,
)
_sym_db.RegisterEnumDescriptor(_BOOKCATEGORY)
BookCategory = enum_type_wrapper.EnumTypeWrapper(_BOOKCATEGORY)
MYSTERY = 0
SCIENCE_FICTION = 1
SELF_HELP = 2
_RECOMMENDATIONREQUEST = _descriptor.Descriptor(
name="RecommendationRequest",
full_name="RecommendationRequest",
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name="user_id",
full_name="RecommendationRequest.user_id",
index=0,
number=1,
type=5,
cpp_type=1,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
_descriptor.FieldDescriptor(
name="category",
full_name="RecommendationRequest.category",
index=1,
number=2,
type=14,
cpp_type=8,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
_descriptor.FieldDescriptor(
name="max_results",
full_name="RecommendationRequest.max_results",
index=2,
number=3,
type=5,
cpp_type=1,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=25,
serialized_end=119,
)
_BOOKRECOMMENDATION = _descriptor.Descriptor(
name="BookRecommendation",
full_name="BookRecommendation",
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name="id",
full_name="BookRecommendation.id",
index=0,
number=1,
type=5,
cpp_type=1,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
_descriptor.FieldDescriptor(
name="title",
full_name="BookRecommendation.title",
index=1,
number=2,
type=9,
cpp_type=9,
label=1,
has_default_value=False,
default_value=b"".decode("utf-8"),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=121,
serialized_end=168,
)
_RECOMMENDATIONRESPONSE = _descriptor.Descriptor(
name="RecommendationResponse",
full_name="RecommendationResponse",
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name="recommendations",
full_name="RecommendationResponse.recommendations",
index=0,
number=1,
type=11,
cpp_type=10,
label=3,
has_default_value=False,
default_value=[],
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=170,
serialized_end=240,
)
_RECOMMENDATIONREQUEST.fields_by_name["category"].enum_type = _BOOKCATEGORY
_RECOMMENDATIONRESPONSE.fields_by_name[
"recommendations"
].message_type = _BOOKRECOMMENDATION
DESCRIPTOR.message_types_by_name[
"RecommendationRequest"
] = _RECOMMENDATIONREQUEST
DESCRIPTOR.message_types_by_name["BookRecommendation"] = _BOOKRECOMMENDATION
DESCRIPTOR.message_types_by_name[
"RecommendationResponse"
] = _RECOMMENDATIONRESPONSE
DESCRIPTOR.enum_types_by_name["BookCategory"] = _BOOKCATEGORY
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
RecommendationRequest = _reflection.GeneratedProtocolMessageType(
"RecommendationRequest",
(_message.Message,),
{
"DESCRIPTOR": _RECOMMENDATIONREQUEST,
"__module__": "recommendations_pb2"
# @@protoc_insertion_point(class_scope:RecommendationRequest)
},
)
_sym_db.RegisterMessage(RecommendationRequest)
BookRecommendation = _reflection.GeneratedProtocolMessageType(
"BookRecommendation",
(_message.Message,),
{
"DESCRIPTOR": _BOOKRECOMMENDATION,
"__module__": "recommendations_pb2"
# @@protoc_insertion_point(class_scope:BookRecommendation)
},
)
_sym_db.RegisterMessage(BookRecommendation)
RecommendationResponse = _reflection.GeneratedProtocolMessageType(
"RecommendationResponse",
(_message.Message,),
{
"DESCRIPTOR": _RECOMMENDATIONRESPONSE,
"__module__": "recommendations_pb2"
# @@protoc_insertion_point(class_scope:RecommendationResponse)
},
)
_sym_db.RegisterMessage(RecommendationResponse)
_RECOMMENDATIONS = _descriptor.ServiceDescriptor(
name="Recommendations",
full_name="Recommendations",
file=DESCRIPTOR,
index=0,
serialized_options=None,
serialized_start=307,
serialized_end=386,
methods=[
_descriptor.MethodDescriptor(
name="Recommend",
full_name="Recommendations.Recommend",
index=0,
containing_service=None,
input_type=_RECOMMENDATIONREQUEST,
output_type=_RECOMMENDATIONRESPONSE,
serialized_options=None,
),
],
)
_sym_db.RegisterServiceDescriptor(_RECOMMENDATIONS)
DESCRIPTOR.services_by_name["Recommendations"] = _RECOMMENDATIONS
# @@protoc_insertion_point(module_scope)
# flake8: noqa
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
import recommendations_pb2 as recommendations__pb2
class RecommendationsStub(object):
"""Missing associated documentation comment in .proto file"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.Recommend = channel.unary_unary(
"/Recommendations/Recommend",
request_serializer=recommendations__pb2.RecommendationRequest.SerializeToString,
response_deserializer=recommendations__pb2.RecommendationResponse.FromString,
)
class RecommendationsServicer(object):
"""Missing associated documentation comment in .proto file"""
def Recommend(self, request, context):
"""Missing associated documentation comment in .proto file"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def add_RecommendationsServicer_to_server(servicer, server):
rpc_method_handlers = {
"Recommend": grpc.unary_unary_rpc_method_handler(
servicer.Recommend,
request_deserializer=recommendations__pb2.RecommendationRequest.FromString,
response_serializer=recommendations__pb2.RecommendationResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
"Recommendations", rpc_method_handlers
)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class Recommendations(object):
"""Missing associated documentation comment in .proto file"""
@staticmethod
def Recommend(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/Recommendations/Recommend",
recommendations__pb2.RecommendationRequest.SerializeToString,
recommendations__pb2.RecommendationResponse.FromString,
options,
channel_credentials,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
flask ~= 1.1
grpcio-tools ~= 1.30
Jinja2 ~= 2.11
pytest ~= 5.4
<!doctype html>
<html lang="en">
<head>
<title>Online Books For You</title>
</head>
<body>
<h1>Mystery books you may like</h1>
<ul>
{% for book in recommendations %}
<li>{{ book.title }}</li>
{% endfor %}
</ul>
</body>
syntax = "proto3";
enum BookCategory {
MYSTERY = 0;
SCIENCE_FICTION = 1;
SELF_HELP = 2;
}
message RecommendationRequest {
int32 user_id = 1;
BookCategory category = 2;
int32 max_results = 3;
}
message BookRecommendation {
int32 id = 1;
string title = 2;
}
message RecommendationResponse {
repeated BookRecommendation recommendations = 1;
}
service Recommendations {
rpc Recommend (RecommendationRequest) returns (RecommendationResponse);
}
# syntax = docker/dockerfile:1.0-experimental
# DOCKER_BUILDKIT=1 docker build . -f recommendations/Dockerfile -t recommendations --secret id=ca.key,src=ca.key
FROM python
RUN mkdir /service
COPY protobufs/ /service/protobufs/
COPY recommendations/ /service/recommendations/
COPY ca.pem /service/recommendations/
WORKDIR /service/recommendations
RUN pip install -r requirements.txt
RUN python -m grpc_tools.protoc -I ../protobufs --python_out=. \
--grpc_python_out=. ../protobufs/recommendations.proto
RUN openssl req -nodes -newkey rsa:4096 -subj /CN=recommendations \
-keyout server.key -out server.csr
RUN --mount=type=secret,id=ca.key \
openssl x509 -req -in server.csr -CA ca.pem -CAkey /run/secrets/ca.key \
-set_serial 1 -out server.pem
EXPOSE 50051
ENTRYPOINT [ "python", "recommendations.py" ]
from concurrent import futures
import random
import grpc
from grpc_interceptor import ExceptionToStatusInterceptor
from grpc_interceptor.exceptions import NotFound
from recommendations_pb2 import (
BookCategory,
BookRecommendation,
RecommendationResponse,
)
import recommendations_pb2_grpc
books_by_category = {
BookCategory.MYSTERY: [
BookRecommendation(id=1, title="The Maltese Falcon"),
BookRecommendation(id=2, title="Murder on the Orient Express"),
BookRecommendation(id=3, title="The Hound of the Baskervilles"),
],
BookCategory.SCIENCE_FICTION: [
BookRecommendation(id=4, title="The Hitchhiker's Guide To The Galaxy"),
BookRecommendation(id=5, title="Ender's Game"),
BookRecommendation(id=6, title="The Dune Chronicles"),
],
BookCategory.SELF_HELP: [
BookRecommendation(
id=7, title="The 7 Habits of Highly Effective People"
),
BookRecommendation(
id=8, title="How to Win Friends and Influence People"
),
BookRecommendation(id=9, title="Man’s Search for Meaning"),
],
}
class RecommendationService(recommendations_pb2_grpc.RecommendationsServicer):
def Recommend(self, request, context):
if request.category not in books_by_category:
raise NotFound("Category not found")
books_for_category = books_by_category[request.category]
num_results = min(request.max_results, len(books_for_category))
books_to_recommend = random.sample(books_for_category, num_results)
return RecommendationResponse(recommendations=books_to_recommend)
def serve():
interceptors = [ExceptionToStatusInterceptor()]
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10), interceptors=interceptors
)
recommendations_pb2_grpc.add_RecommendationsServicer_to_server(
RecommendationService(), server
)
with open("server.key", "rb") as fp:
server_key = fp.read()
with open("server.pem", "rb") as fp:
server_cert = fp.read()
with open("ca.pem", "rb") as fp:
ca_cert = fp.read()
creds = grpc.ssl_server_credentials(
[(server_key, server_cert)],
root_certificates=ca_cert,
require_client_auth=True,
)
server.add_secure_port("[::]:443", creds)
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serve()
# -*- coding: utf-8 -*-
# flake8: noqa
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: recommendations.proto
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name="recommendations.proto",
package="",
syntax="proto3",
serialized_options=None,
serialized_pb=b'\n\x15recommendations.proto"^\n\x15RecommendationRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\x05\x12\x1f\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32\r.BookCategory\x12\x13\n\x0bmax_results\x18\x03 \x01(\x05"/\n\x12\x42ookRecommendation\x12\n\n\x02id\x18\x01 \x01(\x05\x12\r\n\x05title\x18\x02 \x01(\t"F\n\x16RecommendationResponse\x12,\n\x0frecommendations\x18\x01 \x03(\x0b\x32\x13.BookRecommendation*?\n\x0c\x42ookCategory\x12\x0b\n\x07MYSTERY\x10\x00\x12\x13\n\x0fSCIENCE_FICTION\x10\x01\x12\r\n\tSELF_HELP\x10\x02\x32O\n\x0fRecommendations\x12<\n\tRecommend\x12\x16.RecommendationRequest\x1a\x17.RecommendationResponseb\x06proto3',
)
_BOOKCATEGORY = _descriptor.EnumDescriptor(
name="BookCategory",
full_name="BookCategory",
filename=None,
file=DESCRIPTOR,
values=[
_descriptor.EnumValueDescriptor(
name="MYSTERY",
index=0,
number=0,
serialized_options=None,
type=None,
),
_descriptor.EnumValueDescriptor(
name="SCIENCE_FICTION",
index=1,
number=1,
serialized_options=None,
type=None,
),
_descriptor.EnumValueDescriptor(
name="SELF_HELP",
index=2,
number=2,
serialized_options=None,
type=None,
),
],
containing_type=None,
serialized_options=None,
serialized_start=242,
serialized_end=305,
)
_sym_db.RegisterEnumDescriptor(_BOOKCATEGORY)
BookCategory = enum_type_wrapper.EnumTypeWrapper(_BOOKCATEGORY)
MYSTERY = 0
SCIENCE_FICTION = 1
SELF_HELP = 2
_RECOMMENDATIONREQUEST = _descriptor.Descriptor(
name="RecommendationRequest",
full_name="RecommendationRequest",
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name="user_id",
full_name="RecommendationRequest.user_id",
index=0,
number=1,
type=5,
cpp_type=1,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
_descriptor.FieldDescriptor(
name="category",
full_name="RecommendationRequest.category",
index=1,
number=2,
type=14,
cpp_type=8,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
_descriptor.FieldDescriptor(
name="max_results",
full_name="RecommendationRequest.max_results",
index=2,
number=3,
type=5,
cpp_type=1,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=25,
serialized_end=119,
)
_BOOKRECOMMENDATION = _descriptor.Descriptor(
name="BookRecommendation",
full_name="BookRecommendation",
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name="id",
full_name="BookRecommendation.id",
index=0,
number=1,
type=5,
cpp_type=1,
label=1,
has_default_value=False,
default_value=0,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
_descriptor.FieldDescriptor(
name="title",
full_name="BookRecommendation.title",
index=1,
number=2,
type=9,
cpp_type=9,
label=1,
has_default_value=False,
default_value=b"".decode("utf-8"),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=121,
serialized_end=168,
)
_RECOMMENDATIONRESPONSE = _descriptor.Descriptor(
name="RecommendationResponse",
full_name="RecommendationResponse",
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name="recommendations",
full_name="RecommendationResponse.recommendations",
index=0,
number=1,
type=11,
cpp_type=10,
label=3,
has_default_value=False,
default_value=[],
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=170,
serialized_end=240,
)
_RECOMMENDATIONREQUEST.fields_by_name["category"].enum_type = _BOOKCATEGORY
_RECOMMENDATIONRESPONSE.fields_by_name[
"recommendations"
].message_type = _BOOKRECOMMENDATION
DESCRIPTOR.message_types_by_name[
"RecommendationRequest"
] = _RECOMMENDATIONREQUEST
DESCRIPTOR.message_types_by_name["BookRecommendation"] = _BOOKRECOMMENDATION
DESCRIPTOR.message_types_by_name[
"RecommendationResponse"
] = _RECOMMENDATIONRESPONSE
DESCRIPTOR.enum_types_by_name["BookCategory"] = _BOOKCATEGORY
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
RecommendationRequest = _reflection.GeneratedProtocolMessageType(
"RecommendationRequest",
(_message.Message,),
{
"DESCRIPTOR": _RECOMMENDATIONREQUEST,
"__module__": "recommendations_pb2"
# @@protoc_insertion_point(class_scope:RecommendationRequest)
},
)
_sym_db.RegisterMessage(RecommendationRequest)
BookRecommendation = _reflection.GeneratedProtocolMessageType(
"BookRecommendation",
(_message.Message,),
{
"DESCRIPTOR": _BOOKRECOMMENDATION,
"__module__": "recommendations_pb2"
# @@protoc_insertion_point(class_scope:BookRecommendation)
},
)
_sym_db.RegisterMessage(BookRecommendation)
RecommendationResponse = _reflection.GeneratedProtocolMessageType(
"RecommendationResponse",
(_message.Message,),
{
"DESCRIPTOR": _RECOMMENDATIONRESPONSE,
"__module__": "recommendations_pb2"
# @@protoc_insertion_point(class_scope:RecommendationResponse)
},
)
_sym_db.RegisterMessage(RecommendationResponse)
_RECOMMENDATIONS = _descriptor.ServiceDescriptor(
name="Recommendations",
full_name="Recommendations",
file=DESCRIPTOR,
index=0,
serialized_options=None,
serialized_start=307,
serialized_end=386,
methods=[
_descriptor.MethodDescriptor(
name="Recommend",
full_name="Recommendations.Recommend",
index=0,
containing_service=None,
input_type=_RECOMMENDATIONREQUEST,
output_type=_RECOMMENDATIONRESPONSE,
serialized_options=None,
),
],
)
_sym_db.RegisterServiceDescriptor(_RECOMMENDATIONS)
DESCRIPTOR.services_by_name["Recommendations"] = _RECOMMENDATIONS
# @@protoc_insertion_point(module_scope)
# flake8: noqa
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc
import recommendations_pb2 as recommendations__pb2
class RecommendationsStub(object):
"""Missing associated documentation comment in .proto file"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.Recommend = channel.unary_unary(
"/Recommendations/Recommend",
request_serializer=recommendations__pb2.RecommendationRequest.SerializeToString,
response_deserializer=recommendations__pb2.RecommendationResponse.FromString,
)
class RecommendationsServicer(object):
"""Missing associated documentation comment in .proto file"""
def Recommend(self, request, context):
"""Missing associated documentation comment in .proto file"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def add_RecommendationsServicer_to_server(servicer, server):
rpc_method_handlers = {
"Recommend": grpc.unary_unary_rpc_method_handler(
servicer.Recommend,
request_deserializer=recommendations__pb2.RecommendationRequest.FromString,
response_serializer=recommendations__pb2.RecommendationResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
"Recommendations", rpc_method_handlers
)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class Recommendations(object):
"""Missing associated documentation comment in .proto file"""
@staticmethod
def Recommend(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/Recommendations/Recommend",
recommendations__pb2.RecommendationRequest.SerializeToString,
recommendations__pb2.RecommendationResponse.FromString,
options,
channel_credentials,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
from recommendations import RecommendationService
from recommendations_pb2 import BookCategory, RecommendationRequest
def test_recommendations():
service = RecommendationService()
request = RecommendationRequest(
user_id=1, category=BookCategory.MYSTERY, max_results=1
)
response = service.Recommend(request, None)
assert len(response.recommendations) == 1
grpc-interceptor ~= 0.11.0
grpcio-tools ~= 1.30
pytest ~= 5.4
# Python Web Applications: Deploy Your Script as a Flask App
Code snippets supplementing the [Python Web Applications: Deploy Your Script as a Flask App](https://realpython.com/python-web-applications-update/) tutorial.
## Running Locally
Create and activate a Python virtual environment:
```shell
$ python -m venv venv
$ source venv/bin/activate
```
Update `pip` and install the required dependencies:
```shell
(venv) $ pip install -U pip
(venv) $ pip install -r requirements.txt
```
Start the Flask server:
```shell
(venv) $ python main.py
* Serving Flask app "main" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 339-986-221
```
Navigate your web browser to this address: <http://127.0.0.1:8080/>
# Sample Text Editor
A **sample text editor** that shows how to use Qt Designer to create GUI applications.
## How to Run this Application
To run this application, you need to [install `PyQt5`](https://realpython.com/python-pyqt-gui-calculator/#installing-pyqt) on your Python environment. To do that, you can run the following commands in a terminal or command prompt:
```sh
$ python3 -m venv ./venv
$ source venv/bin/activate
(venv) $ pip install PyQt5
```
Once you have [PyQt](https://www.riverbankcomputing.com/static/Docs/PyQt5/) installed, you can run the application by executing the following command:
```sh
(venv) $ cd sample_editor/
(venv) $ python app.py
```
This command will launch the application, so you'll be able to experiment with it.
## About the Author
Leodanis Pozo Ramos – [@lpozo78](https://twitter.com/lpozo78) – lpozor78@gmail.com
## License
The set of icons used in this application are part of the [TurkinOS](https://github.com/llamaret/turkinos-icon) icon theme, which is distributed under the [GPL v3.0 license](https://github.com/llamaret/turkinos-icon/blob/master/LICENSE). See `ui/resources/LICENSE` for details.
\ No newline at end of file
import sys
from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow, QMessageBox
from PyQt5.uic import loadUi
from main_window_ui import Ui_MainWindow
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.connectSignalsSlots()
def connectSignalsSlots(self):
self.action_Exit.triggered.connect(self.close)
self.action_Find_Replace.triggered.connect(self.findAndReplace)
self.action_About.triggered.connect(self.about)
def findAndReplace(self):
dialog = FindReplaceDialog(self)
dialog.exec()
def about(self):
QMessageBox.about(
self,
"About Sample Editor",
"<p>A sample text editor app built with:</p>"
"<p>- PyQt</p>"
"<p>- Qt Designer</p>"
"<p>- Python</p>",
)
class FindReplaceDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
loadUi("ui/find_replace.ui", self)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/main_window.ui'
#
# Created by: PyQt5 UI code generator 5.15.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(413, 299)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setContentsMargins(1, 1, 1, 1)
self.verticalLayout.setObjectName("verticalLayout")
self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
self.textEdit.setObjectName("textEdit")
self.verticalLayout.addWidget(self.textEdit)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 413, 20))
self.menubar.setObjectName("menubar")
self.menu_File = QtWidgets.QMenu(self.menubar)
self.menu_File.setObjectName("menu_File")
self.menuOpen_Recent = QtWidgets.QMenu(self.menu_File)
self.menuOpen_Recent.setObjectName("menuOpen_Recent")
self.menu_Edit = QtWidgets.QMenu(self.menubar)
self.menu_Edit.setObjectName("menu_Edit")
self.menu_Help = QtWidgets.QMenu(self.menubar)
self.menu_Help.setObjectName("menu_Help")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.toolBar = QtWidgets.QToolBar(MainWindow)
self.toolBar.setObjectName("toolBar")
MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar)
self.action_New = QtWidgets.QAction(MainWindow)
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap("ui/resources/file-new.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_New.setIcon(icon)
self.action_New.setObjectName("action_New")
self.action_Open = QtWidgets.QAction(MainWindow)
icon1 = QtGui.QIcon()
icon1.addPixmap(
QtGui.QPixmap("ui/resources/file-open.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_Open.setIcon(icon1)
self.action_Open.setObjectName("action_Open")
self.action_Save = QtWidgets.QAction(MainWindow)
icon2 = QtGui.QIcon()
icon2.addPixmap(
QtGui.QPixmap("ui/resources/file-save.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_Save.setIcon(icon2)
self.action_Save.setObjectName("action_Save")
self.action_Exit = QtWidgets.QAction(MainWindow)
icon3 = QtGui.QIcon()
icon3.addPixmap(
QtGui.QPixmap("ui/resources/file-exit.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_Exit.setIcon(icon3)
self.action_Exit.setObjectName("action_Exit")
self.action_Copy = QtWidgets.QAction(MainWindow)
icon4 = QtGui.QIcon()
icon4.addPixmap(
QtGui.QPixmap("ui/resources/edit-copy.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_Copy.setIcon(icon4)
self.action_Copy.setObjectName("action_Copy")
self.action_Paste = QtWidgets.QAction(MainWindow)
icon5 = QtGui.QIcon()
icon5.addPixmap(
QtGui.QPixmap("ui/resources/edit-paste.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_Paste.setIcon(icon5)
self.action_Paste.setObjectName("action_Paste")
self.action_Cut = QtWidgets.QAction(MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(
QtGui.QPixmap("ui/resources/edit-cut.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_Cut.setIcon(icon6)
self.action_Cut.setObjectName("action_Cut")
self.actionOpen_All = QtWidgets.QAction(MainWindow)
self.actionOpen_All.setObjectName("actionOpen_All")
self.action_About = QtWidgets.QAction(MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(
QtGui.QPixmap("ui/resources/help-content.png"),
QtGui.QIcon.Normal,
QtGui.QIcon.Off,
)
self.action_About.setIcon(icon7)
self.action_About.setObjectName("action_About")
self.action_Find_Replace = QtWidgets.QAction(MainWindow)
self.action_Find_Replace.setObjectName("action_Find_Replace")
self.menuOpen_Recent.addAction(self.actionOpen_All)
self.menu_File.addAction(self.action_New)
self.menu_File.addSeparator()
self.menu_File.addAction(self.action_Open)
self.menu_File.addAction(self.menuOpen_Recent.menuAction())
self.menu_File.addSeparator()
self.menu_File.addAction(self.action_Save)
self.menu_File.addSeparator()
self.menu_File.addAction(self.action_Exit)
self.menu_Edit.addAction(self.action_Copy)
self.menu_Edit.addAction(self.action_Paste)
self.menu_Edit.addAction(self.action_Cut)
self.menu_Edit.addSeparator()
self.menu_Edit.addAction(self.action_Find_Replace)
self.menu_Help.addAction(self.action_About)
self.menubar.addAction(self.menu_File.menuAction())
self.menubar.addAction(self.menu_Edit.menuAction())
self.menubar.addAction(self.menu_Help.menuAction())
self.toolBar.addAction(self.action_New)
self.toolBar.addSeparator()
self.toolBar.addAction(self.action_Open)
self.toolBar.addSeparator()
self.toolBar.addAction(self.action_Save)
self.toolBar.addSeparator()
self.toolBar.addAction(self.action_Copy)
self.toolBar.addAction(self.action_Paste)
self.toolBar.addAction(self.action_Cut)
self.toolBar.addSeparator()
self.toolBar.addAction(self.action_About)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Sample Editor"))
self.menu_File.setTitle(_translate("MainWindow", "&File"))
self.menuOpen_Recent.setTitle(_translate("MainWindow", "Open &Recent"))
self.menu_Edit.setTitle(_translate("MainWindow", "&Edit"))
self.menu_Help.setTitle(_translate("MainWindow", "&Help"))
self.toolBar.setWindowTitle(_translate("MainWindow", "toolBar"))
self.action_New.setText(_translate("MainWindow", "&New..."))
self.action_New.setToolTip(
_translate("MainWindow", "Create a New Document")
)
self.action_New.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.action_Open.setText(_translate("MainWindow", "&Open..."))
self.action_Open.setToolTip(
_translate("MainWindow", "Open a Document")
)
self.action_Open.setShortcut(_translate("MainWindow", "Ctrl+O"))
self.action_Save.setText(_translate("MainWindow", "&Save"))
self.action_Save.setToolTip(
_translate("MainWindow", "Save the Current Document")
)
self.action_Save.setShortcut(_translate("MainWindow", "Ctrl+S"))
self.action_Exit.setText(_translate("MainWindow", "&Exit"))
self.action_Copy.setText(_translate("MainWindow", "&Copy"))
self.action_Copy.setToolTip(
_translate("MainWindow", "Copy Slected Text")
)
self.action_Copy.setShortcut(_translate("MainWindow", "Ctrl+C"))
self.action_Paste.setText(_translate("MainWindow", "&Paste"))
self.action_Paste.setToolTip(
_translate("MainWindow", "Paste Copied Text")
)
self.action_Paste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.action_Cut.setText(_translate("MainWindow", "C&ut"))
self.action_Cut.setToolTip(
_translate("MainWindow", "Cut Selected Text")
)
self.action_Cut.setShortcut(_translate("MainWindow", "Ctrl+X"))
self.actionOpen_All.setText(_translate("MainWindow", "Open All"))
self.actionOpen_All.setToolTip(
_translate("MainWindow", "Open All Recent Documents")
)
self.action_About.setText(_translate("MainWindow", "&About..."))
self.action_Find_Replace.setText(
_translate("MainWindow", "&Find and Replace...")
)
self.action_Find_Replace.setToolTip(
_translate("MainWindow", "Launch Find and Replace Dialog")
)
self.action_Find_Replace.setShortcut(
_translate("MainWindow", "Ctrl+F")
)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>389</width>
<height>132</height>
</rect>
</property>
<property name="windowTitle">
<string>Find and Replace</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>F&amp;ind:</string>
</property>
<property name="buddy">
<cstring>lineEdit</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Re&amp;place:</string>
</property>
<property name="buddy">
<cstring>lineEdit_2</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Match &amp;Case</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_2">
<property name="text">
<string>Match Whole &amp;Word</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>&amp;Find</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="text">
<string>&amp;Replace</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_3">
<property name="text">
<string>&amp;Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>lineEdit</tabstop>
<tabstop>lineEdit_2</tabstop>
<tabstop>pushButton</tabstop>
<tabstop>pushButton_2</tabstop>
<tabstop>checkBox</tabstop>
<tabstop>checkBox_2</tabstop>
<tabstop>pushButton_3</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>pushButton_3</sender>
<signal>clicked()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>302</x>
<y>102</y>
</hint>
<hint type="destinationlabel">
<x>328</x>
<y>67</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>413</width>
<height>299</height>
</rect>
</property>
<property name="windowTitle">
<string>Sample Editor</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>1</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>1</number>
</property>
<property name="bottomMargin">
<number>1</number>
</property>
<item>
<widget class="QTextEdit" name="textEdit"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>413</width>
<height>20</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
<property name="title">
<string>&amp;File</string>
</property>
<widget class="QMenu" name="menuOpen_Recent">
<property name="title">
<string>Open &amp;Recent</string>
</property>
<addaction name="actionOpen_All"/>
</widget>
<addaction name="action_New"/>
<addaction name="separator"/>
<addaction name="action_Open"/>
<addaction name="menuOpen_Recent"/>
<addaction name="separator"/>
<addaction name="action_Save"/>
<addaction name="separator"/>
<addaction name="action_Exit"/>
</widget>
<widget class="QMenu" name="menu_Edit">
<property name="title">
<string>&amp;Edit</string>
</property>
<addaction name="action_Copy"/>
<addaction name="action_Paste"/>
<addaction name="action_Cut"/>
<addaction name="separator"/>
<addaction name="action_Find_Replace"/>
</widget>
<widget class="QMenu" name="menu_Help">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="action_About"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menu_Edit"/>
<addaction name="menu_Help"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_New"/>
<addaction name="separator"/>
<addaction name="action_Open"/>
<addaction name="separator"/>
<addaction name="action_Save"/>
<addaction name="separator"/>
<addaction name="action_Copy"/>
<addaction name="action_Paste"/>
<addaction name="action_Cut"/>
<addaction name="separator"/>
<addaction name="action_About"/>
</widget>
<action name="action_New">
<property name="icon">
<iconset>
<normaloff>resources/file-new.png</normaloff>resources/file-new.png</iconset>
</property>
<property name="text">
<string>&amp;New...</string>
</property>
<property name="toolTip">
<string>Create a New Document</string>
</property>
<property name="shortcut">
<string>Ctrl+N</string>
</property>
</action>
<action name="action_Open">
<property name="icon">
<iconset>
<normaloff>resources/file-open.png</normaloff>resources/file-open.png</iconset>
</property>
<property name="text">
<string>&amp;Open...</string>
</property>
<property name="toolTip">
<string>Open a Document</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="action_Save">
<property name="icon">
<iconset>
<normaloff>resources/file-save.png</normaloff>resources/file-save.png</iconset>
</property>
<property name="text">
<string>&amp;Save</string>
</property>
<property name="toolTip">
<string>Save the Current Document</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="action_Exit">
<property name="icon">
<iconset>
<normaloff>resources/file-exit.png</normaloff>resources/file-exit.png</iconset>
</property>
<property name="text">
<string>&amp;Exit</string>
</property>
</action>
<action name="action_Copy">
<property name="icon">
<iconset>
<normaloff>resources/edit-copy.png</normaloff>resources/edit-copy.png</iconset>
</property>
<property name="text">
<string>&amp;Copy</string>
</property>
<property name="toolTip">
<string>Copy Slected Text</string>
</property>
<property name="shortcut">
<string>Ctrl+C</string>
</property>
</action>
<action name="action_Paste">
<property name="icon">
<iconset>
<normaloff>resources/edit-paste.png</normaloff>resources/edit-paste.png</iconset>
</property>
<property name="text">
<string>&amp;Paste</string>
</property>
<property name="toolTip">
<string>Paste Copied Text</string>
</property>
<property name="shortcut">
<string>Ctrl+V</string>
</property>
</action>
<action name="action_Cut">
<property name="icon">
<iconset>
<normaloff>resources/edit-cut.png</normaloff>resources/edit-cut.png</iconset>
</property>
<property name="text">
<string>C&amp;ut</string>
</property>
<property name="toolTip">
<string>Cut Selected Text</string>
</property>
<property name="shortcut">
<string>Ctrl+X</string>
</property>
</action>
<action name="actionOpen_All">
<property name="text">
<string>Open All</string>
</property>
<property name="toolTip">
<string>Open All Recent Documents</string>
</property>
</action>
<action name="action_About">
<property name="icon">
<iconset>
<normaloff>resources/help-content.png</normaloff>resources/help-content.png</iconset>
</property>
<property name="text">
<string>&amp;About...</string>
</property>
</action>
<action name="action_Find_Replace">
<property name="text">
<string>&amp;Find and Replace...</string>
</property>
<property name="toolTip">
<string>Launch Find and Replace Dialog</string>
</property>
<property name="shortcut">
<string>Ctrl+F</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册