├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── demos ├── appstats │ ├── README │ └── protorpc_appstats │ │ ├── __init__.py │ │ ├── appstats.descriptor │ │ └── main.py ├── echo │ ├── README │ ├── app.yaml │ ├── appengine_config.py │ ├── main.py │ └── services.py ├── experimental │ └── wsgi │ │ ├── README │ │ ├── app.yaml │ │ └── services.py ├── guestbook │ ├── README │ ├── client │ │ ├── app.yaml │ │ ├── appengine_config.py │ │ ├── guestbook.py │ │ ├── index.yaml │ │ └── main.py │ └── server │ │ ├── app.yaml │ │ ├── appengine_config.py │ │ ├── guestbook.py │ │ └── index.yaml ├── hello │ └── server │ │ ├── README │ │ ├── app.yaml │ │ ├── appengine_config.py │ │ └── services.py ├── quotas │ └── backend │ │ ├── README │ │ ├── app.yaml │ │ ├── backends.yaml │ │ ├── quotas.json │ │ └── quotas │ │ ├── __init__.py │ │ ├── main.py │ │ ├── services.py │ │ └── services_test.py └── tunes_db │ ├── README │ ├── client │ ├── album.html │ ├── album_search.html │ ├── albums.html │ ├── app.yaml │ ├── appengine_config.py │ ├── artist.html │ ├── artists.html │ ├── base.css │ ├── base.html │ ├── fetch_descriptor.py │ ├── main.py │ ├── music_service.descriptor │ └── tunes_db.py │ └── server │ ├── app.yaml │ ├── appengine_config.py │ ├── datastore_test_util.py │ ├── index.yaml │ ├── main.py │ ├── model.py │ ├── model_test.py │ ├── services.py │ ├── tunes_db.py │ └── tunes_db_test.py ├── experimental └── javascript │ ├── build.sh │ ├── closure │ ├── base.js │ ├── debug │ │ └── error.js │ ├── json.js │ ├── string │ │ └── string.js │ ├── wrapperxmlhttpfactory.js │ ├── xmlhttp.js │ └── xmlhttpfactory.js │ ├── descriptor.js │ ├── messages.js │ ├── protorpc.js │ └── util.js ├── ez_setup.py ├── gen_protorpc.py ├── protorpc ├── __init__.py ├── definition.py ├── definition_test.py ├── descriptor.py ├── descriptor_test.py ├── end2end_test.py ├── experimental │ ├── __init__.py │ └── parser │ │ ├── protobuf.g │ │ ├── protobuf_lexer.g │ │ ├── pyprotobuf.g │ │ └── test.proto ├── generate.py ├── generate_proto.py ├── generate_proto_test.py ├── generate_python.py ├── generate_python_test.py ├── generate_test.py ├── google_imports.py ├── message_types.py ├── message_types_test.py ├── messages.py ├── messages_test.py ├── non_sdk_imports.py ├── protobuf.py ├── protobuf_test.py ├── protojson.py ├── protojson_test.py ├── protorpc_test.proto ├── protorpc_test_pb2.py ├── protourlencode.py ├── protourlencode_test.py ├── registry.py ├── registry_test.py ├── remote.py ├── remote_test.py ├── static │ ├── base.html │ ├── forms.html │ ├── forms.js │ ├── jquery-1.4.2.min.js │ ├── jquery.json-2.2.min.js │ └── methods.html ├── test_util.py ├── transport.py ├── transport_test.py ├── util.py ├── util_test.py ├── webapp │ ├── __init__.py │ ├── forms.py │ ├── forms_test.py │ ├── google_imports.py │ ├── service_handlers.py │ └── service_handlers_test.py ├── webapp_test_util.py └── wsgi │ ├── __init__.py │ ├── service.py │ ├── service_test.py │ ├── util.py │ └── util_test.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # python .gitignore, from http://github.com/github/gitignore 2 | 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # Unit test / coverage reports 29 | htmlcov/ 30 | .tox/ 31 | .coverage 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | 36 | # Jetbrains IDE directories 37 | .idea/ 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.6 5 | sudo: false 6 | install: pip install tox-travis 7 | script: tox 8 | notifications: 9 | email: false 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.py 3 | include protorpc/*.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ProtoRPC 2 | -------- 3 | 4 | [![Build Status](https://travis-ci.org/google/protorpc.svg?branch=master)](https://travis-ci.org/google/protorpc) 5 | -------------------------------------------------------------------------------- /demos/appstats/README: -------------------------------------------------------------------------------- 1 | Appstats Service 2 | ================ 3 | 4 | This utility can be integrated in to an existing application that uses 5 | appstats to turn appstats in to a service that can be remotely 6 | queried. 7 | 8 | Google App Engine 9 | ================= 10 | 11 | For more information about Google App Engine, and to download the SDK, see: 12 | 13 | http://code.google.com/appengine 14 | 15 | Appstats 16 | ======== 17 | 18 | For more information about appstats, please see: 19 | 20 | http://code.google.com/appengine/docs/python/tools/appstats.html 21 | 22 | The file appstats.descriptor is a binary FileSet descriptor encoded in 23 | protocol buffer format as defined in protorpc.descriptor.FileSet. The 24 | defintions were described from yet to be published .proto file. The 25 | generated classes are the same as defined in: 26 | 27 | google.appengine.ext.appstats.datamodel_pb.py 28 | 29 | Running locally 30 | =============== 31 | 32 | ProtoRPC is packaged with the App Engine SDK, but the version shipped 33 | may not be the most current version due to the App Engine release 34 | schedule. If you'd like to use the most current version of ProtoRPC, 35 | you can install it in the appstats directory. 36 | 37 | For example on a unix-like OS, you could either copy: 38 | 39 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/appstats 40 | 41 | or symlink the directory: 42 | 43 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/appstats/protorpc 44 | 45 | To run this demo locally, you need to run an instance of the Google App 46 | Engine dev-appserver. The server defaults on port 8080. 47 | 48 | Example on a unix-like OS: 49 | 50 | $ python $GAE_SDK/dev_appserver.py $PROTORPC/demos/appstats 51 | -------------------------------------------------------------------------------- /demos/appstats/protorpc_appstats/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import cStringIO 19 | import logging 20 | import os 21 | 22 | from protorpc import descriptor 23 | from protorpc import messages 24 | from protorpc import protobuf 25 | from protorpc import remote 26 | from protorpc import stub 27 | 28 | from google.appengine.api import memcache 29 | from google.appengine.ext.appstats import recording 30 | 31 | 32 | # Import contents of appstats.descriptor in to this module from binary appstats 33 | # protobuf descriptor. Definitions are imported into module apphosting. 34 | stub.import_file_set(os.path.join(os.path.dirname(__file__), 35 | 'appstats.descriptor')) 36 | import apphosting 37 | 38 | 39 | class Summary(messages.Message): 40 | """Response for AppStatsService.get_summary. 41 | 42 | Fields: 43 | stats: List of RequestStatProto objects summarizing application activity. 44 | """ 45 | 46 | stats = messages.MessageField(apphosting.RequestStatProto, 1, repeated=True) 47 | 48 | 49 | class GetDetailsRequest(messages.Message): 50 | """Request for AppStatsService.get_details. 51 | 52 | Fields: 53 | timestamp: Timestamp of appstats detail to retrieve. 54 | """ 55 | 56 | timestamp = messages.IntegerField(1, required=True) 57 | 58 | 59 | class Details(messages.Message): 60 | """Response for AppStatsService.get_details. 61 | 62 | Fields: 63 | stat: Individual stat details if found, else None. 64 | """ 65 | 66 | stat = messages.MessageField(apphosting.RequestStatProto, 1) 67 | 68 | 69 | # TODO(rafek): Remove this function when recording.load_summary_protos is 70 | # refactored in the App Engine SDK. 71 | def load_summary_protos(): 72 | """Load all valid summary records from memcache. 73 | 74 | Returns: 75 | A list of RequestStatProto instances, in reverse chronological order 76 | (i.e. most recent first). 77 | 78 | NOTE: This is limited to returning at most config.KEY_MODULUS records, 79 | since there are only that many distinct keys. See also make_key(). 80 | """ 81 | tmpl = '%s%s%s' % (recording.config.KEY_PREFIX, 82 | recording.config.KEY_TEMPLATE, 83 | recording.config.PART_SUFFIX) 84 | keys = [tmpl % i 85 | for i in 86 | range(0, recording.config.KEY_DISTANCE * recording.config.KEY_MODULUS, 87 | recording.config.KEY_DISTANCE)] 88 | results = memcache.get_multi(keys, namespace=recording.config.KEY_NAMESPACE) 89 | records = [] 90 | for rec in results.itervalues(): 91 | try: 92 | pb = protobuf.decode_message(apphosting.RequestStatProto, rec) 93 | except Exception, err: 94 | logging.warn('Bad record: %s', err) 95 | else: 96 | records.append(pb) 97 | logging.info('Loaded %d raw records, %d valid', len(results), len(records)) 98 | # Sorts by time, newest first. 99 | records.sort(key=lambda pb: -pb.start_timestamp_milliseconds) 100 | return records 101 | 102 | 103 | # TODO(rafek): Remove this function when recording.load_full_protos is 104 | # refactored in the App Engine SDK. 105 | def load_full_proto(timestamp): 106 | """Load the full record for a given timestamp. 107 | 108 | Args: 109 | timestamp: The start_timestamp of the record, as a float in seconds 110 | (see make_key() for details). 111 | 112 | Returns: 113 | A RequestStatProto instance if the record exists and can be loaded; 114 | None otherwise. 115 | """ 116 | full_key = recording.make_key(timestamp) + recording.config.FULL_SUFFIX 117 | full_binary = memcache.get(full_key, namespace=recording.config.KEY_NAMESPACE) 118 | if full_binary is None: 119 | logging.info('No full record at %s', full_key) 120 | return None 121 | try: 122 | full = protobuf.decode_message(apphosting.RequestStatProto, full_binary) 123 | except Exception, err: 124 | logging.warn('Bad full record at %s: %s', full_key, err) 125 | return None 126 | if full.start_timestamp_milliseconds != int(timestamp * 1000): 127 | logging.warn('Hash collision, record at %d has timestamp %d', 128 | int(timestamp * 1000), full.start_timestamp_milliseconds) 129 | return None # Hash collision -- the requested record no longer exists. 130 | return full 131 | 132 | 133 | class AppStatsService(remote.Service): 134 | """Service for getting access to AppStats data.""" 135 | 136 | @remote.method(response_type=Summary) 137 | def get_summary(self, request): 138 | """Get appstats summary.""" 139 | response = Summary() 140 | 141 | response.stats = load_summary_protos() 142 | 143 | return response 144 | 145 | @remote.method(GetDetailsRequest, Details) 146 | def get_details(self, request): 147 | """Get appstats details for a particular timestamp.""" 148 | response = Details() 149 | recording_timestamp = request.timestamp * 0.001 150 | logging.error('Fetching recording from %f', recording_timestamp) 151 | response.stat = load_full_proto(recording_timestamp) 152 | return response 153 | -------------------------------------------------------------------------------- /demos/appstats/protorpc_appstats/appstats.descriptor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/protorpc/95849c9a3e414b9ba2d3da91fd850156348fe558/demos/appstats/protorpc_appstats/appstats.descriptor -------------------------------------------------------------------------------- /demos/appstats/protorpc_appstats/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import re 20 | 21 | from google.appengine.ext import webapp 22 | from google.appengine.ext.webapp import util 23 | 24 | from protorpc.webapp import service_handlers 25 | 26 | import protorpc_appstats 27 | 28 | # This regular expression is used to extract the full path of 29 | # an incoming request so that the service can be correctly 30 | # registered with its internal registry. The only assumption 31 | # that is made about the placement of the appstats service by 32 | # this main module is that the last element of the service path 33 | # is 'service'. 34 | _METHOD_REGEX = r'\.[^/]+' 35 | _SERVICE_PATH_REGEX = re.compile(r'(.*)/service(%s|/protorpc(%s)?)?' % 36 | (_METHOD_REGEX, _METHOD_REGEX)) 37 | 38 | 39 | def parse_service_path(path_info): 40 | """Parse the service path from PATH_INFO in the environment. 41 | 42 | The appstats service may be placed at any URL path within a webapp 43 | application. It isn't possible to know what the actual path is 44 | until the actual request time. This function attempts to parse 45 | the incoming request to determine where the appstats service is 46 | configured. If it can successfully determine its location, it 47 | will attempt to map protorpc RegistryService underneath its service 48 | path. 49 | 50 | The appstats service is always expected to be /service. The 51 | RegistryService is mapped to /service/protorpc. 52 | 53 | Args: 54 | path_info: PATH_INFO extracted from the CGI environment. 55 | 56 | Returns: 57 | A pair paths (appstats_service_path, registry_service_path): 58 | appstats_service_path: The full path of the appstats service. If the 59 | full path cannot be determined this will be '.*/service'. 60 | registry_service_path: The full path of the appstats registry service. 61 | If the path of the appstats service cannot be determined this will be 62 | None. 63 | """ 64 | match = _SERVICE_PATH_REGEX.match(path_info) 65 | if match: 66 | appstats_service_path = '%s/service' % (match.group(1),) 67 | # Creates a "local" registry service. 68 | registry_service_path = '%s/protorpc' % (appstats_service_path,) 69 | else: 70 | # Not possible to determine full service name for registry. Do 71 | # not create registry for service. 72 | appstats_service_path = '.*/service' 73 | registry_service_path = None 74 | return appstats_service_path, registry_service_path 75 | 76 | 77 | def main(): 78 | path_info = os.environ.get('PATH_INFO', '') 79 | service_path, registry_path = parse_service_path(path_info) 80 | 81 | # Create webapp URL mappings for service and private registry. 82 | mapping = service_handlers.service_mapping( 83 | [(service_path, protorpc_appstats.AppStatsService)], registry_path) 84 | 85 | application = webapp.WSGIApplication(mapping) 86 | util.run_wsgi_app(application) 87 | 88 | 89 | if __name__ == '__main__': 90 | main() 91 | -------------------------------------------------------------------------------- /demos/echo/README: -------------------------------------------------------------------------------- 1 | Echo Service 2 | ============ 3 | 4 | The echo service is a service that echos a request back to the caller. Useful 5 | for testing and illustrates all data types in the forms interface. 6 | 7 | Google App Engine 8 | ================= 9 | 10 | For more information about Google App Engine, and to download the SDK, see: 11 | 12 | http://code.google.com/appengine 13 | 14 | Running locally 15 | =============== 16 | 17 | ProtoRPC is packaged with the App Engine SDK, but the version shipped 18 | may not be the most current version due to the App Engine release 19 | schedule. If you'd like to use the most current version of ProtoRPC, 20 | you can install it in the echo directory. 21 | 22 | For example on a unix-like OS, you could either copy: 23 | 24 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/echo 25 | 26 | or symlink the directory: 27 | 28 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/echo/protorpc 29 | 30 | To run this demo locally, you need to run an instance of the Google App 31 | Engine dev-appserver. The server defaults on port 8080. 32 | 33 | Example on a unix-like OS: 34 | 35 | $ python $GAE_SDK/dev_appserver.py $PROTORPC/demos/echo 36 | -------------------------------------------------------------------------------- /demos/echo/app.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | application: echo-service-demo 19 | version: 1 20 | api_version: 1 21 | runtime: python 22 | 23 | handlers: 24 | 25 | - url: / 26 | script: main.py 27 | 28 | - url: /.+ 29 | script: services.py 30 | 31 | -------------------------------------------------------------------------------- /demos/echo/appengine_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import sys 20 | 21 | sys.path.insert(0, os.path.dirname(__file__)) 22 | -------------------------------------------------------------------------------- /demos/echo/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | __author__ = 'rafek@google.com (Rafe Kaplan)' 19 | 20 | import appengine_config 21 | 22 | from google.appengine.ext import webapp 23 | from google.appengine.ext.webapp import util 24 | 25 | 26 | application = webapp.WSGIApplication( 27 | [('/', webapp.RedirectHandler.new_factory('/protorpc/form'))], 28 | debug=True) 29 | 30 | 31 | def main(): 32 | util.run_wsgi_app(application) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /demos/echo/services.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Echo service demo. 19 | 20 | Implements a simple echo service. The request and response objects are 21 | the same message that contains numerous different fields useful for testing and 22 | illustrating the forms interface. 23 | """ 24 | 25 | __author__ = 'rafek@google.com (Rafe Kaplan)' 26 | 27 | import appengine_config 28 | 29 | import logging 30 | import time 31 | 32 | from protorpc import messages 33 | from protorpc import remote 34 | from protorpc.webapp import service_handlers 35 | 36 | package = 'protorpc.echo' 37 | 38 | 39 | class SubMessage(messages.Message): 40 | """A sub-message that can be required by EchoData.""" 41 | 42 | value = messages.StringField(1) 43 | 44 | 45 | class EchoData(messages.Message): 46 | """Echo message. 47 | 48 | Contains all relevant ProtoRPC data-types including recursive reference 49 | to itself in nested and repeated form. 50 | """ 51 | 52 | class Color(messages.Enum): 53 | """A simple enumeration type.""" 54 | 55 | RED = 1 56 | GREEN = 2 57 | BLUE = 3 58 | 59 | # A required field with a default. 60 | required = messages.EnumField(Color, 1, 61 | required=True, 62 | default=Color.BLUE) 63 | required_message = messages.MessageField(SubMessage, 18, required=True) 64 | 65 | # Optional fields. 66 | a_string = messages.StringField(2) 67 | an_int = messages.IntegerField(3) 68 | a_float = messages.FloatField(4) 69 | a_bool = messages.BooleanField(5) 70 | a_bytes = messages.BytesField(6) 71 | a_color = messages.EnumField(Color, 7) 72 | an_echo = messages.MessageField('EchoData', 8) 73 | 74 | # Repeated fields. 75 | strings = messages.StringField(9, repeated=True) 76 | ints = messages.IntegerField(10, repeated=True) 77 | floats = messages.FloatField(11, repeated=True) 78 | bools = messages.BooleanField(12, repeated=True); 79 | bytes = messages.BytesField(13, repeated=True) 80 | colors = messages.EnumField(Color, 14, repeated=True) 81 | echos = messages.MessageField('EchoData', 15, repeated=True) 82 | 83 | # With defaults 84 | default_string = messages.StringField(19, default='a default') 85 | default_int = messages.IntegerField(20, default=30) 86 | default_float = messages.FloatField(21, default=3.1415) 87 | default_bool = messages.BooleanField(22, default=True) 88 | default_bytes = messages.BytesField(23, default='YSBieXRlcw==') 89 | default_color = messages.EnumField(Color, 24, default=Color.GREEN) 90 | 91 | # If want_time is set to True, the response will contain current seconds 92 | # since epoch. 93 | want_time = messages.BooleanField(16) 94 | time = messages.IntegerField(17) 95 | 96 | 97 | class EchoService(remote.Service): 98 | """Echo service echos response to client.""" 99 | 100 | @remote.method(EchoData, EchoData) 101 | def echo(self, request): 102 | """Echo method.""" 103 | logging.info('\n'.join( 104 | ['Received request:', 105 | ' Host = %s' % self.request_state.remote_host, 106 | ' IP Address = %s' % self.request_state.remote_address, 107 | ])) 108 | if request.want_time: 109 | request.time = int(time.time()) 110 | return request 111 | 112 | 113 | def main(): 114 | service_handlers.run_services( 115 | [('/echo', EchoService), 116 | ]) 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /demos/experimental/wsgi/README: -------------------------------------------------------------------------------- 1 | Experimental WSGI Service 2 | ========================= 3 | 4 | Simple Demo for testing experimental WSGI Code. 5 | 6 | Google App Engine 7 | ================= 8 | 9 | For more information about Google App Engine, and to download the SDK, see: 10 | 11 | http://code.google.com/appengine 12 | 13 | Running locally 14 | =============== 15 | 16 | ProtoRPC is packaged with the App Engine SDK, but the version shipped 17 | may not be the most current version due to the App Engine release 18 | schedule. If you'd like to use the most current version of ProtoRPC, 19 | you can install it in the experimental/wsgi directory. 20 | 21 | For example on a unix-like OS, you could either copy: 22 | 23 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/experimental/wsgi 24 | 25 | or symlink the directory: 26 | 27 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/experimental/wsgi/protorpc 28 | 29 | To run this demo locally, you need to run an instance of the Google App 30 | Engine dev-appserver. The server defaults on port 8080. 31 | 32 | Example on a unix-like OS: 33 | 34 | $ python $GAE_SDK/dev_appserver.py $PROTORPC/demos/experimental/wsgi 35 | -------------------------------------------------------------------------------- /demos/experimental/wsgi/app.yaml: -------------------------------------------------------------------------------- 1 | application: experimental-wsgi 2 | version: 1 3 | api_version: 1 4 | runtime: python 5 | 6 | handlers: 7 | 8 | - url: /protorpc.* 9 | script: services.py 10 | -------------------------------------------------------------------------------- /demos/experimental/wsgi/services.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext.webapp import util 2 | 3 | from protorpc.experimental import wsgi_service 4 | from protorpc.experimental import util as wsgi_util 5 | from protorpc import protobuf 6 | from protorpc import protojson 7 | 8 | from protorpc import registry 9 | 10 | protocols = wsgi_util.Protocols() 11 | protocols.add_protocol(protobuf, 'protobuf') 12 | protocols.add_protocol(protojson, 'json') 13 | 14 | reg = {'/protorpc': registry.RegistryService} 15 | registry_service = registry.RegistryService.new_factory(reg) 16 | application = wsgi_service.service_app(registry_service, 17 | '/protorpc', 18 | protocols=protocols) 19 | 20 | 21 | def main(): 22 | util.run_bare_wsgi_app(application) 23 | 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /demos/guestbook/README: -------------------------------------------------------------------------------- 1 | Guestbook Service 2 | ================= 3 | 4 | This sample contains two sample ProtoRPC services that allow users to 5 | sign a guestbook. The two applications act as client and server. 6 | 7 | Google App Engine 8 | ================= 9 | 10 | For more information about Google App Engine, and to download the SDK, see: 11 | 12 | http://code.google.com/appengine 13 | 14 | Running locally 15 | =============== 16 | 17 | ProtoRPC is packaged with the App Engine SDK, but the version shipped 18 | may not be the most current version due to the App Engine release 19 | schedule. If you'd like to use the most current version of ProtoRPC, 20 | you can install it in the guestbook/client and guestbook/server 21 | directories. 22 | 23 | For example on a unix-like OS, you could either copy: 24 | 25 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/guestbook/client 26 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/guestbook/server 27 | 28 | or symlink the directory: 29 | 30 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/guestbook/client/protorpc 31 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/guestbook/server/protorpc 32 | 33 | To run this demo locally, you need to run an instance of the Google App 34 | Engine dev-appserver both for the client and the server. The dev-appserver 35 | defaults on port 8080, so use different ports to avoid a port collision. 36 | 37 | Example on a unix-like OS: 38 | 39 | $ python $GAE_SDK/dev_appserver.py --port=8080 $PROTORPC/demos/guestbook/client & 40 | $ python $GAE_SDK/dev_appserver.py --port=8081 $PROTORPC/demos/guestbook/server 41 | -------------------------------------------------------------------------------- /demos/guestbook/client/app.yaml: -------------------------------------------------------------------------------- 1 | application: postservice-client 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: .* 8 | script: main.py 9 | -------------------------------------------------------------------------------- /demos/guestbook/client/appengine_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import sys 20 | 21 | sys.path.insert(0, os.path.dirname(__file__)) 22 | -------------------------------------------------------------------------------- /demos/guestbook/client/guestbook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from protorpc import messages 19 | from protorpc import remote 20 | package = 'guestbook' 21 | 22 | 23 | class GetNotesRequest(messages.Message): 24 | 25 | 26 | class Order(messages.Enum): 27 | 28 | WHEN = 1 29 | TEXT = 2 30 | 31 | limit = messages.IntegerField(1, default=10) 32 | on_or_before = messages.IntegerField(2) 33 | order = messages.EnumField('guestbook.GetNotesRequest.Order', 3, default=1) 34 | 35 | 36 | class Note(messages.Message): 37 | 38 | text = messages.StringField(1, required=True) 39 | when = messages.IntegerField(2) 40 | 41 | 42 | class Notes(messages.Message): 43 | 44 | notes = messages.MessageField('guestbook.Note', 1, repeated=True) 45 | 46 | 47 | class PostService(remote.Service): 48 | 49 | @remote.method('guestbook.GetNotesRequest', 'guestbook.Notes') 50 | def get_notes(self, request): 51 | raise NotImplementedError('Method get_notes is not implemented') 52 | 53 | @remote.method('guestbook.Note', 'protorpc.message_types.VoidMessage') 54 | def post_note(self, request): 55 | raise NotImplementedError('Method post_note is not implemented') 56 | -------------------------------------------------------------------------------- /demos/guestbook/client/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | -------------------------------------------------------------------------------- /demos/guestbook/client/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import appengine_config 19 | 20 | import os 21 | 22 | from google.appengine.ext import webapp 23 | from google.appengine.ext.webapp import util 24 | 25 | from protorpc import transport 26 | from protorpc import protojson 27 | 28 | import guestbook 29 | 30 | 31 | postservice = guestbook.PostService.Stub( 32 | transport.HttpTransport('http://postservice-demo.appspot.com/postservice')) 33 | 34 | 35 | class MainHandler(webapp.RequestHandler): 36 | def get(self): 37 | notes = postservice.get_notes(limit=10) 38 | self.response.out.write('Last %d posts...' % len(notes.notes)) 39 | for note in notes.notes: 40 | self.response.out.write('

%s' % note.text) 41 | 42 | 43 | def main(): 44 | application = webapp.WSGIApplication([('/', MainHandler)], 45 | debug=True) 46 | util.run_wsgi_app(application) 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /demos/guestbook/server/app.yaml: -------------------------------------------------------------------------------- 1 | application: postservice-demo 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | 8 | - url: .* 9 | script: guestbook.py 10 | -------------------------------------------------------------------------------- /demos/guestbook/server/appengine_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import sys 20 | 21 | sys.path.insert(0, os.path.dirname(__file__)) 22 | -------------------------------------------------------------------------------- /demos/guestbook/server/guestbook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import appengine_config 19 | 20 | import cgi 21 | import datetime 22 | import time 23 | import wsgiref.handlers 24 | 25 | from google.appengine.ext import db 26 | from google.appengine.api import users 27 | from google.appengine.ext import webapp 28 | 29 | from protorpc import message_types 30 | from protorpc import messages 31 | from protorpc import remote 32 | from protorpc.webapp import service_handlers 33 | 34 | 35 | class Greeting(db.Model): 36 | author = db.UserProperty() 37 | content = db.StringProperty(multiline=True) 38 | date = db.DateTimeProperty(auto_now_add=True) 39 | 40 | 41 | class MainPage(webapp.RequestHandler): 42 | def get(self): 43 | self.response.out.write('') 44 | 45 | greetings = db.GqlQuery("SELECT * " 46 | "FROM Greeting " 47 | "ORDER BY date DESC LIMIT 10") 48 | 49 | for greeting in greetings: 50 | if greeting.author: 51 | self.response.out.write('%s wrote:' % greeting.author.nickname()) 52 | else: 53 | self.response.out.write('An anonymous person wrote:') 54 | self.response.out.write('

%s
' % 55 | cgi.escape(greeting.content)) 56 | 57 | 58 | self.response.out.write(""" 59 |
60 |
61 |
62 |
63 | 64 | """) 65 | 66 | 67 | class Guestbook(webapp.RequestHandler): 68 | def post(self): 69 | greeting = Greeting() 70 | 71 | if users.get_current_user(): 72 | greeting.author = users.get_current_user() 73 | 74 | greeting.content = self.request.get('content') 75 | greeting.put() 76 | self.redirect('/') 77 | 78 | 79 | class Note(messages.Message): 80 | 81 | text = messages.StringField(1, required=True) 82 | when = messages.IntegerField(2) 83 | 84 | 85 | class GetNotesRequest(messages.Message): 86 | 87 | limit = messages.IntegerField(1, default=10) 88 | on_or_before = messages.IntegerField(2) 89 | 90 | class Order(messages.Enum): 91 | WHEN = 1 92 | TEXT = 2 93 | order = messages.EnumField(Order, 3, default=Order.WHEN) 94 | 95 | 96 | class Notes(messages.Message): 97 | notes = messages.MessageField(Note, 1, repeated=True) 98 | 99 | 100 | class PostService(remote.Service): 101 | 102 | # Add the remote decorator to indicate the service methods 103 | @remote.method(Note) 104 | def post_note(self, request): 105 | 106 | # If the Note instance has a timestamp, use that timestamp 107 | if request.when is not None: 108 | when = datetime.datetime.utcfromtimestamp(request.when) 109 | 110 | # Else use the current time 111 | else: 112 | when = datetime.datetime.now() 113 | note = Greeting(content=request.text, date=when) 114 | note.put() 115 | return message_types.VoidMessage() 116 | 117 | @remote.method(GetNotesRequest, Notes) 118 | def get_notes(self, request): 119 | query = Greeting.all().order('-date') 120 | 121 | if request.on_or_before: 122 | when = datetime.datetime.utcfromtimestamp( 123 | request.on_or_before) 124 | query.filter('date <=', when) 125 | 126 | notes = [] 127 | for note_model in query.fetch(request.limit): 128 | if note_model.date: 129 | when = int(time.mktime(note_model.date.utctimetuple())) 130 | else: 131 | when = None 132 | note = Note(text=note_model.content, when=when) 133 | notes.append(note) 134 | 135 | if request.order == GetNotesRequest.Order.TEXT: 136 | notes.sort(key=lambda note: note.text) 137 | 138 | return Notes(notes=notes) 139 | 140 | 141 | service_mapping = service_handlers.service_mapping( 142 | [('/postservice', PostService)]) 143 | 144 | application = webapp.WSGIApplication([ 145 | ('/', MainPage), 146 | ('/sign', Guestbook), 147 | ] + service_mapping, 148 | debug=True) 149 | 150 | 151 | def main(): 152 | wsgiref.handlers.CGIHandler().run(application) 153 | 154 | 155 | if __name__ == '__main__': 156 | main() 157 | -------------------------------------------------------------------------------- /demos/guestbook/server/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | -------------------------------------------------------------------------------- /demos/hello/server/README: -------------------------------------------------------------------------------- 1 | Hello Service 2 | ============= 3 | 4 | This is a simple service which asks for a name in the request and 5 | responds with a hello. 6 | 7 | Google App Engine 8 | ================= 9 | 10 | For more information about Google App Engine, and to download the SDK, see: 11 | 12 | http://code.google.com/appengine 13 | 14 | Running locally 15 | =============== 16 | 17 | ProtoRPC is packaged with the App Engine SDK, but the version shipped 18 | may not be the most current version due to the App Engine release 19 | schedule. If you'd like to use the most current version of ProtoRPC, 20 | you can install it in the hello/server directory. 21 | 22 | For example on a unix-like OS, you could either copy: 23 | 24 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/hello/server 25 | 26 | or symlink the directory: 27 | 28 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/hello/server/protorpc 29 | 30 | To run this demo locally, you need to run an instance of the Google App 31 | Engine dev-appserver. The server defaults on port 8080. 32 | 33 | Example on a unix-like OS: 34 | 35 | $ python $GAE_SDK/dev_appserver.py $PROTORPC/demos/hello/server 36 | -------------------------------------------------------------------------------- /demos/hello/server/app.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | application: hello-service-demo 19 | version: 1 20 | api_version: 1 21 | runtime: python 22 | 23 | handlers: 24 | 25 | - url: .* 26 | script: services.py 27 | 28 | -------------------------------------------------------------------------------- /demos/hello/server/appengine_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import sys 20 | 21 | sys.path.insert(0, os.path.dirname(__file__)) 22 | -------------------------------------------------------------------------------- /demos/hello/server/services.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import appengine_config 19 | 20 | from protorpc import messages 21 | from protorpc import remote 22 | from protorpc.webapp import service_handlers 23 | 24 | 25 | class HelloRequest(messages.Message): 26 | 27 | my_name = messages.StringField(1, required=True) 28 | 29 | 30 | class HelloResponse(messages.Message): 31 | 32 | hello = messages.StringField(1, required=True) 33 | 34 | 35 | class HelloService(remote.Service): 36 | 37 | @remote.method(HelloRequest, HelloResponse) 38 | def hello(self, request): 39 | return HelloResponse(hello='Hello there, %s!' % request.my_name) 40 | 41 | 42 | def main(): 43 | service_handlers.run_services([('/hello', HelloService)]) 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /demos/quotas/backend/README: -------------------------------------------------------------------------------- 1 | Quota Service 2 | ============= 3 | 4 | It is possible to set up a quota service in your own application by following 5 | these steps: 6 | 7 | 1) Make a copy of the contents of this directory, including subdirectories 8 | for use as a custom backend of any client application that will use it. 9 | 10 | 2) Modify app.yaml: 11 | a. Change the application to the name of your application. 12 | b. Change the version to one that is appropriate for your application. 13 | 14 | 3) Edit quotas.js so that its settings are appropriate for your application. 15 | The format of the quotas.js file is a JSON encoded ProtoRPC message 16 | as defined by quotas.service.QuotaConfig of this demo. 17 | 18 | TODO: Define an easy to use client library. 19 | -------------------------------------------------------------------------------- /demos/quotas/backend/app.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | application: quota-server-demo 19 | version: 1 20 | api_version: 1 21 | runtime: python 22 | 23 | handlers: 24 | 25 | - url: /protorpc.* 26 | script: quotas/main.py 27 | 28 | - url: /quota-service.* 29 | script: quotas/main.py 30 | 31 | 32 | -------------------------------------------------------------------------------- /demos/quotas/backend/backends.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | backends: 19 | 20 | - name: quota-server 21 | class: B1 22 | instances: 1 23 | start: /quota-service/start 24 | options: dynamic 25 | 26 | 27 | -------------------------------------------------------------------------------- /demos/quotas/backend/quotas.json: -------------------------------------------------------------------------------- 1 | { 2 | "buckets": [ 3 | { "name": "DISK", "initial_tokens": 1000000 }, 4 | { "name": "EMAILS", "initial_tokens": 20, "refresh_every": 3600 } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /demos/quotas/backend/quotas/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demos/quotas/backend/quotas/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Quota service main handler. 19 | 20 | Hosts a QuotaServer ProtoRPC service. This application is intended for use 21 | as a backend server only. The service is configured based on a 22 | configuration file loaded from a quotas.json in the backend application 23 | directory. The quotas.json file is a JSON encoded 24 | quotas.service.QuotaConfig ProtoRPC message. 25 | """ 26 | 27 | from __future__ import with_statement 28 | 29 | import os 30 | import logging 31 | 32 | from google.appengine.ext import webapp 33 | from google.appengine.ext.webapp import util 34 | 35 | from protorpc import protojson 36 | from protorpc.webapp import service_handlers 37 | 38 | from quotas import services 39 | 40 | APP_DIR = os.path.dirname(os.path.dirname(os.environ['PATH_TRANSLATED'])) 41 | QUOTA_CONFIG_PATH = os.path.join(APP_DIR, 'quotas.json') 42 | 43 | 44 | def load_quota_config(quota_config_path=QUOTA_CONFIG_PATH): 45 | """Load quota configuration from from file. 46 | 47 | Args: 48 | quota_config_path: Quota to configuration file. Contents of file must be 49 | a JSON encoded ProtoRPC message of the format defined by 50 | quota.services.QuotaConfig. 51 | 52 | Returns: 53 | quota.services.QuotaConfig instance with contents parsed from quota file. 54 | """ 55 | with open(quota_config_path) as quota_config_file: 56 | encoded_config = quota_config_file.read() 57 | return protojson.decode_message(services.QuotaConfig, encoded_config) 58 | 59 | service_mappings = service_handlers.service_mapping( 60 | [ 61 | ('/quota-service', 62 | services.QuotaService.new_factory(load_quota_config(), {})), 63 | ]) 64 | 65 | application = webapp.WSGIApplication(service_mappings, debug=True) 66 | 67 | 68 | def main(): 69 | util.run_wsgi_app(application) 70 | 71 | 72 | if __name__ == '__main__': 73 | main() 74 | -------------------------------------------------------------------------------- /demos/tunes_db/README: -------------------------------------------------------------------------------- 1 | Tunes DB 2 | ======== 3 | 4 | server: Tunes DB webapp is a ProtoRPC implementation of a music library. 5 | 6 | client: A webapp that is a Tunes DB client and user interface. Includes utility 7 | for retrieving the Tunes DB service descriptor. 8 | 9 | Google App Engine 10 | ================= 11 | 12 | For more information about Google App Engine, and to download the SDK, see: 13 | 14 | http://code.google.com/appengine 15 | 16 | Running locally 17 | =============== 18 | 19 | ProtoRPC is packaged with the App Engine SDK, but the version shipped 20 | may not be the most current version due to the App Engine release 21 | schedule. If you'd like to use the most current version of ProtoRPC, 22 | you can install it in the tunes_db/client and tunes_db/server 23 | directories. 24 | 25 | For example on a unix-like OS, you could either copy: 26 | 27 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/tunes_db/client 28 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/tunes_db/server 29 | 30 | or symlink the directory: 31 | 32 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/tunes_db/client/protorpc 33 | $ ln -s $PROTORPC/python/protorpc $PROTORPC/demos/tunes_db/server/protorpc 34 | 35 | 36 | Example on a unix-like OS: 37 | 38 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/tunes_db/client 39 | $ cp -r $PROTORPC/python/protorpc $PROTORPC/demos/tunes_db/server 40 | 41 | To run this demo locally, you need to run two instances of the Google App 42 | Engine dev-appserver, one for the server and the other for the client. The 43 | server must be run on port 8082. 44 | 45 | Example on a unix-like OS: 46 | 47 | $ python $GAE_SDK/dev_appserver.py --port 8080 $PROTORPC/demos/tunes_db/client & 48 | $ python $GAE_SDK/dev_appserver.py --port 8082 $PROTORPC/demos/tunes_db/server 49 | -------------------------------------------------------------------------------- /demos/tunes_db/client/album.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | {% extends "base.html" %} 18 | 19 | {% block title %}Album - {{album.name}}{% endblock %} 20 | 21 | {% block body %} 22 | 23 |
24 | Album:
25 |
by {{artist.name}}
26 | Released:
27 | 28 | 29 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /demos/tunes_db/client/album_search.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | {% extends "base.html" %} 18 | 19 | {% block title %}Albums{% endblock %} 20 | 21 | {% block body %} 22 | 23 | {% block top %}{% endblock %} 24 | 25 |
26 | 27 | {% if artist %} 28 | 29 | {% endif %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% if not artist %} 37 | 38 | {% endif %} 39 | 40 | 41 | 42 | 43 | {% for album in albums %} 44 | 45 | 50 | 51 | {% if not artist %} 52 | 55 | {% endif %} 56 | 57 | 58 | {% endfor %} 59 | 60 |
AlbumArtistReleased
46 | 49 | {{album.name}} 53 | {{album.artist_id}} 54 | {% if album.released %}{{album.released}}{% endif %}
61 |
62 | 63 | {% block bottom %}{% endblock %} 64 | 65 | {% endblock %} 66 | 67 | -------------------------------------------------------------------------------- /demos/tunes_db/client/albums.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | {% extends "album_search.html" %} 18 | 19 | {% block top %} 20 | 21 |
22 | 23 | 24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /demos/tunes_db/client/app.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | application: tunes-db-client 19 | version: 1 20 | api_version: 1 21 | runtime: python 22 | 23 | handlers: 24 | 25 | - url: /(.*\.(png|css|js)) 26 | static_files: \1 27 | upload: (.*\.(png|css|js)) 28 | 29 | - url: /.* 30 | script: main.py 31 | 32 | -------------------------------------------------------------------------------- /demos/tunes_db/client/appengine_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import sys 20 | 21 | sys.path.insert(0, os.path.dirname(__file__)) 22 | -------------------------------------------------------------------------------- /demos/tunes_db/client/artist.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | {% extends "album_search.html" %} 18 | 19 | {% block title %}Artist - {{artist.name}}{% endblock %} 20 | 21 | {% block top %} 22 | 23 |
24 | Artist:
25 | 26 | 27 |
28 | 29 | {% endblock %} 30 | 31 | 32 | {% block bottom %} 33 | 34 |
35 | New Album: 36 | Released: 37 | 38 | 39 |
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /demos/tunes_db/client/artists.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | {% extends "base.html" %} 18 | 19 | {% block title %}Artists{% endblock %} 20 | 21 | {% block body %} 22 | 23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for artist in artists %} 43 | 44 | 49 | 54 | 55 | 56 | {% endfor %} 57 | 58 |
ArtistAlbums
45 | 48 | 50 | 51 | {{artist.name}} 52 | 53 | {{artist.album_count}}
59 |
60 | 61 |
62 | New Artist: 63 | 64 | 65 | 66 |
67 | 68 | {% endblock %} 69 | 70 | -------------------------------------------------------------------------------- /demos/tunes_db/client/base.css: -------------------------------------------------------------------------------- 1 | 16 | 17 | thead { 18 | background: lightgrey; 19 | } 20 | 21 | .error_message { 22 | color: red; 23 | text-indent: 0.25in; 24 | } 25 | -------------------------------------------------------------------------------- /demos/tunes_db/client/base.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | Tunes DB - {% block title %}No Title{% endblock %} 21 | 24 | 25 | 26 | 27 | 31 |
32 | {% if error_message %} 33 |
{{error_message}}
34 | {% endif %} 35 | {% block body %} 36 | Missing body! 37 | {% endblock %} 38 | 39 | {% if nav_action %} 40 | 49 | {% endif %} 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demos/tunes_db/client/fetch_descriptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Boot-strap development by fetching package set from service. 19 | 20 | This script fetches a protobuf encoded FileSet from get_file_set 21 | method of a service. 22 | """ 23 | 24 | __author__ = 'rafek@google.com (Rafe Kaplan)' 25 | 26 | import optparse 27 | import os 28 | import sys 29 | import urllib2 30 | 31 | from protorpc import protobuf 32 | from protorpc import protojson 33 | from protorpc import registry 34 | from protorpc import transport 35 | 36 | 37 | def parse_options(argv): 38 | """Parse options. 39 | 40 | Args: 41 | argv: List of original unparsed options. 42 | 43 | Returns: 44 | Options object as parsed by optparse. 45 | """ 46 | program = os.path.split(__file__)[-1] 47 | parser = optparse.OptionParser(usage='%s [options]' % program) 48 | 49 | parser.add_option('-o', '--output', 50 | dest='output', 51 | help='Write descriptor to FILE.', 52 | metavar='FILE', 53 | default='music_service.descriptor') 54 | 55 | parser.add_option('-r', '--registry_path', 56 | dest='registry_path', 57 | help='Path to registry service.', 58 | metavar='REGISTRY_PATH', 59 | default='/protorpc') 60 | 61 | parser.add_option('-s', '--server', 62 | dest='server', 63 | help='Tunes DB server.', 64 | metavar='SERVER', 65 | default='tunes-db.appspot.com') 66 | 67 | options, args = parser.parse_args(argv) 68 | 69 | if args: 70 | parser.print_help() 71 | sys.exit(1) 72 | 73 | return options 74 | 75 | 76 | def main(argv): 77 | options = parse_options(argv[1:]) 78 | 79 | registry_url = 'http://%s%s' % (options.server, 80 | options.registry_path) 81 | 82 | http_transport = transport.HttpTransport(registry_url, protocol=protojson) 83 | remote_registry = registry.RegistryService.Stub(http_transport) 84 | 85 | # Get complete list of services. 86 | services = remote_registry.services() 87 | 88 | # Get file set for all services on server. 89 | get_file_set = registry.GetFileSetRequest() 90 | get_file_set.names = [service.name for service in services.services] 91 | file_set = remote_registry.get_file_set(get_file_set).file_set 92 | 93 | # Save file sets to disk. 94 | output = open(options.output, 'wb') 95 | try: 96 | output.write(protobuf.encode_message(file_set)) 97 | finally: 98 | output.close() 99 | 100 | 101 | if __name__ == '__main__': 102 | main(sys.argv) 103 | -------------------------------------------------------------------------------- /demos/tunes_db/client/music_service.descriptor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/protorpc/95849c9a3e414b9ba2d3da91fd850156348fe558/demos/tunes_db/client/music_service.descriptor -------------------------------------------------------------------------------- /demos/tunes_db/client/tunes_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Client library for tunes_db service.""" 19 | 20 | import os 21 | 22 | from protorpc import definition 23 | 24 | definition.import_file_set(os.path.join(os.path.dirname(__file__), 25 | 'music_service.descriptor')) 26 | -------------------------------------------------------------------------------- /demos/tunes_db/server/app.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | application: tunes-db 19 | version: 1 20 | api_version: 1 21 | runtime: python 22 | 23 | handlers: 24 | 25 | - url: /forms/(.*)\.(png|css|js) 26 | static_files: forms_static/\1.\2 27 | upload: forms_static/(.*)\.(png|css|js) 28 | 29 | - url: /music.* 30 | script: services.py 31 | 32 | - url: /protorpc.* 33 | script: services.py 34 | 35 | - url: /_ah/stats/service.* 36 | script: protorpc_appstats/main.py 37 | 38 | - url: /_ah/stats.* 39 | script: $PYTHON_LIB/google/appengine/ext/appstats/ui.py 40 | 41 | - url: / 42 | script: main.py 43 | -------------------------------------------------------------------------------- /demos/tunes_db/server/appengine_config.py: -------------------------------------------------------------------------------- 1 | def webapp_add_wsgi_middleware(app): 2 | """Configure additional middlewares for webapp. 3 | 4 | This function is called automatically by webapp.util.run_wsgi_app 5 | to give the opportunity for an application to register additional 6 | wsgi middleware components. 7 | 8 | See http://http://code.google.com/appengine/docs/python/tools/appstats.html 9 | for more information about configuring and running appstats. 10 | """ 11 | from google.appengine.ext.appstats import recording 12 | app = recording.appstats_wsgi_middleware(app) 13 | return app 14 | 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.dirname(__file__)) 19 | -------------------------------------------------------------------------------- /demos/tunes_db/server/datastore_test_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | 20 | from google.appengine.api import apiproxy_stub_map 21 | from google.appengine.api import datastore_file_stub 22 | from protorpc import test_util 23 | 24 | # DO NOT SUBMIT: Just letting reviewer know that most of this 25 | # came from apphosting/ext/api_testutil.py 26 | 27 | 28 | class DatastoreTest(test_util.TestCase): 29 | """Base class for tests that require datastore.""" 30 | 31 | __apiproxy_initialized = False 32 | 33 | def setUp(self): 34 | """Set up the datastore.""" 35 | self.app_id = 'my-app' 36 | 37 | # Set environment variable for app id. 38 | os.environ['APPLICATION_ID'] = self.app_id 39 | 40 | # Don't use the filesystem with this stub. 41 | self.datastore_stub = datastore_file_stub.DatastoreFileStub( 42 | self.app_id, None) 43 | 44 | # Register stub. 45 | self.ResetApiProxyStubMap() 46 | apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', self.datastore_stub) 47 | 48 | def ResetApiProxyStubMap(self): 49 | """Reset the proxy stub-map. 50 | 51 | Args: 52 | force: When True, always reset the stubs regardless of their status. 53 | 54 | Must be called before stubs can be configured. 55 | 56 | Every time a new test is created, it is necessary to run with a brand new 57 | stub. The problem is that RegisterStub won't allow stubs to be replaced. 58 | If the global instance is not reset, it raises an exception when a run a 59 | new test gets run that wants to use a new stub. 60 | 61 | Calling this method more than once per APITest instance will only cause 62 | a new stub-map to be created once. Therefore it is called automatically 63 | during each Configure method. 64 | """ 65 | if self.__apiproxy_initialized: 66 | return 67 | self.__apiproxy_initialized = True 68 | apiproxy_stub_map.apiproxy = apiproxy_stub_map.GetDefaultAPIProxy() 69 | -------------------------------------------------------------------------------- /demos/tunes_db/server/index.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | indexes: 19 | 20 | - kind: Info 21 | properties: 22 | - name: class 23 | - name: encoded_name 24 | 25 | - kind: Info 26 | properties: 27 | - name: artist 28 | - name: class 29 | - name: encoded_name 30 | - name: name 31 | 32 | - kind: Info 33 | properties: 34 | - name: class 35 | - name: encoded_name 36 | - name: name 37 | 38 | # AUTOGENERATED 39 | 40 | # This index.yaml is automatically updated whenever the dev_appserver 41 | # detects that a new type of query is run. If you want to manage the 42 | # index.yaml file manually, remove the above marker line (the line 43 | # saying "# AUTOGENERATED"). If you want to manage some indexes 44 | # manually, move them above the marker line. The index.yaml file is 45 | # automatically uploaded to the admin console when you next deploy 46 | # your application using appcfg.py. 47 | -------------------------------------------------------------------------------- /demos/tunes_db/server/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | __author__ = 'rafek@google.com (Rafe Kaplan)' 19 | 20 | import appengine_config 21 | 22 | from google.appengine.ext import webapp 23 | from google.appengine.ext.webapp import util 24 | 25 | 26 | application = webapp.WSGIApplication( 27 | [('/', webapp.RedirectHandler.new_factory('/protorpc/form'))], 28 | debug=True) 29 | 30 | 31 | def main(): 32 | util.run_wsgi_app(application) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /demos/tunes_db/server/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Internal Datastore model for the Tunes DB. 19 | 20 | The Tunes DB is a simple polymophic structure composed of polymorphic 21 | Info entities. Artists and Albums are represented. 22 | """ 23 | 24 | __author__ = 'rafek@google.com (Rafe Kaplan)' 25 | 26 | import re 27 | 28 | from google.appengine.ext import db 29 | from google.appengine.ext.db import polymodel 30 | 31 | 32 | _SEARCH_NAME_REGEX = re.compile('\w+', re.UNICODE) 33 | 34 | 35 | def _normalize_name(name): 36 | """Helper used to convert a user entered name in to search compatible string. 37 | 38 | In order to make search as error free as possible, names of info records 39 | are converted to a simplified utf-8 encoded string that makes prefix searches 40 | easy. to make searching simpler, it removes all extra punctuation and spaces. 41 | 42 | Examples: 43 | _normalize_name('Duke Ellington') == 'duke ellington' 44 | _normalize_name(' Duke Ellington ') == 'duke ellington' 45 | _normalize_name('Duke-Ellington!') == 'duke ellington' 46 | _normalize_name('Duke_Ellington') == 'duke ellington' 47 | _normalize_name(u'Duk\xea Ellington') == 'Duk\xc3\xaa Ellington' 48 | 49 | Args: 50 | name: Name to convert to search string. 51 | 52 | Returns: 53 | Lower case, single space separated ByteString of name with punctuation 54 | removed. Unicode values are converted to UTF-8 encoded string. 55 | """ 56 | if name is None: 57 | return None 58 | elif isinstance(name, str): 59 | name = name.decode('utf-8') 60 | 61 | # Must explicitly replace '_' because the \w re part does not 62 | name = name.replace(u'_', u' ') 63 | 64 | names = _SEARCH_NAME_REGEX.findall(name) 65 | name = ' '.join(names) 66 | return db.ByteString(name.lower().encode('utf-8')) 67 | 68 | 69 | class Info(polymodel.PolyModel): 70 | """Base class for all Info records in Tunes DB. 71 | 72 | Properties: 73 | name: User friendly name for record. 74 | encoded_name: Derived from name to allow easy prefix searching. Name is 75 | transformed using _normalize_name. 76 | """ 77 | 78 | name = db.StringProperty() 79 | 80 | @db.ComputedProperty 81 | def encoded_name(self): 82 | return _normalize_name(self.name) 83 | 84 | @classmethod 85 | def search(cls, name_prefix=None): 86 | """Create search query based on info record name prefix. 87 | 88 | Args: 89 | name_prefix: User input name-prefix to search for. If name_prefix 90 | is empty string or None returns all records of Info sub-class. Records 91 | are sorted by their encoded name. 92 | 93 | Returns: 94 | Datastore query pointing to search results. 95 | """ 96 | name_prefix = _normalize_name(name_prefix) 97 | query = cls.all().order('encoded_name') 98 | if name_prefix: 99 | query.filter('encoded_name >=', db.ByteString(name_prefix)) 100 | # Do not need to worry about name_prefix + '\xff\xff' because not 101 | # a unicode character. 102 | query.filter('encoded_name <=', db.ByteString(name_prefix + '\xff')) 103 | return query 104 | 105 | 106 | class ArtistInfo(Info): 107 | """Musician or music group responsible for recording. 108 | 109 | Properties: 110 | album_count: Number of albums produced by artist. 111 | albums: Implicit collection of albums produced by artist. 112 | """ 113 | 114 | album_count = db.IntegerProperty(default=0) 115 | 116 | 117 | class AlbumInfo(Info): 118 | """Album produced by a musician or music group. 119 | 120 | Properties: 121 | artist: Artist that produced album. 122 | released: Year that album was released. 123 | """ 124 | 125 | artist = db.ReferenceProperty(ArtistInfo, 126 | collection_name='albums', 127 | required=True) 128 | released = db.IntegerProperty() 129 | -------------------------------------------------------------------------------- /demos/tunes_db/server/model_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Tests for model.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | import unittest 23 | 24 | from google.appengine.ext import db 25 | 26 | import datastore_test_util 27 | import model 28 | 29 | 30 | class InfoTest(datastore_test_util.DatastoreTest): 31 | """Test the info base class. 32 | 33 | This test uses the ArtistInfo sub-class, but the functionality defined 34 | there will work for all sub-classes. 35 | """ 36 | 37 | def testEncodedName(self): 38 | """Test the encoded_name derived property.""" 39 | 40 | def get_encoded_name(name): 41 | """Helper to get encoded name for an provided name. 42 | 43 | Args: 44 | name: Encoded name to convert to encoded_name. 45 | """ 46 | return db.get(model.ArtistInfo(name=name).put()).encoded_name 47 | 48 | # Normal strings. 49 | self.assertEquals('stereo total', get_encoded_name('Stereo Total')) 50 | # Not alphabetic characters. 51 | self.assertEquals('the go team', get_encoded_name('The Go! Team')) 52 | # Unecessary spaces. 53 | self.assertEquals('ananda shankar', 54 | get_encoded_name(' Ananda Shankar ')) 55 | # Non-ascii unicode. 56 | self.assertEquals('vive la f\xc3\xaate', 57 | get_encoded_name(u'Vive la f\xeate')) 58 | # Numerics. 59 | self.assertEquals('delta5', get_encoded_name(u'Delta5')) 60 | 61 | # The pesky '_'. 62 | self.assertEquals('wendy carlos', get_encoded_name('Wendy__Carlos')) 63 | 64 | def testSearch(self): 65 | """Test searching by name prefix.""" 66 | # Defined out of order to make sure search is in order. 67 | model.ArtistInfo(name='The Bee__Gees').put() 68 | model.ArtistInfo(name=' The-DooRs ').put() 69 | model.ArtistInfo(name='Wendy Carlos').put() 70 | model.ArtistInfo(name='Amadeus Mozart').put() 71 | model.ArtistInfo(name='The Beatles').put() 72 | 73 | names = [artist.name for artist in model.ArtistInfo.search(' ')] 74 | self.assertEquals(['Amadeus Mozart', 'The Beatles', 'The Bee__Gees', 75 | ' The-DooRs ', 'Wendy Carlos'], 76 | names) 77 | 78 | names = [artist.name for artist in model.ArtistInfo.search(' !tHe} ')] 79 | self.assertEquals(['The Beatles', 'The Bee__Gees', ' The-DooRs '], names) 80 | 81 | names = [artist.name for artist in model.ArtistInfo.search('the bee gees')] 82 | self.assertEquals(['The Bee__Gees'], names) 83 | 84 | names = [artist.name for artist in model.ArtistInfo.search('the doors')] 85 | self.assertEquals([' The-DooRs '], names) 86 | 87 | 88 | if __name__ == '__main__': 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /demos/tunes_db/server/services.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Web services demo. 19 | 20 | Implements a simple music database. The music database is served 21 | off the /music URL. It supports all built in protocols (url-encoded 22 | and protocol buffers) using the default RPC mapping scheme. 23 | 24 | For details about the Tunes service itself, please see tunes_db.py. 25 | 26 | For details about the datastore representation of the Tunes db, please 27 | see model.py. 28 | """ 29 | 30 | __author__ = 'rafek@google.com (Rafe Kaplan)' 31 | 32 | import appengine_config 33 | 34 | from protorpc.webapp import service_handlers 35 | 36 | import tunes_db 37 | 38 | 39 | def main(): 40 | service_handlers.run_services( 41 | [('/music', tunes_db.MusicLibraryService), 42 | ]) 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /experimental/javascript/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p build/ 4 | java -jar compiler.jar \ 5 | --compilation_level ADVANCED_OPTIMIZATIONS \ 6 | --generate_exports \ 7 | --output_wrapper="(function(){%output%})();" \ 8 | --warning_level VERBOSE \ 9 | --js ./closure/base.js \ 10 | --js ./closure/debug/error.js \ 11 | --js ./closure/string/string.js \ 12 | --js util.js \ 13 | --js ./closure/json.js \ 14 | --js ./closure/xmlhttpfactory.js \ 15 | --js ./closure/wrapperxmlhttpfactory.js \ 16 | --js ./closure/xmlhttp.js \ 17 | --js messages.js \ 18 | --js descriptor.js \ 19 | --js protorpc.js \ 20 | > build/protorpc_lib.js 21 | -------------------------------------------------------------------------------- /experimental/javascript/closure/debug/error.js: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Closure Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Provides a base class for custom Error objects such that the 17 | * stack is correctly maintained. 18 | * 19 | * You should never need to throw goog.debug.Error(msg) directly, Error(msg) is 20 | * sufficient. 21 | * 22 | */ 23 | 24 | goog.provide('goog.debug.Error'); 25 | 26 | 27 | 28 | /** 29 | * Base class for custom error objects. 30 | * @param {*=} opt_msg The message associated with the error. 31 | * @constructor 32 | * @extends {Error} 33 | */ 34 | goog.debug.Error = function(opt_msg) { 35 | 36 | // Ensure there is a stack trace. 37 | this.stack = new Error().stack || ''; 38 | 39 | if (opt_msg) { 40 | this.message = String(opt_msg); 41 | } 42 | }; 43 | goog.inherits(goog.debug.Error, Error); 44 | 45 | 46 | /** @inheritDoc */ 47 | goog.debug.Error.prototype.name = 'CustomError'; 48 | -------------------------------------------------------------------------------- /experimental/javascript/closure/wrapperxmlhttpfactory.js: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Closure Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Implementation of XmlHttpFactory which allows construction from 17 | * simple factory methods. 18 | * @author dbk@google.com (David Barrett-Kahn) 19 | */ 20 | 21 | goog.provide('goog.net.WrapperXmlHttpFactory'); 22 | 23 | goog.require('goog.net.XmlHttpFactory'); 24 | 25 | 26 | 27 | /** 28 | * An xhr factory subclass which can be constructed using two factory methods. 29 | * This exists partly to allow the preservation of goog.net.XmlHttp.setFactory() 30 | * with an unchanged signature. 31 | * @param {function() : !(XMLHttpRequest|GearsHttpRequest)} xhrFactory A 32 | * function which returns a new XHR object. 33 | * @param {function() : !Object} optionsFactory A function which returns the 34 | * options associated with xhr objects from this factory. 35 | * @extends {goog.net.XmlHttpFactory} 36 | * @constructor 37 | */ 38 | goog.net.WrapperXmlHttpFactory = function(xhrFactory, optionsFactory) { 39 | goog.net.XmlHttpFactory.call(this); 40 | 41 | /** 42 | * XHR factory method. 43 | * @type {function() : !(XMLHttpRequest|GearsHttpRequest)} 44 | * @private 45 | */ 46 | this.xhrFactory_ = xhrFactory; 47 | 48 | /** 49 | * Options factory method. 50 | * @type {function() : !Object} 51 | * @private 52 | */ 53 | this.optionsFactory_ = optionsFactory; 54 | }; 55 | goog.inherits(goog.net.WrapperXmlHttpFactory, goog.net.XmlHttpFactory); 56 | 57 | 58 | /** @inheritDoc */ 59 | goog.net.WrapperXmlHttpFactory.prototype.createInstance = function() { 60 | return this.xhrFactory_(); 61 | }; 62 | 63 | 64 | /** @inheritDoc */ 65 | goog.net.WrapperXmlHttpFactory.prototype.getOptions = function() { 66 | return this.optionsFactory_(); 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /experimental/javascript/closure/xmlhttp.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 The Closure Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Low level handling of XMLHttpRequest. 17 | */ 18 | 19 | goog.provide('goog.net.DefaultXmlHttpFactory'); 20 | goog.provide('goog.net.XmlHttp'); 21 | goog.provide('goog.net.XmlHttp.OptionType'); 22 | goog.provide('goog.net.XmlHttp.ReadyState'); 23 | 24 | goog.require('goog.net.WrapperXmlHttpFactory'); 25 | goog.require('goog.net.XmlHttpFactory'); 26 | 27 | 28 | /** 29 | * Static class for creating XMLHttpRequest objects. 30 | * @return {!(XMLHttpRequest|GearsHttpRequest)} A new XMLHttpRequest object. 31 | */ 32 | goog.net.XmlHttp = function() { 33 | return goog.net.XmlHttp.factory_.createInstance(); 34 | }; 35 | 36 | 37 | /** 38 | * Gets the options to use with the XMLHttpRequest objects obtained using 39 | * the static methods. 40 | * @return {Object} The options. 41 | */ 42 | goog.net.XmlHttp.getOptions = function() { 43 | return goog.net.XmlHttp.factory_.getOptions(); 44 | }; 45 | 46 | 47 | /** 48 | * Type of options that an XmlHttp object can have. 49 | * @enum {number} 50 | */ 51 | goog.net.XmlHttp.OptionType = { 52 | /** 53 | * Whether a goog.nullFunction should be used to clear the onreadystatechange 54 | * handler instead of null. 55 | */ 56 | USE_NULL_FUNCTION: 0, 57 | 58 | /** 59 | * NOTE(user): In IE if send() errors on a *local* request the readystate 60 | * is still changed to COMPLETE. We need to ignore it and allow the 61 | * try/catch around send() to pick up the error. 62 | */ 63 | LOCAL_REQUEST_ERROR: 1 64 | }; 65 | 66 | 67 | /** 68 | * Status constants for XMLHTTP, matches: 69 | * http://msdn.microsoft.com/library/default.asp?url=/library/ 70 | * en-us/xmlsdk/html/0e6a34e4-f90c-489d-acff-cb44242fafc6.asp 71 | * @enum {number} 72 | */ 73 | goog.net.XmlHttp.ReadyState = { 74 | /** 75 | * Constant for when xmlhttprequest.readyState is uninitialized 76 | */ 77 | UNINITIALIZED: 0, 78 | 79 | /** 80 | * Constant for when xmlhttprequest.readyState is loading. 81 | */ 82 | LOADING: 1, 83 | 84 | /** 85 | * Constant for when xmlhttprequest.readyState is loaded. 86 | */ 87 | LOADED: 2, 88 | 89 | /** 90 | * Constant for when xmlhttprequest.readyState is in an interactive state. 91 | */ 92 | INTERACTIVE: 3, 93 | 94 | /** 95 | * Constant for when xmlhttprequest.readyState is completed 96 | */ 97 | COMPLETE: 4 98 | }; 99 | 100 | 101 | /** 102 | * The global factory instance for creating XMLHttpRequest objects. 103 | * @type {goog.net.XmlHttpFactory} 104 | * @private 105 | */ 106 | goog.net.XmlHttp.factory_; 107 | 108 | 109 | /** 110 | * Sets the factories for creating XMLHttpRequest objects and their options. 111 | * @param {Function} factory The factory for XMLHttpRequest objects. 112 | * @param {Function} optionsFactory The factory for options. 113 | * @deprecated Use setGlobalFactory instead. 114 | */ 115 | goog.net.XmlHttp.setFactory = function(factory, optionsFactory) { 116 | goog.net.XmlHttp.setGlobalFactory(new goog.net.WrapperXmlHttpFactory( 117 | (/** @type {function() : !(XMLHttpRequest|GearsHttpRequest)} */ factory), 118 | (/** @type {function() : !Object}*/ optionsFactory))); 119 | }; 120 | 121 | 122 | /** 123 | * Sets the global factory object. 124 | * @param {!goog.net.XmlHttpFactory} factory New global factory object. 125 | */ 126 | goog.net.XmlHttp.setGlobalFactory = function(factory) { 127 | goog.net.XmlHttp.factory_ = factory; 128 | }; 129 | 130 | 131 | 132 | /** 133 | * Default factory to use when creating xhr objects. You probably shouldn't be 134 | * instantiating this directly, but rather using it via goog.net.XmlHttp. 135 | * @extends {goog.net.XmlHttpFactory} 136 | * @constructor 137 | */ 138 | goog.net.DefaultXmlHttpFactory = function() { 139 | goog.net.XmlHttpFactory.call(this); 140 | }; 141 | goog.inherits(goog.net.DefaultXmlHttpFactory, goog.net.XmlHttpFactory); 142 | 143 | 144 | /** @inheritDoc */ 145 | goog.net.DefaultXmlHttpFactory.prototype.createInstance = function() { 146 | var progId = this.getProgId_(); 147 | if (progId) { 148 | return new ActiveXObject(progId); 149 | } else { 150 | return new XMLHttpRequest(); 151 | } 152 | }; 153 | 154 | 155 | /** @inheritDoc */ 156 | goog.net.DefaultXmlHttpFactory.prototype.internalGetOptions = function() { 157 | var progId = this.getProgId_(); 158 | var options = {}; 159 | if (progId) { 160 | options[goog.net.XmlHttp.OptionType.USE_NULL_FUNCTION] = true; 161 | options[goog.net.XmlHttp.OptionType.LOCAL_REQUEST_ERROR] = true; 162 | } 163 | return options; 164 | }; 165 | 166 | 167 | /** 168 | * The ActiveX PROG ID string to use to create xhr's in IE. Lazily initialized. 169 | * @type {?string} 170 | * @private 171 | */ 172 | goog.net.DefaultXmlHttpFactory.prototype.ieProgId_ = null; 173 | 174 | 175 | /** 176 | * Initialize the private state used by other functions. 177 | * @return {string} The ActiveX PROG ID string to use to create xhr's in IE. 178 | * @private 179 | */ 180 | goog.net.DefaultXmlHttpFactory.prototype.getProgId_ = function() { 181 | // The following blog post describes what PROG IDs to use to create the 182 | // XMLHTTP object in Internet Explorer: 183 | // http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx 184 | // However we do not (yet) fully trust that this will be OK for old versions 185 | // of IE on Win9x so we therefore keep the last 2. 186 | if (!this.ieProgId_ && typeof XMLHttpRequest == 'undefined' && 187 | typeof ActiveXObject != 'undefined') { 188 | // Candidate Active X types. 189 | var ACTIVE_X_IDENTS = ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0', 190 | 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP']; 191 | for (var i = 0; i < ACTIVE_X_IDENTS.length; i++) { 192 | var candidate = ACTIVE_X_IDENTS[i]; 193 | /** @preserveTry */ 194 | try { 195 | new ActiveXObject(candidate); 196 | // NOTE(user): cannot assign progid and return candidate in one line 197 | // because JSCompiler complaings: BUG 658126 198 | this.ieProgId_ = candidate; 199 | return candidate; 200 | } catch (e) { 201 | // do nothing; try next choice 202 | } 203 | } 204 | 205 | // couldn't find any matches 206 | throw Error('Could not create ActiveXObject. ActiveX might be disabled,' + 207 | ' or MSXML might not be installed'); 208 | } 209 | 210 | return /** @type {string} */ (this.ieProgId_); 211 | }; 212 | 213 | 214 | //Set the global factory to an instance of the default factory. 215 | goog.net.XmlHttp.setGlobalFactory(new goog.net.DefaultXmlHttpFactory()); 216 | -------------------------------------------------------------------------------- /experimental/javascript/closure/xmlhttpfactory.js: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Closure Library Authors. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Interface for a factory for creating XMLHttpRequest objects 17 | * and metadata about them. 18 | * @author dbk@google.com (David Barrett-Kahn) 19 | */ 20 | 21 | goog.provide('goog.net.XmlHttpFactory'); 22 | 23 | 24 | 25 | /** 26 | * Abstract base class for an XmlHttpRequest factory. 27 | * @constructor 28 | */ 29 | goog.net.XmlHttpFactory = function() { 30 | }; 31 | 32 | 33 | /** 34 | * Cache of options - we only actually call internalGetOptions once. 35 | * @type {Object} 36 | * @private 37 | */ 38 | goog.net.XmlHttpFactory.prototype.cachedOptions_ = null; 39 | 40 | 41 | /** 42 | * @return {!(XMLHttpRequest|GearsHttpRequest)} A new XMLHttpRequest instance. 43 | */ 44 | goog.net.XmlHttpFactory.prototype.createInstance = goog.abstractMethod; 45 | 46 | 47 | /** 48 | * @return {Object} Options describing how xhr objects obtained from this 49 | * factory should be used. 50 | */ 51 | goog.net.XmlHttpFactory.prototype.getOptions = function() { 52 | return this.cachedOptions_ || 53 | (this.cachedOptions_ = this.internalGetOptions()); 54 | }; 55 | 56 | 57 | /** 58 | * Override this method in subclasses to preserve the caching offered by 59 | * getOptions(). 60 | * @return {Object} Options describing how xhr objects obtained from this 61 | * factory should be used. 62 | * @protected 63 | */ 64 | goog.net.XmlHttpFactory.prototype.internalGetOptions = goog.abstractMethod; 65 | -------------------------------------------------------------------------------- /experimental/javascript/descriptor.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Various descriptor definitions. 17 | * @author joetyson@gmail.com (Joe Tyson) 18 | */ 19 | 20 | goog.provide('ProtoRpc.EnumDescriptor'); 21 | goog.provide('ProtoRpc.EnumValueDescriptor'); 22 | /* 23 | goog.provide('ProtoRpc.FieldDescriptor'); 24 | goog.provide('ProtoRpc.MessageDescriptor'); 25 | goog.provide('ProtoRpc.MethodDescriptor'); 26 | goog.provide('ProtoRpc.FileDescriptor'); 27 | goog.provide('ProtoRpc.FileSet'); 28 | goog.provide('ProtoRpc.ServiceDescriptor');*/ 29 | 30 | goog.require('ProtoRpc.IntegerField'); 31 | goog.require('ProtoRpc.Message'); 32 | goog.require('ProtoRpc.StringField'); 33 | 34 | 35 | /** 36 | * 37 | */ 38 | ProtoRpc.EnumValueDescriptor = ProtoRpc.Message('EnumValueDescriptor', { 39 | fields: { 40 | 'name': new ProtoRpc.StringField(1, {required: true}), 41 | 'number': new ProtoRpc.IntegerField(2, {required: true}) 42 | } 43 | }); 44 | 45 | 46 | /** 47 | * Enum class descriptor. 48 | * @export 49 | */ 50 | ProtoRpc.EnumDescriptor = ProtoRpc.Message('EnumDescriptor', { 51 | fields: { 52 | 'name': new ProtoRpc.StringField(1), 53 | 'values': new ProtoRpc.MessageField(ProtoRpc.EnumValueDescriptor, 2, { 54 | repeated: true}) 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /experimental/javascript/util.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * @fileoverview Utilities for ProtoRpc. 17 | */ 18 | 19 | goog.provide('ProtoRpc.Util.Error'); 20 | 21 | goog.require('goog.debug.Error'); 22 | goog.require('goog.string'); 23 | 24 | 25 | /** 26 | * Base class for all ProtoRpc errors. 27 | * @param {string} pattern The pattern to use for the error message. 28 | * @param {!Array.<*>} args The items to substitue into the pattern. 29 | * @constructor 30 | * @extends {goog.debug.Error} 31 | */ 32 | ProtoRpc.Util.Error = function(pattern, args) { 33 | args.unshift(pattern); 34 | goog.base(this, goog.string.subs.apply(null, args)); 35 | }; 36 | goog.inherits(ProtoRpc.Util.Error, goog.debug.Error); 37 | 38 | 39 | /** 40 | * Convert underscores and dashes to camelCase. 41 | * 42 | * @param {string} str The string to camel case. 43 | * @param {string=} prefix An optional prefix. 44 | */ 45 | ProtoRpc.Util.toCamelCase = function(str, prefix) { 46 | if (prefix) { 47 | str = [prefix, '_', str].join(''); 48 | } 49 | return str.replace(/[_|-]([a-z])/g, function(all, match) { 50 | return match.toUpperCase(); 51 | }); 52 | }; -------------------------------------------------------------------------------- /ez_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap setuptools installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import sys 17 | DEFAULT_VERSION = "0.6c11" 18 | DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] 19 | 20 | md5_data = { 21 | 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 22 | 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 23 | 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 24 | 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 25 | 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 26 | 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 27 | 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 28 | 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 29 | 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 30 | 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 31 | 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 32 | 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 33 | 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 34 | 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 35 | 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', 36 | } 37 | 38 | import sys, os 39 | try: from hashlib import md5 40 | except ImportError: from md5 import md5 41 | 42 | def _validate_md5(egg_name, data): 43 | if egg_name in md5_data: 44 | digest = md5(data).hexdigest() 45 | if digest != md5_data[egg_name]: 46 | print >>sys.stderr, ( 47 | "md5 validation of %s failed! (Possible download problem?)" 48 | % egg_name 49 | ) 50 | sys.exit(2) 51 | return data 52 | 53 | def use_setuptools( 54 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, 55 | download_delay=15 56 | ): 57 | """Automatically find/download setuptools and make it available on sys.path 58 | 59 | `version` should be a valid setuptools version number that is available 60 | as an egg for download under the `download_base` URL (which should end with 61 | a '/'). `to_dir` is the directory where setuptools will be downloaded, if 62 | it is not already available. If `download_delay` is specified, it should 63 | be the number of seconds that will be paused before initiating a download, 64 | should one be required. If an older version of setuptools is installed, 65 | this routine will print a message to ``sys.stderr`` and raise SystemExit in 66 | an attempt to abort the calling script. 67 | """ 68 | was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules 69 | def do_download(): 70 | egg = download_setuptools(version, download_base, to_dir, download_delay) 71 | sys.path.insert(0, egg) 72 | import setuptools; setuptools.bootstrap_install_from = egg 73 | try: 74 | import pkg_resources 75 | except ImportError: 76 | return do_download() 77 | try: 78 | pkg_resources.require("setuptools>="+version); return 79 | except pkg_resources.VersionConflict, e: 80 | if was_imported: 81 | print >>sys.stderr, ( 82 | "The required version of setuptools (>=%s) is not available, and\n" 83 | "can't be installed while this script is running. Please install\n" 84 | " a more recent version first, using 'easy_install -U setuptools'." 85 | "\n\n(Currently using %r)" 86 | ) % (version, e.args[0]) 87 | sys.exit(2) 88 | except pkg_resources.DistributionNotFound: 89 | pass 90 | 91 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 92 | return do_download() 93 | 94 | def download_setuptools( 95 | version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, 96 | delay = 15 97 | ): 98 | """Download setuptools from a specified location and return its filename 99 | 100 | `version` should be a valid setuptools version number that is available 101 | as an egg for download under the `download_base` URL (which should end 102 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 103 | `delay` is the number of seconds to pause before an actual download attempt. 104 | """ 105 | import urllib2, shutil 106 | egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) 107 | url = download_base + egg_name 108 | saveto = os.path.join(to_dir, egg_name) 109 | src = dst = None 110 | if not os.path.exists(saveto): # Avoid repeated downloads 111 | try: 112 | from distutils import log 113 | if delay: 114 | log.warn(""" 115 | --------------------------------------------------------------------------- 116 | This script requires setuptools version %s to run (even to display 117 | help). I will attempt to download it for you (from 118 | %s), but 119 | you may need to enable firewall access for this script first. 120 | I will start the download in %d seconds. 121 | 122 | (Note: if this machine does not have network access, please obtain the file 123 | 124 | %s 125 | 126 | and place it in this directory before rerunning this script.) 127 | ---------------------------------------------------------------------------""", 128 | version, download_base, delay, url 129 | ); from time import sleep; sleep(delay) 130 | log.warn("Downloading %s", url) 131 | src = urllib2.urlopen(url) 132 | # Read/write all in one block, so we don't create a corrupt file 133 | # if the download is interrupted. 134 | data = _validate_md5(egg_name, src.read()) 135 | dst = open(saveto,"wb"); dst.write(data) 136 | finally: 137 | if src: src.close() 138 | if dst: dst.close() 139 | return os.path.realpath(saveto) 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | def main(argv, version=DEFAULT_VERSION): 177 | """Install or upgrade setuptools and EasyInstall""" 178 | try: 179 | import setuptools 180 | except ImportError: 181 | egg = None 182 | try: 183 | egg = download_setuptools(version, delay=0) 184 | sys.path.insert(0,egg) 185 | from setuptools.command.easy_install import main 186 | return main(list(argv)+[egg]) # we're done here 187 | finally: 188 | if egg and os.path.exists(egg): 189 | os.unlink(egg) 190 | else: 191 | if setuptools.__version__ == '0.0.1': 192 | print >>sys.stderr, ( 193 | "You have an obsolete version of setuptools installed. Please\n" 194 | "remove it from your system entirely before rerunning this script." 195 | ) 196 | sys.exit(2) 197 | 198 | req = "setuptools>="+version 199 | import pkg_resources 200 | try: 201 | pkg_resources.require(req) 202 | except pkg_resources.VersionConflict: 203 | try: 204 | from setuptools.command.easy_install import main 205 | except ImportError: 206 | from easy_install import main 207 | main(list(argv)+[download_setuptools(delay=0)]) 208 | sys.exit(0) # try to force an exit 209 | else: 210 | if argv: 211 | from setuptools.command.easy_install import main 212 | main(argv) 213 | else: 214 | print "Setuptools version",version,"or greater has been installed." 215 | print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' 216 | 217 | def update_md5(filenames): 218 | """Update our built-in md5 registry""" 219 | 220 | import re 221 | 222 | for name in filenames: 223 | base = os.path.basename(name) 224 | f = open(name,'rb') 225 | md5_data[base] = md5(f.read()).hexdigest() 226 | f.close() 227 | 228 | data = [" %r: %r,\n" % it for it in md5_data.items()] 229 | data.sort() 230 | repl = "".join(data) 231 | 232 | import inspect 233 | srcfile = inspect.getsourcefile(sys.modules[__name__]) 234 | f = open(srcfile, 'rb'); src = f.read(); f.close() 235 | 236 | match = re.search("\nmd5_data = {\n([^}]+)}", src) 237 | if not match: 238 | print >>sys.stderr, "Internal error!" 239 | sys.exit(2) 240 | 241 | src = src[:match.start(1)] + repl + src[match.end(1):] 242 | f = open(srcfile,'w') 243 | f.write(src) 244 | f.close() 245 | 246 | 247 | if __name__=='__main__': 248 | if len(sys.argv)>2 and sys.argv[1]=='--md5update': 249 | update_md5(sys.argv[2:]) 250 | else: 251 | main(sys.argv[1:]) 252 | -------------------------------------------------------------------------------- /protorpc/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Main module for ProtoRPC package.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | __version__ = '1.0' 22 | -------------------------------------------------------------------------------- /protorpc/end2end_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """End to end tests for ProtoRPC.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | 23 | import unittest 24 | 25 | from protorpc import protojson 26 | from protorpc import remote 27 | from protorpc import test_util 28 | from protorpc import util 29 | from protorpc import webapp_test_util 30 | 31 | package = 'test_package' 32 | 33 | 34 | class EndToEndTest(webapp_test_util.EndToEndTestBase): 35 | 36 | def testSimpleRequest(self): 37 | self.assertEquals(test_util.OptionalMessage(string_value='+blar'), 38 | self.stub.optional_message(string_value='blar')) 39 | 40 | def testSimpleRequestComplexContentType(self): 41 | response = self.DoRawRequest( 42 | 'optional_message', 43 | content='{"string_value": "blar"}', 44 | content_type='application/json; charset=utf-8') 45 | headers = response.headers 46 | self.assertEquals(200, response.code) 47 | self.assertEquals('{"string_value": "+blar"}', response.read()) 48 | self.assertEquals('application/json', headers['content-type']) 49 | 50 | def testInitParameter(self): 51 | self.assertEquals(test_util.OptionalMessage(string_value='uninitialized'), 52 | self.stub.init_parameter()) 53 | self.assertEquals(test_util.OptionalMessage(string_value='initialized'), 54 | self.other_stub.init_parameter()) 55 | 56 | def testMissingContentType(self): 57 | code, content, headers = self.RawRequestError( 58 | 'optional_message', 59 | content='{"string_value": "blar"}', 60 | content_type='') 61 | self.assertEquals(400, code) 62 | self.assertEquals(util.pad_string('Bad Request'), content) 63 | self.assertEquals('text/plain; charset=utf-8', headers['content-type']) 64 | 65 | def testWrongPath(self): 66 | self.assertRaisesWithRegexpMatch(remote.ServerError, 67 | 'HTTP Error 404: Not Found', 68 | self.bad_path_stub.optional_message) 69 | 70 | def testUnsupportedContentType(self): 71 | code, content, headers = self.RawRequestError( 72 | 'optional_message', 73 | content='{"string_value": "blar"}', 74 | content_type='image/png') 75 | self.assertEquals(415, code) 76 | self.assertEquals(util.pad_string('Unsupported Media Type'), content) 77 | self.assertEquals(headers['content-type'], 'text/plain; charset=utf-8') 78 | 79 | def testUnsupportedHttpMethod(self): 80 | code, content, headers = self.RawRequestError('optional_message') 81 | self.assertEquals(405, code) 82 | self.assertEquals( 83 | util.pad_string('/my/service.optional_message is a ProtoRPC method.\n\n' 84 | 'Service protorpc.webapp_test_util.TestService\n\n' 85 | 'More about ProtoRPC: ' 86 | 'http://code.google.com/p/google-protorpc\n'), 87 | content) 88 | self.assertEquals(headers['content-type'], 'text/plain; charset=utf-8') 89 | 90 | def testMethodNotFound(self): 91 | self.assertRaisesWithRegexpMatch(remote.MethodNotFoundError, 92 | 'Unrecognized RPC method: does_not_exist', 93 | self.mismatched_stub.does_not_exist) 94 | 95 | def testBadMessageError(self): 96 | code, content, headers = self.RawRequestError('nested_message', 97 | content='{}') 98 | self.assertEquals(400, code) 99 | 100 | expected_content = protojson.encode_message(remote.RpcStatus( 101 | state=remote.RpcState.REQUEST_ERROR, 102 | error_message=('Error parsing ProtoRPC request ' 103 | '(Unable to parse request content: ' 104 | 'Message NestedMessage is missing ' 105 | 'required field a_value)'))) 106 | self.assertEquals(util.pad_string(expected_content), content) 107 | self.assertEquals(headers['content-type'], 'application/json') 108 | 109 | def testApplicationError(self): 110 | try: 111 | self.stub.raise_application_error() 112 | except remote.ApplicationError as err: 113 | self.assertEquals('This is an application error', unicode(err)) 114 | self.assertEquals('ERROR_NAME', err.error_name) 115 | else: 116 | self.fail('Expected application error') 117 | 118 | def testRpcError(self): 119 | try: 120 | self.stub.raise_rpc_error() 121 | except remote.ServerError as err: 122 | self.assertEquals('Internal Server Error', unicode(err)) 123 | else: 124 | self.fail('Expected server error') 125 | 126 | def testUnexpectedError(self): 127 | try: 128 | self.stub.raise_unexpected_error() 129 | except remote.ServerError as err: 130 | self.assertEquals('Internal Server Error', unicode(err)) 131 | else: 132 | self.fail('Expected server error') 133 | 134 | def testBadResponse(self): 135 | try: 136 | self.stub.return_bad_message() 137 | except remote.ServerError as err: 138 | self.assertEquals('Internal Server Error', unicode(err)) 139 | else: 140 | self.fail('Expected server error') 141 | 142 | 143 | def main(): 144 | unittest.main() 145 | 146 | 147 | if __name__ == '__main__': 148 | main() 149 | -------------------------------------------------------------------------------- /protorpc/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Main module for ProtoRPC package.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | -------------------------------------------------------------------------------- /protorpc/experimental/parser/protobuf.g: -------------------------------------------------------------------------------- 1 | /* !/usr/bin/env python 2 | * 3 | * Copyright 2011 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | parser grammar protobuf; 19 | 20 | scalar_value 21 | : STRING 22 | | FLOAT 23 | | INT 24 | | BOOL 25 | ; 26 | 27 | id 28 | : ID 29 | | PACKAGE 30 | | SERVICE 31 | | MESSAGE 32 | | ENUM 33 | | DATA_TYPE 34 | | EXTENSIONS 35 | ; 36 | 37 | user_option_id 38 | : '(' name_root='.'? qualified_name ')' 39 | -> ^(USER_OPTION_ID $name_root? qualified_name) 40 | ; 41 | 42 | option_id 43 | : (id | user_option_id) ('.'! (id | user_option_id))* 44 | ; 45 | 46 | option 47 | : option_id '=' (scalar_value | id) 48 | -> ^(OPTION ^(OPTION_ID option_id) scalar_value? id?) 49 | ; 50 | 51 | decl_options 52 | : '[' option (',' option)* ']' 53 | -> ^(OPTIONS option*) 54 | ; 55 | 56 | qualified_name 57 | : id ('.'! id)* 58 | ; 59 | 60 | field_decl 61 | : qualified_name id '=' INT decl_options? ';' 62 | -> ^(FIELD_TYPE qualified_name) id INT decl_options? 63 | | GROUP id '=' INT '{' message_def '}' 64 | -> ^(FIELD_TYPE GROUP) id INT ^(GROUP_MESSAGE message_def) 65 | ; 66 | 67 | field 68 | : LABEL field_decl 69 | -> ^(FIELD LABEL field_decl) 70 | ; 71 | 72 | enum_decl 73 | : id '=' INT decl_options? ';' 74 | -> ^(ENUM_DECL id INT decl_options?) 75 | ; 76 | 77 | enum_def 78 | : ENUM id '{' (def_option | enum_decl | ';')* '}' 79 | -> ^(ENUM id 80 | ^(OPTIONS def_option*) 81 | ^(ENUM_DECLS enum_decl*)) 82 | ; 83 | 84 | extensions 85 | : EXTENSIONS start=INT (TO (end=INT | end=MAX))? ';' -> ^(EXTENSION_RANGE $start $end) 86 | ; 87 | 88 | message_def 89 | : ( field 90 | | enum_def 91 | | message 92 | | extension 93 | | extensions 94 | | def_option 95 | | ';' 96 | )* -> 97 | ^(FIELDS field*) 98 | ^(MESSAGES message*) 99 | ^(ENUMS enum_def*) 100 | ^(EXTENSIONS extensions*) 101 | ^(OPTIONS def_option*) 102 | ; 103 | 104 | message 105 | : MESSAGE^ id '{'! message_def '}'! 106 | ; 107 | 108 | method_options 109 | : '{'! (def_option | ';'!)+ '}'! 110 | ; 111 | 112 | method_def 113 | : RPC id '(' qualified_name ')' 114 | RETURNS '(' qualified_name ')' (method_options | ';') 115 | ; 116 | 117 | service_defs 118 | : (def_option | method_def | ';')+ 119 | ; 120 | 121 | service 122 | : SERVICE id '{' service_defs? '}' 123 | ; 124 | 125 | extension 126 | : EXTEND qualified_name '{' message_def '}' 127 | ; 128 | 129 | import_line 130 | : IMPORT! STRING ';'! 131 | ; 132 | 133 | package_decl 134 | : PACKAGE^ qualified_name ';'! 135 | ; 136 | 137 | def_option 138 | : OPTION option ';' -> option 139 | ; 140 | 141 | proto_file 142 | : ( package_decl 143 | | import_line 144 | | message 145 | | enum_def 146 | | service 147 | | extension 148 | | def_option 149 | | ';' 150 | )* 151 | -> ^(PROTO_FILE package_decl* 152 | ^(IMPORTS import_line*) 153 | ^(MESSAGES message*) 154 | ^(ENUMS enum_def*) 155 | ^(SERVICES service*) 156 | ^(EXTENSIONS extension*) 157 | ^(OPTIONS def_option*) 158 | ) 159 | ; 160 | -------------------------------------------------------------------------------- /protorpc/experimental/parser/protobuf_lexer.g: -------------------------------------------------------------------------------- 1 | /* !/usr/bin/env python 2 | * 3 | * Copyright 2011 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | lexer grammar protobuf_lexer; 18 | 19 | tokens { 20 | // Imaginary tree nodes. 21 | ENUMS; 22 | ENUM_DECL; 23 | ENUM_DECLS; 24 | EXTENSION_RANGE; 25 | FIELD; 26 | FIELDS; 27 | FIELD_TYPE; 28 | GROUP_MESSAGE; 29 | IMPORTS; 30 | MESSAGES; 31 | NAME_ROOT; 32 | OPTIONS; 33 | OPTION_ID; 34 | PROTO_FILE; 35 | SERVICES; 36 | USER_OPTION_ID; 37 | } 38 | 39 | // Basic keyword tokens. 40 | ENUM : 'enum'; 41 | MESSAGE : 'message'; 42 | IMPORT : 'import'; 43 | OPTION : 'option'; 44 | PACKAGE : 'package'; 45 | RPC : 'rpc'; 46 | SERVICE : 'service'; 47 | RETURNS : 'returns'; 48 | EXTEND : 'extend'; 49 | EXTENSIONS : 'extensions'; 50 | TO : 'to'; 51 | GROUP : 'group'; 52 | MAX : 'max'; 53 | 54 | COMMENT 55 | : '//' ~('\n'|'\r')* '\r'? '\n' {$channel=HIDDEN;} 56 | | '/*' ( options {greedy=false;} : . )* '*/' {$channel=HIDDEN;} 57 | ; 58 | 59 | WS 60 | : ( ' ' 61 | | '\t' 62 | | '\r' 63 | | '\n' 64 | ) {$channel=HIDDEN;} 65 | ; 66 | 67 | DATA_TYPE 68 | : 'double' 69 | | 'float' 70 | | 'int32' 71 | | 'int64' 72 | | 'uint32' 73 | | 'uint64' 74 | | 'sint32' 75 | | 'sint64' 76 | | 'fixed32' 77 | | 'fixed64' 78 | | 'sfixed32' 79 | | 'sfixed64' 80 | | 'bool' 81 | | 'string' 82 | | 'bytes' 83 | ; 84 | 85 | LABEL 86 | : 'required' 87 | | 'optional' 88 | | 'repeated' 89 | ; 90 | 91 | BOOL 92 | : 'true' 93 | | 'false' 94 | ; 95 | 96 | ID 97 | : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* 98 | ; 99 | 100 | INT 101 | : '-'? ('0'..'9'+ | '0x' ('a'..'f'|'A'..'F'|'0'..'9')+ | 'inf') 102 | | 'nan' 103 | ; 104 | 105 | FLOAT 106 | : '-'? ('0'..'9')+ '.' ('0'..'9')* EXPONENT? 107 | | '-'? '.' ('0'..'9')+ EXPONENT? 108 | | '-'? ('0'..'9')+ EXPONENT 109 | ; 110 | 111 | STRING 112 | : '"' ( STRING_INNARDS )* '"'; 113 | 114 | fragment 115 | STRING_INNARDS 116 | : ESC_SEQ 117 | | ~('\\'|'"') 118 | ; 119 | 120 | fragment 121 | EXPONENT 122 | : ('e'|'E') ('+'|'-')? ('0'..'9')+ 123 | ; 124 | 125 | fragment 126 | HEX_DIGIT 127 | : ('0'..'9'|'a'..'f'|'A'..'F') 128 | ; 129 | 130 | fragment 131 | ESC_SEQ 132 | : '\\' ('a'|'b'|'t'|'n'|'f'|'r'|'v'|'\"'|'\''|'\\') 133 | | UNICODE_ESC 134 | | OCTAL_ESC 135 | | HEX_ESC 136 | ; 137 | 138 | fragment 139 | OCTAL_ESC 140 | : '\\' ('0'..'3') ('0'..'7') ('0'..'7') 141 | | '\\' ('0'..'7') ('0'..'7') 142 | | '\\' ('0'..'7') 143 | ; 144 | 145 | fragment 146 | HEX_ESC 147 | : '\\x' HEX_DIGIT HEX_DIGIT 148 | ; 149 | 150 | fragment 151 | UNICODE_ESC 152 | : '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT 153 | ; 154 | -------------------------------------------------------------------------------- /protorpc/experimental/parser/pyprotobuf.g: -------------------------------------------------------------------------------- 1 | /* !/usr/bin/env python 2 | * 3 | * Copyright 2011 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | grammar pyprotobuf; 19 | 20 | options { 21 | // language=Python; 22 | output = AST; 23 | ASTLabelType = CommonTree; 24 | } 25 | 26 | import protobuf_lexer, protobuf; 27 | 28 | // For reasons I do not understand the HIDDEN elements from the imported 29 | // with their channel intact. 30 | 31 | COMMENT 32 | : '//' ~('\n'|'\r')* '\r'? '\n' {$channel=HIDDEN;} 33 | | '/*' ( options {greedy=false;} : . )* '*/' {$channel=HIDDEN;} 34 | ; 35 | 36 | WS : ( ' ' 37 | | '\t' 38 | | '\r' 39 | | '\n' 40 | ) {$channel=HIDDEN;} 41 | ; 42 | 43 | py_proto_file 44 | : proto_file EOF^ 45 | ; 46 | -------------------------------------------------------------------------------- /protorpc/experimental/parser/test.proto: -------------------------------------------------------------------------------- 1 | /* !/usr/bin/env python 2 | * 3 | * Copyright 2011 Google Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package a.b.c; 18 | 19 | import "abc.def"; 20 | import "from/here"; 21 | 22 | message MyMessage { 23 | required int64 thing = 1 [a="b"]; 24 | optional group whatever = 2 { 25 | repeated int64 thing = 1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /protorpc/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | __author__ = 'rafek@google.com (Rafe Kaplan)' 19 | 20 | import contextlib 21 | 22 | from . import messages 23 | from . import util 24 | 25 | __all__ = ['IndentationError', 26 | 'IndentWriter', 27 | ] 28 | 29 | 30 | class IndentationError(messages.Error): 31 | """Raised when end_indent is called too many times.""" 32 | 33 | 34 | class IndentWriter(object): 35 | """Utility class to make it easy to write formatted indented text. 36 | 37 | IndentWriter delegates to a file-like object and is able to keep track of the 38 | level of indentation. Each call to write_line will write a line terminated 39 | by a new line proceeded by a number of spaces indicated by the current level 40 | of indentation. 41 | 42 | IndexWriter overloads the << operator to make line writing operations clearer. 43 | 44 | The indent method returns a context manager that can be used by the Python 45 | with statement that makes generating python code easier to use. For example: 46 | 47 | index_writer << 'def factorial(n):' 48 | with index_writer.indent(): 49 | index_writer << 'if n <= 1:' 50 | with index_writer.indent(): 51 | index_writer << 'return 1' 52 | index_writer << 'else:' 53 | with index_writer.indent(): 54 | index_writer << 'return factorial(n - 1)' 55 | 56 | This would generate: 57 | 58 | def factorial(n): 59 | if n <= 1: 60 | return 1 61 | else: 62 | return factorial(n - 1) 63 | """ 64 | 65 | @util.positional(2) 66 | def __init__(self, output, indent_space=2): 67 | """Constructor. 68 | 69 | Args: 70 | output: File-like object to wrap. 71 | indent_space: Number of spaces each level of indentation will be. 72 | """ 73 | # Private attributes: 74 | # 75 | # __output: The wrapped file-like object. 76 | # __indent_space: String to append for each level of indentation. 77 | # __indentation: The current full indentation string. 78 | self.__output = output 79 | self.__indent_space = indent_space * ' ' 80 | self.__indentation = 0 81 | 82 | @property 83 | def indent_level(self): 84 | """Current level of indentation for IndentWriter.""" 85 | return self.__indentation 86 | 87 | def write_line(self, line): 88 | """Write line to wrapped file-like object using correct indentation. 89 | 90 | The line is written with the current level of indentation printed before it 91 | and terminated by a new line. 92 | 93 | Args: 94 | line: Line to write to wrapped file-like object. 95 | """ 96 | if line != '': 97 | self.__output.write(self.__indentation * self.__indent_space) 98 | self.__output.write(line) 99 | self.__output.write('\n') 100 | 101 | def begin_indent(self): 102 | """Begin a level of indentation.""" 103 | self.__indentation += 1 104 | 105 | def end_indent(self): 106 | """Undo the most recent level of indentation. 107 | 108 | Raises: 109 | IndentationError when called with no indentation levels. 110 | """ 111 | if not self.__indentation: 112 | raise IndentationError('Unable to un-indent further') 113 | self.__indentation -= 1 114 | 115 | @contextlib.contextmanager 116 | def indent(self): 117 | """Create indentation level compatible with the Python 'with' keyword.""" 118 | self.begin_indent() 119 | yield 120 | self.end_indent() 121 | 122 | def __lshift__(self, line): 123 | """Syntactic sugar for write_line method. 124 | 125 | Args: 126 | line: Line to write to wrapped file-like object. 127 | """ 128 | self.write_line(line) 129 | -------------------------------------------------------------------------------- /protorpc/generate_proto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from __future__ import with_statement 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | import logging 23 | 24 | from . import descriptor 25 | from . import generate 26 | from . import messages 27 | from . import util 28 | 29 | 30 | __all__ = ['format_proto_file'] 31 | 32 | 33 | @util.positional(2) 34 | def format_proto_file(file_descriptor, output, indent_space=2): 35 | out = generate.IndentWriter(output, indent_space=indent_space) 36 | 37 | if file_descriptor.package: 38 | out << 'package %s;' % file_descriptor.package 39 | 40 | def write_enums(enum_descriptors): 41 | """Write nested and non-nested Enum types. 42 | 43 | Args: 44 | enum_descriptors: List of EnumDescriptor objects from which to generate 45 | enums. 46 | """ 47 | # Write enums. 48 | for enum in enum_descriptors or []: 49 | out << '' 50 | out << '' 51 | out << 'enum %s {' % enum.name 52 | out << '' 53 | 54 | with out.indent(): 55 | if enum.values: 56 | for enum_value in enum.values: 57 | out << '%s = %s;' % (enum_value.name, enum_value.number) 58 | 59 | out << '}' 60 | 61 | write_enums(file_descriptor.enum_types) 62 | 63 | def write_fields(field_descriptors): 64 | """Write fields for Message types. 65 | 66 | Args: 67 | field_descriptors: List of FieldDescriptor objects from which to generate 68 | fields. 69 | """ 70 | for field in field_descriptors or []: 71 | default_format = '' 72 | if field.default_value is not None: 73 | if field.label == descriptor.FieldDescriptor.Label.REPEATED: 74 | logging.warning('Default value for repeated field %s is not being ' 75 | 'written to proto file' % field.name) 76 | else: 77 | # Convert default value to string. 78 | if field.variant == messages.Variant.MESSAGE: 79 | logging.warning( 80 | 'Message field %s should not have default values' % field.name) 81 | default = None 82 | elif field.variant == messages.Variant.STRING: 83 | default = repr(field.default_value.encode('utf-8')) 84 | elif field.variant == messages.Variant.BYTES: 85 | default = repr(field.default_value) 86 | else: 87 | default = str(field.default_value) 88 | 89 | if default is not None: 90 | default_format = ' [default=%s]' % default 91 | 92 | if field.variant in (messages.Variant.MESSAGE, messages.Variant.ENUM): 93 | field_type = field.type_name 94 | else: 95 | field_type = str(field.variant).lower() 96 | 97 | out << '%s %s %s = %s%s;' % (str(field.label).lower(), 98 | field_type, 99 | field.name, 100 | field.number, 101 | default_format) 102 | 103 | def write_messages(message_descriptors): 104 | """Write nested and non-nested Message types. 105 | 106 | Args: 107 | message_descriptors: List of MessageDescriptor objects from which to 108 | generate messages. 109 | """ 110 | for message in message_descriptors or []: 111 | out << '' 112 | out << '' 113 | out << 'message %s {' % message.name 114 | 115 | with out.indent(): 116 | if message.enum_types: 117 | write_enums(message.enum_types) 118 | 119 | if message.message_types: 120 | write_messages(message.message_types) 121 | 122 | if message.fields: 123 | write_fields(message.fields) 124 | 125 | out << '}' 126 | 127 | write_messages(file_descriptor.message_types) 128 | -------------------------------------------------------------------------------- /protorpc/generate_proto_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Tests for protorpc.generate_proto_test.""" 19 | 20 | 21 | import os 22 | import shutil 23 | import cStringIO 24 | import sys 25 | import tempfile 26 | import unittest 27 | 28 | from protorpc import descriptor 29 | from protorpc import generate_proto 30 | from protorpc import test_util 31 | from protorpc import util 32 | 33 | 34 | class ModuleInterfaceTest(test_util.ModuleInterfaceTest, 35 | test_util.TestCase): 36 | 37 | MODULE = generate_proto 38 | 39 | 40 | class FormatProtoFileTest(test_util.TestCase): 41 | 42 | def setUp(self): 43 | self.file_descriptor = descriptor.FileDescriptor() 44 | self.output = cStringIO.StringIO() 45 | 46 | @property 47 | def result(self): 48 | return self.output.getvalue() 49 | 50 | def MakeMessage(self, name='MyMessage', fields=[]): 51 | message = descriptor.MessageDescriptor() 52 | message.name = name 53 | message.fields = fields 54 | 55 | messages_list = getattr(self.file_descriptor, 'fields', []) 56 | messages_list.append(message) 57 | self.file_descriptor.message_types = messages_list 58 | 59 | def testBlankPackage(self): 60 | self.file_descriptor.package = None 61 | generate_proto.format_proto_file(self.file_descriptor, self.output) 62 | self.assertEquals('', self.result) 63 | 64 | def testEmptyPackage(self): 65 | self.file_descriptor.package = 'my_package' 66 | generate_proto.format_proto_file(self.file_descriptor, self.output) 67 | self.assertEquals('package my_package;\n', self.result) 68 | 69 | def testSingleField(self): 70 | field = descriptor.FieldDescriptor() 71 | field.name = 'integer_field' 72 | field.number = 1 73 | field.label = descriptor.FieldDescriptor.Label.OPTIONAL 74 | field.variant = descriptor.FieldDescriptor.Variant.INT64 75 | 76 | self.MakeMessage(fields=[field]) 77 | 78 | generate_proto.format_proto_file(self.file_descriptor, self.output) 79 | self.assertEquals('\n\n' 80 | 'message MyMessage {\n' 81 | ' optional int64 integer_field = 1;\n' 82 | '}\n', 83 | self.result) 84 | 85 | def testSingleFieldWithDefault(self): 86 | field = descriptor.FieldDescriptor() 87 | field.name = 'integer_field' 88 | field.number = 1 89 | field.label = descriptor.FieldDescriptor.Label.OPTIONAL 90 | field.variant = descriptor.FieldDescriptor.Variant.INT64 91 | field.default_value = '10' 92 | 93 | self.MakeMessage(fields=[field]) 94 | 95 | generate_proto.format_proto_file(self.file_descriptor, self.output) 96 | self.assertEquals('\n\n' 97 | 'message MyMessage {\n' 98 | ' optional int64 integer_field = 1 [default=10];\n' 99 | '}\n', 100 | self.result) 101 | 102 | def testRepeatedFieldWithDefault(self): 103 | field = descriptor.FieldDescriptor() 104 | field.name = 'integer_field' 105 | field.number = 1 106 | field.label = descriptor.FieldDescriptor.Label.REPEATED 107 | field.variant = descriptor.FieldDescriptor.Variant.INT64 108 | field.default_value = '[10, 20]' 109 | 110 | self.MakeMessage(fields=[field]) 111 | 112 | generate_proto.format_proto_file(self.file_descriptor, self.output) 113 | self.assertEquals('\n\n' 114 | 'message MyMessage {\n' 115 | ' repeated int64 integer_field = 1;\n' 116 | '}\n', 117 | self.result) 118 | 119 | def testSingleFieldWithDefaultString(self): 120 | field = descriptor.FieldDescriptor() 121 | field.name = 'string_field' 122 | field.number = 1 123 | field.label = descriptor.FieldDescriptor.Label.OPTIONAL 124 | field.variant = descriptor.FieldDescriptor.Variant.STRING 125 | field.default_value = 'hello' 126 | 127 | self.MakeMessage(fields=[field]) 128 | 129 | generate_proto.format_proto_file(self.file_descriptor, self.output) 130 | self.assertEquals('\n\n' 131 | 'message MyMessage {\n' 132 | " optional string string_field = 1 [default='hello'];\n" 133 | '}\n', 134 | self.result) 135 | 136 | def testSingleFieldWithDefaultEmptyString(self): 137 | field = descriptor.FieldDescriptor() 138 | field.name = 'string_field' 139 | field.number = 1 140 | field.label = descriptor.FieldDescriptor.Label.OPTIONAL 141 | field.variant = descriptor.FieldDescriptor.Variant.STRING 142 | field.default_value = '' 143 | 144 | self.MakeMessage(fields=[field]) 145 | 146 | generate_proto.format_proto_file(self.file_descriptor, self.output) 147 | self.assertEquals('\n\n' 148 | 'message MyMessage {\n' 149 | " optional string string_field = 1 [default=''];\n" 150 | '}\n', 151 | self.result) 152 | 153 | def testSingleFieldWithDefaultMessage(self): 154 | field = descriptor.FieldDescriptor() 155 | field.name = 'message_field' 156 | field.number = 1 157 | field.label = descriptor.FieldDescriptor.Label.OPTIONAL 158 | field.variant = descriptor.FieldDescriptor.Variant.MESSAGE 159 | field.type_name = 'MyNestedMessage' 160 | field.default_value = 'not valid' 161 | 162 | self.MakeMessage(fields=[field]) 163 | 164 | generate_proto.format_proto_file(self.file_descriptor, self.output) 165 | self.assertEquals('\n\n' 166 | 'message MyMessage {\n' 167 | " optional MyNestedMessage message_field = 1;\n" 168 | '}\n', 169 | self.result) 170 | 171 | def testSingleFieldWithDefaultEnum(self): 172 | field = descriptor.FieldDescriptor() 173 | field.name = 'enum_field' 174 | field.number = 1 175 | field.label = descriptor.FieldDescriptor.Label.OPTIONAL 176 | field.variant = descriptor.FieldDescriptor.Variant.ENUM 177 | field.type_name = 'my_package.MyEnum' 178 | field.default_value = '17' 179 | 180 | self.MakeMessage(fields=[field]) 181 | 182 | generate_proto.format_proto_file(self.file_descriptor, self.output) 183 | self.assertEquals('\n\n' 184 | 'message MyMessage {\n' 185 | " optional my_package.MyEnum enum_field = 1 " 186 | "[default=17];\n" 187 | '}\n', 188 | self.result) 189 | 190 | 191 | def main(): 192 | unittest.main() 193 | 194 | 195 | if __name__ == '__main__': 196 | main() 197 | 198 | -------------------------------------------------------------------------------- /protorpc/generate_python.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from __future__ import with_statement 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | from . import descriptor 23 | from . import generate 24 | from . import message_types 25 | from . import messages 26 | from . import util 27 | 28 | 29 | __all__ = ['format_python_file'] 30 | 31 | _MESSAGE_FIELD_MAP = { 32 | message_types.DateTimeMessage.definition_name(): message_types.DateTimeField, 33 | } 34 | 35 | 36 | def _write_enums(enum_descriptors, out): 37 | """Write nested and non-nested Enum types. 38 | 39 | Args: 40 | enum_descriptors: List of EnumDescriptor objects from which to generate 41 | enums. 42 | out: Indent writer used for generating text. 43 | """ 44 | # Write enums. 45 | for enum in enum_descriptors or []: 46 | out << '' 47 | out << '' 48 | out << 'class %s(messages.Enum):' % enum.name 49 | out << '' 50 | 51 | with out.indent(): 52 | if not enum.values: 53 | out << 'pass' 54 | else: 55 | for enum_value in enum.values: 56 | out << '%s = %s' % (enum_value.name, enum_value.number) 57 | 58 | 59 | def _write_fields(field_descriptors, out): 60 | """Write fields for Message types. 61 | 62 | Args: 63 | field_descriptors: List of FieldDescriptor objects from which to generate 64 | fields. 65 | out: Indent writer used for generating text. 66 | """ 67 | out << '' 68 | for field in field_descriptors or []: 69 | type_format = '' 70 | label_format = '' 71 | 72 | message_field = _MESSAGE_FIELD_MAP.get(field.type_name) 73 | if message_field: 74 | module = 'message_types' 75 | field_type = message_field 76 | else: 77 | module = 'messages' 78 | field_type = messages.Field.lookup_field_type_by_variant(field.variant) 79 | 80 | if field_type in (messages.EnumField, messages.MessageField): 81 | type_format = '\'%s\', ' % field.type_name 82 | 83 | if field.label == descriptor.FieldDescriptor.Label.REQUIRED: 84 | label_format = ', required=True' 85 | 86 | elif field.label == descriptor.FieldDescriptor.Label.REPEATED: 87 | label_format = ', repeated=True' 88 | 89 | if field_type.DEFAULT_VARIANT != field.variant: 90 | variant_format = ', variant=messages.Variant.%s' % field.variant 91 | else: 92 | variant_format = '' 93 | 94 | if field.default_value: 95 | if field_type in [messages.BytesField, 96 | messages.StringField, 97 | ]: 98 | default_value = repr(field.default_value) 99 | elif field_type is messages.EnumField: 100 | try: 101 | default_value = str(int(field.default_value)) 102 | except ValueError: 103 | default_value = repr(field.default_value) 104 | else: 105 | default_value = field.default_value 106 | 107 | default_format = ', default=%s' % (default_value,) 108 | else: 109 | default_format = '' 110 | 111 | out << '%s = %s.%s(%s%s%s%s%s)' % (field.name, 112 | module, 113 | field_type.__name__, 114 | type_format, 115 | field.number, 116 | label_format, 117 | variant_format, 118 | default_format) 119 | 120 | 121 | def _write_messages(message_descriptors, out): 122 | """Write nested and non-nested Message types. 123 | 124 | Args: 125 | message_descriptors: List of MessageDescriptor objects from which to 126 | generate messages. 127 | out: Indent writer used for generating text. 128 | """ 129 | for message in message_descriptors or []: 130 | out << '' 131 | out << '' 132 | out << 'class %s(messages.Message):' % message.name 133 | 134 | with out.indent(): 135 | if not (message.enum_types or message.message_types or message.fields): 136 | out << '' 137 | out << 'pass' 138 | else: 139 | _write_enums(message.enum_types, out) 140 | _write_messages(message.message_types, out) 141 | _write_fields(message.fields, out) 142 | 143 | 144 | def _write_methods(method_descriptors, out): 145 | """Write methods of Service types. 146 | 147 | All service method implementations raise NotImplementedError. 148 | 149 | Args: 150 | method_descriptors: List of MethodDescriptor objects from which to 151 | generate methods. 152 | out: Indent writer used for generating text. 153 | """ 154 | for method in method_descriptors: 155 | out << '' 156 | out << "@remote.method('%s', '%s')" % (method.request_type, 157 | method.response_type) 158 | out << 'def %s(self, request):' % (method.name,) 159 | with out.indent(): 160 | out << ('raise NotImplementedError' 161 | "('Method %s is not implemented')" % (method.name)) 162 | 163 | 164 | def _write_services(service_descriptors, out): 165 | """Write Service types. 166 | 167 | Args: 168 | service_descriptors: List of ServiceDescriptor instances from which to 169 | generate services. 170 | out: Indent writer used for generating text. 171 | """ 172 | for service in service_descriptors or []: 173 | out << '' 174 | out << '' 175 | out << 'class %s(remote.Service):' % service.name 176 | 177 | with out.indent(): 178 | if service.methods: 179 | _write_methods(service.methods, out) 180 | else: 181 | out << '' 182 | out << 'pass' 183 | 184 | 185 | @util.positional(2) 186 | def format_python_file(file_descriptor, output, indent_space=2): 187 | """Format FileDescriptor object as a single Python module. 188 | 189 | Services generated by this function will raise NotImplementedError. 190 | 191 | All Python classes generated by this function use delayed binding for all 192 | message fields, enum fields and method parameter types. For example a 193 | service method might be generated like so: 194 | 195 | class MyService(remote.Service): 196 | 197 | @remote.method('my_package.MyRequestType', 'my_package.MyResponseType') 198 | def my_method(self, request): 199 | raise NotImplementedError('Method my_method is not implemented') 200 | 201 | Args: 202 | file_descriptor: FileDescriptor instance to format as python module. 203 | output: File-like object to write module source code to. 204 | indent_space: Number of spaces for each level of Python indentation. 205 | """ 206 | out = generate.IndentWriter(output, indent_space=indent_space) 207 | 208 | out << 'from protorpc import message_types' 209 | out << 'from protorpc import messages' 210 | if file_descriptor.service_types: 211 | out << 'from protorpc import remote' 212 | 213 | if file_descriptor.package: 214 | out << "package = '%s'" % file_descriptor.package 215 | 216 | _write_enums(file_descriptor.enum_types, out) 217 | _write_messages(file_descriptor.message_types, out) 218 | _write_services(file_descriptor.service_types, out) 219 | -------------------------------------------------------------------------------- /protorpc/generate_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Tests for protorpc.generate.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | import operator 23 | 24 | import cStringIO 25 | import sys 26 | import unittest 27 | 28 | from protorpc import generate 29 | from protorpc import test_util 30 | 31 | 32 | class ModuleInterfaceTest(test_util.ModuleInterfaceTest, 33 | test_util.TestCase): 34 | 35 | MODULE = generate 36 | 37 | 38 | class IndentWriterTest(test_util.TestCase): 39 | 40 | def setUp(self): 41 | self.out = cStringIO.StringIO() 42 | self.indent_writer = generate.IndentWriter(self.out) 43 | 44 | def testWriteLine(self): 45 | self.indent_writer.write_line('This is a line') 46 | self.indent_writer.write_line('This is another line') 47 | 48 | self.assertEquals('This is a line\n' 49 | 'This is another line\n', 50 | self.out.getvalue()) 51 | 52 | def testLeftShift(self): 53 | self.run_count = 0 54 | def mock_write_line(line): 55 | self.run_count += 1 56 | self.assertEquals('same as calling write_line', line) 57 | 58 | self.indent_writer.write_line = mock_write_line 59 | self.indent_writer << 'same as calling write_line' 60 | self.assertEquals(1, self.run_count) 61 | 62 | def testIndentation(self): 63 | self.indent_writer << 'indent 0' 64 | self.indent_writer.begin_indent() 65 | self.indent_writer << 'indent 1' 66 | self.indent_writer.begin_indent() 67 | self.indent_writer << 'indent 2' 68 | self.indent_writer.end_indent() 69 | self.indent_writer << 'end 2' 70 | self.indent_writer.end_indent() 71 | self.indent_writer << 'end 1' 72 | self.assertRaises(generate.IndentationError, 73 | self.indent_writer.end_indent) 74 | 75 | self.assertEquals('indent 0\n' 76 | ' indent 1\n' 77 | ' indent 2\n' 78 | ' end 2\n' 79 | 'end 1\n', 80 | self.out.getvalue()) 81 | 82 | def testBlankLine(self): 83 | self.indent_writer << '' 84 | self.indent_writer.begin_indent() 85 | self.indent_writer << '' 86 | self.assertEquals('\n\n', self.out.getvalue()) 87 | 88 | def testNoneInvalid(self): 89 | self.assertRaises( 90 | TypeError, operator.lshift, self.indent_writer, None) 91 | 92 | def testAltIndentation(self): 93 | self.indent_writer = generate.IndentWriter(self.out, indent_space=3) 94 | self.indent_writer << 'indent 0' 95 | self.assertEquals(0, self.indent_writer.indent_level) 96 | self.indent_writer.begin_indent() 97 | self.indent_writer << 'indent 1' 98 | self.assertEquals(1, self.indent_writer.indent_level) 99 | self.indent_writer.begin_indent() 100 | self.indent_writer << 'indent 2' 101 | self.assertEquals(2, self.indent_writer.indent_level) 102 | self.indent_writer.end_indent() 103 | self.indent_writer << 'end 2' 104 | self.assertEquals(1, self.indent_writer.indent_level) 105 | self.indent_writer.end_indent() 106 | self.indent_writer << 'end 1' 107 | self.assertEquals(0, self.indent_writer.indent_level) 108 | self.assertRaises(generate.IndentationError, 109 | self.indent_writer.end_indent) 110 | self.assertEquals(0, self.indent_writer.indent_level) 111 | 112 | self.assertEquals('indent 0\n' 113 | ' indent 1\n' 114 | ' indent 2\n' 115 | ' end 2\n' 116 | 'end 1\n', 117 | self.out.getvalue()) 118 | 119 | def testIndent(self): 120 | self.indent_writer << 'indent 0' 121 | self.assertEquals(0, self.indent_writer.indent_level) 122 | 123 | def indent1(): 124 | self.indent_writer << 'indent 1' 125 | self.assertEquals(1, self.indent_writer.indent_level) 126 | 127 | def indent2(): 128 | self.indent_writer << 'indent 2' 129 | self.assertEquals(2, self.indent_writer.indent_level) 130 | test_util.do_with(self.indent_writer.indent(), indent2) 131 | 132 | self.assertEquals(1, self.indent_writer.indent_level) 133 | self.indent_writer << 'end 2' 134 | test_util.do_with(self.indent_writer.indent(), indent1) 135 | 136 | self.assertEquals(0, self.indent_writer.indent_level) 137 | self.indent_writer << 'end 1' 138 | 139 | self.assertEquals('indent 0\n' 140 | ' indent 1\n' 141 | ' indent 2\n' 142 | ' end 2\n' 143 | 'end 1\n', 144 | self.out.getvalue()) 145 | 146 | 147 | def main(): 148 | unittest.main() 149 | 150 | 151 | if __name__ == '__main__': 152 | main() 153 | -------------------------------------------------------------------------------- /protorpc/google_imports.py: -------------------------------------------------------------------------------- 1 | """Dynamically decide from where to import other SDK modules. 2 | 3 | All other protorpc code should import other SDK modules from 4 | this module. If necessary, add new imports here (in both places). 5 | """ 6 | 7 | __author__ = 'yey@google.com (Ye Yuan)' 8 | 9 | # pylint: disable=g-import-not-at-top 10 | # pylint: disable=unused-import 11 | 12 | try: 13 | from google.net.proto import ProtocolBuffer 14 | except ImportError: 15 | pass 16 | -------------------------------------------------------------------------------- /protorpc/message_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Simple protocol message types. 19 | 20 | Includes new message and field types that are outside what is defined by the 21 | protocol buffers standard. 22 | """ 23 | 24 | __author__ = 'rafek@google.com (Rafe Kaplan)' 25 | 26 | import datetime 27 | 28 | from . import messages 29 | from . import util 30 | 31 | __all__ = [ 32 | 'DateTimeField', 33 | 'DateTimeMessage', 34 | 'VoidMessage', 35 | ] 36 | 37 | class VoidMessage(messages.Message): 38 | """Empty message.""" 39 | 40 | 41 | class DateTimeMessage(messages.Message): 42 | """Message to store/transmit a DateTime. 43 | 44 | Fields: 45 | milliseconds: Milliseconds since Jan 1st 1970 local time. 46 | time_zone_offset: Optional time zone offset, in minutes from UTC. 47 | """ 48 | milliseconds = messages.IntegerField(1, required=True) 49 | time_zone_offset = messages.IntegerField(2) 50 | 51 | 52 | class DateTimeField(messages.MessageField): 53 | """Field definition for datetime values. 54 | 55 | Stores a python datetime object as a field. If time zone information is 56 | included in the datetime object, it will be included in 57 | the encoded data when this is encoded/decoded. 58 | """ 59 | 60 | type = datetime.datetime 61 | 62 | message_type = DateTimeMessage 63 | 64 | @util.positional(3) 65 | def __init__(self, 66 | number, 67 | **kwargs): 68 | super(DateTimeField, self).__init__(self.message_type, 69 | number, 70 | **kwargs) 71 | 72 | def value_from_message(self, message): 73 | """Convert DateTimeMessage to a datetime. 74 | 75 | Args: 76 | A DateTimeMessage instance. 77 | 78 | Returns: 79 | A datetime instance. 80 | """ 81 | message = super(DateTimeField, self).value_from_message(message) 82 | if message.time_zone_offset is None: 83 | return datetime.datetime.utcfromtimestamp(message.milliseconds / 1000.0) 84 | 85 | # Need to subtract the time zone offset, because when we call 86 | # datetime.fromtimestamp, it will add the time zone offset to the 87 | # value we pass. 88 | milliseconds = (message.milliseconds - 89 | 60000 * message.time_zone_offset) 90 | 91 | timezone = util.TimeZoneOffset(message.time_zone_offset) 92 | return datetime.datetime.fromtimestamp(milliseconds / 1000.0, 93 | tz=timezone) 94 | 95 | def value_to_message(self, value): 96 | value = super(DateTimeField, self).value_to_message(value) 97 | # First, determine the delta from the epoch, so we can fill in 98 | # DateTimeMessage's milliseconds field. 99 | if value.tzinfo is None: 100 | time_zone_offset = 0 101 | local_epoch = datetime.datetime.utcfromtimestamp(0) 102 | else: 103 | time_zone_offset = util.total_seconds(value.tzinfo.utcoffset(value)) 104 | # Determine Jan 1, 1970 local time. 105 | local_epoch = datetime.datetime.fromtimestamp(-time_zone_offset, 106 | tz=value.tzinfo) 107 | delta = value - local_epoch 108 | 109 | # Create and fill in the DateTimeMessage, including time zone if 110 | # one was specified. 111 | message = DateTimeMessage() 112 | message.milliseconds = int(util.total_seconds(delta) * 1000) 113 | if value.tzinfo is not None: 114 | utc_offset = value.tzinfo.utcoffset(value) 115 | if utc_offset is not None: 116 | message.time_zone_offset = int( 117 | util.total_seconds(value.tzinfo.utcoffset(value)) / 60) 118 | 119 | return message 120 | -------------------------------------------------------------------------------- /protorpc/message_types_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2013 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Tests for protorpc.message_types.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | 23 | import datetime 24 | 25 | import unittest 26 | 27 | from protorpc import message_types 28 | from protorpc import messages 29 | from protorpc import test_util 30 | from protorpc import util 31 | 32 | 33 | class ModuleInterfaceTest(test_util.ModuleInterfaceTest, 34 | test_util.TestCase): 35 | 36 | MODULE = message_types 37 | 38 | 39 | class DateTimeFieldTest(test_util.TestCase): 40 | 41 | def testValueToMessage(self): 42 | field = message_types.DateTimeField(1) 43 | message = field.value_to_message(datetime.datetime(2033, 2, 4, 11, 22, 10)) 44 | self.assertEqual(message_types.DateTimeMessage(milliseconds=1991128930000), 45 | message) 46 | 47 | def testValueToMessageBadValue(self): 48 | field = message_types.DateTimeField(1) 49 | self.assertRaisesWithRegexpMatch( 50 | messages.EncodeError, 51 | 'Expected type datetime, got int: 20', 52 | field.value_to_message, 20) 53 | 54 | def testValueToMessageWithTimeZone(self): 55 | time_zone = util.TimeZoneOffset(60 * 10) 56 | field = message_types.DateTimeField(1) 57 | message = field.value_to_message( 58 | datetime.datetime(2033, 2, 4, 11, 22, 10, tzinfo=time_zone)) 59 | self.assertEqual(message_types.DateTimeMessage(milliseconds=1991128930000, 60 | time_zone_offset=600), 61 | message) 62 | 63 | def testValueFromMessage(self): 64 | message = message_types.DateTimeMessage(milliseconds=1991128000000) 65 | field = message_types.DateTimeField(1) 66 | timestamp = field.value_from_message(message) 67 | self.assertEqual(datetime.datetime(2033, 2, 4, 11, 6, 40), 68 | timestamp) 69 | 70 | def testValueFromMessageBadValue(self): 71 | field = message_types.DateTimeField(1) 72 | self.assertRaisesWithRegexpMatch( 73 | messages.DecodeError, 74 | 'Expected type DateTimeMessage, got VoidMessage: ', 75 | field.value_from_message, message_types.VoidMessage()) 76 | 77 | def testValueFromMessageWithTimeZone(self): 78 | message = message_types.DateTimeMessage(milliseconds=1991128000000, 79 | time_zone_offset=300) 80 | field = message_types.DateTimeField(1) 81 | timestamp = field.value_from_message(message) 82 | time_zone = util.TimeZoneOffset(60 * 5) 83 | self.assertEqual(datetime.datetime(2033, 2, 4, 11, 6, 40, tzinfo=time_zone), 84 | timestamp) 85 | 86 | 87 | if __name__ == '__main__': 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /protorpc/non_sdk_imports.py: -------------------------------------------------------------------------------- 1 | """Dynamically decide from where to import other non SDK Google modules. 2 | 3 | All other protorpc code should import other non SDK modules from 4 | this module. If necessary, add new imports here (in both places). 5 | """ 6 | 7 | __author__ = 'yey@google.com (Ye Yuan)' 8 | 9 | # pylint: disable=g-import-not-at-top 10 | # pylint: disable=unused-import 11 | 12 | try: 13 | from google.protobuf import descriptor 14 | normal_environment = True 15 | except ImportError: 16 | normal_environment = False 17 | 18 | if normal_environment: 19 | from google.protobuf import descriptor_pb2 20 | from google.protobuf import message 21 | from google.protobuf import reflection 22 | -------------------------------------------------------------------------------- /protorpc/protorpc_test.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2010 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package protorpc; 17 | 18 | // Message used to nest inside another message. 19 | message NestedMessage { 20 | required string a_value = 1; 21 | } 22 | 23 | // Message that contains nested messages. 24 | message HasNestedMessage { 25 | optional NestedMessage nested = 1; 26 | repeated NestedMessage repeated_nested = 2; 27 | } 28 | 29 | message HasDefault { 30 | optional string a_value = 1 [default="a default"]; 31 | } 32 | 33 | // Message that contains all variants as optional fields. 34 | message OptionalMessage { 35 | enum SimpleEnum { 36 | VAL1 = 1; 37 | VAL2 = 2; 38 | } 39 | 40 | optional double double_value = 1; 41 | optional float float_value = 2; 42 | optional int64 int64_value = 3; 43 | optional uint64 uint64_value = 4; 44 | optional int32 int32_value = 5; 45 | optional bool bool_value = 6; 46 | optional string string_value = 7; 47 | optional bytes bytes_value = 8; 48 | optional SimpleEnum enum_value = 10; 49 | 50 | // TODO(rafek): Add support for these variants. 51 | // optional uint32 uint32_value = 9; 52 | // optional sint32 sint32_value = 11; 53 | // optional sint64 sint64_value = 12; 54 | } 55 | 56 | // Message that contains all variants as repeated fields. 57 | message RepeatedMessage { 58 | enum SimpleEnum { 59 | VAL1 = 1; 60 | VAL2 = 2; 61 | } 62 | 63 | repeated double double_value = 1; 64 | repeated float float_value = 2; 65 | repeated int64 int64_value = 3; 66 | repeated uint64 uint64_value = 4; 67 | repeated int32 int32_value = 5; 68 | repeated bool bool_value = 6; 69 | repeated string string_value = 7; 70 | repeated bytes bytes_value = 8; 71 | repeated SimpleEnum enum_value = 10; 72 | 73 | // TODO(rafek): Add support for these variants. 74 | // repeated uint32 uint32_value = 9; 75 | // repeated sint32 sint32_value = 11; 76 | // repeated sint64 sint64_value = 12; 77 | } 78 | 79 | // Message that has nested message with all optional fields. 80 | message HasOptionalNestedMessage { 81 | optional OptionalMessage nested = 1; 82 | repeated OptionalMessage repeated_nested = 2; 83 | } 84 | -------------------------------------------------------------------------------- /protorpc/registry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Service regsitry for service discovery. 19 | 20 | The registry service can be deployed on a server in order to provide a 21 | central place where remote clients can discover available. 22 | 23 | On the server side, each service is registered by their name which is unique 24 | to the registry. Typically this name provides enough information to identify 25 | the service and locate it within a server. For example, for an HTTP based 26 | registry the name is the URL path on the host where the service is invocable. 27 | 28 | The registry is also able to resolve the full descriptor.FileSet necessary to 29 | describe the service and all required data-types (messages and enums). 30 | 31 | A configured registry is itself a remote service and should reference itself. 32 | """ 33 | 34 | import sys 35 | 36 | from . import descriptor 37 | from . import messages 38 | from . import remote 39 | from . import util 40 | 41 | 42 | __all__ = [ 43 | 'ServiceMapping', 44 | 'ServicesResponse', 45 | 'GetFileSetRequest', 46 | 'GetFileSetResponse', 47 | 'RegistryService', 48 | ] 49 | 50 | 51 | class ServiceMapping(messages.Message): 52 | """Description of registered service. 53 | 54 | Fields: 55 | name: Name of service. On HTTP based services this will be the 56 | URL path used for invocation. 57 | definition: Fully qualified name of the service definition. Useful 58 | for clients that can look up service definitions based on an existing 59 | repository of definitions. 60 | """ 61 | 62 | name = messages.StringField(1, required=True) 63 | definition = messages.StringField(2, required=True) 64 | 65 | 66 | class ServicesResponse(messages.Message): 67 | """Response containing all registered services. 68 | 69 | May also contain complete descriptor file-set for all services known by the 70 | registry. 71 | 72 | Fields: 73 | services: Service mappings for all registered services in registry. 74 | file_set: Descriptor file-set describing all services, messages and enum 75 | types needed for use with all requested services if asked for in the 76 | request. 77 | """ 78 | 79 | services = messages.MessageField(ServiceMapping, 1, repeated=True) 80 | 81 | 82 | class GetFileSetRequest(messages.Message): 83 | """Request for service descriptor file-set. 84 | 85 | Request to retrieve file sets for specific services. 86 | 87 | Fields: 88 | names: Names of services to retrieve file-set for. 89 | """ 90 | 91 | names = messages.StringField(1, repeated=True) 92 | 93 | 94 | class GetFileSetResponse(messages.Message): 95 | """Descriptor file-set for all names in GetFileSetRequest. 96 | 97 | Fields: 98 | file_set: Descriptor file-set containing all descriptors for services, 99 | messages and enum types needed for listed names in request. 100 | """ 101 | 102 | file_set = messages.MessageField(descriptor.FileSet, 1, required=True) 103 | 104 | 105 | class RegistryService(remote.Service): 106 | """Registry service. 107 | 108 | Maps names to services and is able to describe all descriptor file-sets 109 | necessary to use contined services. 110 | 111 | On an HTTP based server, the name is the URL path to the service. 112 | """ 113 | 114 | @util.positional(2) 115 | def __init__(self, registry, modules=None): 116 | """Constructor. 117 | 118 | Args: 119 | registry: Map of name to service class. This map is not copied and may 120 | be modified after the reigstry service has been configured. 121 | modules: Module dict to draw descriptors from. Defaults to sys.modules. 122 | """ 123 | # Private Attributes: 124 | # __registry: Map of name to service class. Refers to same instance as 125 | # registry parameter. 126 | # __modules: Mapping of module name to module. 127 | # __definition_to_modules: Mapping of definition types to set of modules 128 | # that they refer to. This cache is used to make repeated look-ups 129 | # faster and to prevent circular references from causing endless loops. 130 | 131 | self.__registry = registry 132 | if modules is None: 133 | modules = sys.modules 134 | self.__modules = modules 135 | # This cache will only last for a single request. 136 | self.__definition_to_modules = {} 137 | 138 | def __find_modules_for_message(self, message_type): 139 | """Find modules referred to by a message type. 140 | 141 | Determines the entire list of modules ultimately referred to by message_type 142 | by iterating over all of its message and enum fields. Includes modules 143 | referred to fields within its referred messages. 144 | 145 | Args: 146 | message_type: Message type to find all referring modules for. 147 | 148 | Returns: 149 | Set of modules referred to by message_type by traversing all its 150 | message and enum fields. 151 | """ 152 | # TODO(rafek): Maybe this should be a method on Message and Service? 153 | def get_dependencies(message_type, seen=None): 154 | """Get all dependency definitions of a message type. 155 | 156 | This function works by collecting the types of all enumeration and message 157 | fields defined within the message type. When encountering a message 158 | field, it will recursivly find all of the associated message's 159 | dependencies. It will terminate on circular dependencies by keeping track 160 | of what definitions it already via the seen set. 161 | 162 | Args: 163 | message_type: Message type to get dependencies for. 164 | seen: Set of definitions that have already been visited. 165 | 166 | Returns: 167 | All dependency message and enumerated types associated with this message 168 | including the message itself. 169 | """ 170 | if seen is None: 171 | seen = set() 172 | seen.add(message_type) 173 | 174 | for field in message_type.all_fields(): 175 | if isinstance(field, messages.MessageField): 176 | if field.message_type not in seen: 177 | get_dependencies(field.message_type, seen) 178 | elif isinstance(field, messages.EnumField): 179 | seen.add(field.type) 180 | 181 | return seen 182 | 183 | found_modules = self.__definition_to_modules.setdefault(message_type, set()) 184 | if not found_modules: 185 | dependencies = get_dependencies(message_type) 186 | found_modules.update(self.__modules[definition.__module__] 187 | for definition in dependencies) 188 | 189 | return found_modules 190 | 191 | def __describe_file_set(self, names): 192 | """Get file-set for named services. 193 | 194 | Args: 195 | names: List of names to get file-set for. 196 | 197 | Returns: 198 | descriptor.FileSet containing all the descriptors for all modules 199 | ultimately referred to by all service types request by names parameter. 200 | """ 201 | service_modules = set() 202 | if names: 203 | for service in (self.__registry[name] for name in names): 204 | found_modules = self.__definition_to_modules.setdefault(service, set()) 205 | if not found_modules: 206 | found_modules.add(self.__modules[service.__module__]) 207 | for method_name in service.all_remote_methods(): 208 | method = getattr(service, method_name) 209 | for message_type in (method.remote.request_type, 210 | method.remote.response_type): 211 | found_modules.update( 212 | self.__find_modules_for_message(message_type)) 213 | service_modules.update(found_modules) 214 | 215 | return descriptor.describe_file_set(service_modules) 216 | 217 | @property 218 | def registry(self): 219 | """Get service registry associated with this service instance.""" 220 | return self.__registry 221 | 222 | @remote.method(response_type=ServicesResponse) 223 | def services(self, request): 224 | """Get all registered services.""" 225 | response = ServicesResponse() 226 | response.services = [] 227 | for name, service_class in self.__registry.items(): 228 | mapping = ServiceMapping() 229 | mapping.name = name.decode('utf-8') 230 | mapping.definition = service_class.definition_name().decode('utf-8') 231 | response.services.append(mapping) 232 | 233 | return response 234 | 235 | @remote.method(GetFileSetRequest, GetFileSetResponse) 236 | def get_file_set(self, request): 237 | """Get file-set for registered servies.""" 238 | response = GetFileSetResponse() 239 | response.file_set = self.__describe_file_set(request.names) 240 | return response 241 | -------------------------------------------------------------------------------- /protorpc/registry_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Tests for protorpc.message.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | 23 | import sys 24 | import unittest 25 | 26 | from protorpc import descriptor 27 | from protorpc import message_types 28 | from protorpc import messages 29 | from protorpc import registry 30 | from protorpc import remote 31 | from protorpc import test_util 32 | 33 | 34 | class ModuleInterfaceTest(test_util.ModuleInterfaceTest, 35 | test_util.TestCase): 36 | 37 | MODULE = registry 38 | 39 | 40 | class MyService1(remote.Service): 41 | """Test service that refers to messages in another module.""" 42 | 43 | @remote.method(test_util.NestedMessage, test_util.NestedMessage) 44 | def a_method(self, request): 45 | pass 46 | 47 | 48 | class MyService2(remote.Service): 49 | """Test service that does not refer to messages in another module.""" 50 | 51 | 52 | class RegistryServiceTest(test_util.TestCase): 53 | 54 | def setUp(self): 55 | self.registry = { 56 | 'my-service1': MyService1, 57 | 'my-service2': MyService2, 58 | } 59 | 60 | self.modules = { 61 | __name__: sys.modules[__name__], 62 | test_util.__name__: test_util, 63 | } 64 | 65 | self.registry_service = registry.RegistryService(self.registry, 66 | modules=self.modules) 67 | 68 | def CheckServiceMappings(self, mappings): 69 | module_name = test_util.get_module_name(RegistryServiceTest) 70 | service1_mapping = registry.ServiceMapping() 71 | service1_mapping.name = 'my-service1' 72 | service1_mapping.definition = '%s.MyService1' % module_name 73 | 74 | service2_mapping = registry.ServiceMapping() 75 | service2_mapping.name = 'my-service2' 76 | service2_mapping.definition = '%s.MyService2' % module_name 77 | 78 | self.assertIterEqual(mappings, [service1_mapping, service2_mapping]) 79 | 80 | def testServices(self): 81 | response = self.registry_service.services(message_types.VoidMessage()) 82 | 83 | self.CheckServiceMappings(response.services) 84 | 85 | def testGetFileSet_All(self): 86 | request = registry.GetFileSetRequest() 87 | request.names = ['my-service1', 'my-service2'] 88 | response = self.registry_service.get_file_set(request) 89 | 90 | expected_file_set = descriptor.describe_file_set(list(self.modules.values())) 91 | self.assertIterEqual(expected_file_set.files, response.file_set.files) 92 | 93 | def testGetFileSet_None(self): 94 | request = registry.GetFileSetRequest() 95 | response = self.registry_service.get_file_set(request) 96 | 97 | self.assertEquals(descriptor.FileSet(), 98 | response.file_set) 99 | 100 | def testGetFileSet_ReferenceOtherModules(self): 101 | request = registry.GetFileSetRequest() 102 | request.names = ['my-service1'] 103 | response = self.registry_service.get_file_set(request) 104 | 105 | # Will suck in and describe the test_util module. 106 | expected_file_set = descriptor.describe_file_set(list(self.modules.values())) 107 | self.assertIterEqual(expected_file_set.files, response.file_set.files) 108 | 109 | def testGetFileSet_DoNotReferenceOtherModules(self): 110 | request = registry.GetFileSetRequest() 111 | request.names = ['my-service2'] 112 | response = self.registry_service.get_file_set(request) 113 | 114 | # Service does not reference test_util, so will only describe this module. 115 | expected_file_set = descriptor.describe_file_set([self.modules[__name__]]) 116 | self.assertIterEqual(expected_file_set.files, response.file_set.files) 117 | 118 | 119 | def main(): 120 | unittest.main() 121 | 122 | 123 | if __name__ == '__main__': 124 | main() 125 | -------------------------------------------------------------------------------- /protorpc/static/base.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | {% block title%}Need title{% endblock %} 21 | 24 | 27 | 30 | 42 | 47 | 48 | 49 | 50 | {% block top %}Need top{% endblock %} 51 | 52 |
53 | 54 | {% block body %}Need body{% endblock %} 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /protorpc/static/forms.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | {% extends 'base.html' %} 18 | 19 | {% block title %}ProtoRPC Methods for {{hostname|escape}}{% endblock %} 20 | 21 | {% block top %} 22 |

ProtoRPC Methods for {{hostname|escape}}

23 | {% endblock %} 24 | 25 | {% block body %} 26 |
27 | {% endblock %} 28 | 29 | {% block call %} 30 | loadServices(showMethods); 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /protorpc/static/jquery.json-2.2.min.js: -------------------------------------------------------------------------------- 1 | 2 | (function($){$.toJSON=function(o) 3 | {if(typeof(JSON)=='object'&&JSON.stringify) 4 | return JSON.stringify(o);var type=typeof(o);if(o===null) 5 | return"null";if(type=="undefined") 6 | return undefined;if(type=="number"||type=="boolean") 7 | return o+"";if(type=="string") 8 | return $.quoteString(o);if(type=='object') 9 | {if(typeof o.toJSON=="function") 10 | return $.toJSON(o.toJSON());if(o.constructor===Date) 11 | {var month=o.getUTCMonth()+1;if(month<10)month='0'+month;var day=o.getUTCDate();if(day<10)day='0'+day;var year=o.getUTCFullYear();var hours=o.getUTCHours();if(hours<10)hours='0'+hours;var minutes=o.getUTCMinutes();if(minutes<10)minutes='0'+minutes;var seconds=o.getUTCSeconds();if(seconds<10)seconds='0'+seconds;var milli=o.getUTCMilliseconds();if(milli<100)milli='0'+milli;if(milli<10)milli='0'+milli;return'"'+year+'-'+month+'-'+day+'T'+ 12 | hours+':'+minutes+':'+seconds+'.'+milli+'Z"';} 13 | if(o.constructor===Array) 14 | {var ret=[];for(var i=0;i 16 | 17 | {% extends 'base.html' %} 18 | 19 | {% block title %}Form for {{service_path|escape}}.{{method_name|escape}}{% endblock %} 20 | 21 | {% block top %} 22 | << Back to method selection 23 |

Form for {{service_path|escape}}.{{method_name|escape}}

24 | {% endblock %} 25 | 26 | {% block body %} 27 | 28 |
29 |
30 |
31 | 32 |
33 | {% endblock %} 34 | 35 | {% block call %} 36 | loadServices(createForm); 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /protorpc/webapp/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | __author__ = 'rafek@google.com (Rafe Kaplan)' 19 | -------------------------------------------------------------------------------- /protorpc/webapp/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Webapp forms interface to ProtoRPC services. 19 | 20 | This webapp application is automatically configured to work with ProtoRPCs 21 | that have a configured protorpc.RegistryService. This webapp is 22 | automatically added to the registry service URL at /forms 23 | (default is /protorpc/form) when configured using the 24 | service_handlers.service_mapping function. 25 | """ 26 | 27 | import os 28 | 29 | from .google_imports import template 30 | from .google_imports import webapp 31 | 32 | 33 | __all__ = ['FormsHandler', 34 | 'ResourceHandler', 35 | 36 | 'DEFAULT_REGISTRY_PATH', 37 | ] 38 | 39 | _TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 40 | 'static') 41 | 42 | _FORMS_TEMPLATE = os.path.join(_TEMPLATES_DIR, 'forms.html') 43 | _METHODS_TEMPLATE = os.path.join(_TEMPLATES_DIR, 'methods.html') 44 | 45 | DEFAULT_REGISTRY_PATH = '/protorpc' 46 | 47 | 48 | class ResourceHandler(webapp.RequestHandler): 49 | """Serves static resources without needing to add static files to app.yaml.""" 50 | 51 | __RESOURCE_MAP = { 52 | 'forms.js': 'text/javascript', 53 | 'jquery-1.4.2.min.js': 'text/javascript', 54 | 'jquery.json-2.2.min.js': 'text/javascript', 55 | } 56 | 57 | def get(self, relative): 58 | """Serve known static files. 59 | 60 | If static file is not known, will return 404 to client. 61 | 62 | Response items are cached for 300 seconds. 63 | 64 | Args: 65 | relative: Name of static file relative to main FormsHandler. 66 | """ 67 | content_type = self.__RESOURCE_MAP.get(relative, None) 68 | if not content_type: 69 | self.response.set_status(404) 70 | self.response.out.write('Resource not found.') 71 | return 72 | 73 | path = os.path.join(_TEMPLATES_DIR, relative) 74 | self.response.headers['Content-Type'] = content_type 75 | static_file = open(path) 76 | try: 77 | contents = static_file.read() 78 | finally: 79 | static_file.close() 80 | self.response.out.write(contents) 81 | 82 | 83 | class FormsHandler(webapp.RequestHandler): 84 | """Handler for display HTML/javascript forms of ProtoRPC method calls. 85 | 86 | When accessed with no query parameters, will show a web page that displays 87 | all services and methods on the associated registry path. Links on this 88 | page fill in the service_path and method_name query parameters back to this 89 | same handler. 90 | 91 | When provided with service_path and method_name parameters will display a 92 | dynamic form representing the request message for that method. When sent, 93 | the form sends a JSON request to the ProtoRPC method and displays the 94 | response in the HTML page. 95 | 96 | Attribute: 97 | registry_path: Read-only registry path known by this handler. 98 | """ 99 | 100 | def __init__(self, registry_path=DEFAULT_REGISTRY_PATH): 101 | """Constructor. 102 | 103 | When configuring a FormsHandler to use with a webapp application do not 104 | pass the request handler class in directly. Instead use new_factory to 105 | ensure that the FormsHandler is created with the correct registry path 106 | for each request. 107 | 108 | Args: 109 | registry_path: Absolute path on server where the ProtoRPC RegsitryService 110 | is located. 111 | """ 112 | assert registry_path 113 | self.__registry_path = registry_path 114 | 115 | @property 116 | def registry_path(self): 117 | return self.__registry_path 118 | 119 | def get(self): 120 | """Send forms and method page to user. 121 | 122 | By default, displays a web page listing all services and methods registered 123 | on the server. Methods have links to display the actual method form. 124 | 125 | If both parameters are set, will display form for method. 126 | 127 | Query Parameters: 128 | service_path: Path to service to display method of. Optional. 129 | method_name: Name of method to display form for. Optional. 130 | """ 131 | params = {'forms_path': self.request.path.rstrip('/'), 132 | 'hostname': self.request.host, 133 | 'registry_path': self.__registry_path, 134 | } 135 | service_path = self.request.get('path', None) 136 | method_name = self.request.get('method', None) 137 | 138 | if service_path and method_name: 139 | form_template = _METHODS_TEMPLATE 140 | params['service_path'] = service_path 141 | params['method_name'] = method_name 142 | else: 143 | form_template = _FORMS_TEMPLATE 144 | 145 | self.response.out.write(template.render(form_template, params)) 146 | 147 | @classmethod 148 | def new_factory(cls, registry_path=DEFAULT_REGISTRY_PATH): 149 | """Construct a factory for use with WSGIApplication. 150 | 151 | This method is called automatically with the correct registry path when 152 | services are configured via service_handlers.service_mapping. 153 | 154 | Args: 155 | registry_path: Absolute path on server where the ProtoRPC RegsitryService 156 | is located. 157 | 158 | Returns: 159 | Factory function that creates a properly configured FormsHandler instance. 160 | """ 161 | def forms_factory(): 162 | return cls(registry_path) 163 | return forms_factory 164 | -------------------------------------------------------------------------------- /protorpc/webapp/forms_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """Tests for protorpc.forms.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | 23 | import os 24 | import unittest 25 | 26 | from protorpc import test_util 27 | from protorpc import webapp_test_util 28 | from protorpc.webapp import forms 29 | from protorpc.webapp.google_imports import template 30 | 31 | 32 | class ModuleInterfaceTest(test_util.ModuleInterfaceTest, 33 | test_util.TestCase): 34 | 35 | MODULE = forms 36 | 37 | 38 | def RenderTemplate(name, **params): 39 | """Load content from static file. 40 | 41 | Args: 42 | name: Name of static file to load from static directory. 43 | params: Passed in to webapp template generator. 44 | 45 | Returns: 46 | Contents of static file. 47 | """ 48 | path = os.path.join(forms._TEMPLATES_DIR, name) 49 | return template.render(path, params) 50 | 51 | 52 | class ResourceHandlerTest(webapp_test_util.RequestHandlerTestBase): 53 | 54 | def CreateRequestHandler(self): 55 | return forms.ResourceHandler() 56 | 57 | def DoStaticContentTest(self, name, expected_type): 58 | """Run the static content test. 59 | 60 | Loads expected static content from source and compares with 61 | results in response. Checks content-type and cache header. 62 | 63 | Args: 64 | name: Name of file that should be served. 65 | expected_type: Expected content-type of served file. 66 | """ 67 | self.handler.get(name) 68 | 69 | content = RenderTemplate(name) 70 | self.CheckResponse('200 OK', 71 | {'content-type': expected_type, 72 | }, 73 | content) 74 | 75 | def testGet(self): 76 | self.DoStaticContentTest('forms.js', 'text/javascript') 77 | 78 | def testNoSuchFile(self): 79 | self.handler.get('unknown.txt') 80 | 81 | self.CheckResponse('404 Not Found', 82 | {}, 83 | 'Resource not found.') 84 | 85 | 86 | class FormsHandlerTest(webapp_test_util.RequestHandlerTestBase): 87 | 88 | def CreateRequestHandler(self): 89 | handler = forms.FormsHandler('/myreg') 90 | self.assertEquals('/myreg', handler.registry_path) 91 | return handler 92 | 93 | def testGetForm(self): 94 | self.handler.get() 95 | 96 | content = RenderTemplate( 97 | 'forms.html', 98 | forms_path='/tmp/myhandler', 99 | hostname=self.request.host, 100 | registry_path='/myreg') 101 | 102 | self.CheckResponse('200 OK', 103 | {}, 104 | content) 105 | 106 | def testGet_MissingPath(self): 107 | self.ResetHandler({'QUERY_STRING': 'method=my_method'}) 108 | 109 | self.handler.get() 110 | 111 | content = RenderTemplate( 112 | 'forms.html', 113 | forms_path='/tmp/myhandler', 114 | hostname=self.request.host, 115 | registry_path='/myreg') 116 | 117 | self.CheckResponse('200 OK', 118 | {}, 119 | content) 120 | 121 | def testGet_MissingMethod(self): 122 | self.ResetHandler({'QUERY_STRING': 'path=/my-path'}) 123 | 124 | self.handler.get() 125 | 126 | content = RenderTemplate( 127 | 'forms.html', 128 | forms_path='/tmp/myhandler', 129 | hostname=self.request.host, 130 | registry_path='/myreg') 131 | 132 | self.CheckResponse('200 OK', 133 | {}, 134 | content) 135 | 136 | def testGetMethod(self): 137 | self.ResetHandler({'QUERY_STRING': 'path=/my-path&method=my_method'}) 138 | 139 | self.handler.get() 140 | 141 | content = RenderTemplate( 142 | 'methods.html', 143 | forms_path='/tmp/myhandler', 144 | hostname=self.request.host, 145 | registry_path='/myreg', 146 | service_path='/my-path', 147 | method_name='my_method') 148 | 149 | self.CheckResponse('200 OK', 150 | {}, 151 | content) 152 | 153 | 154 | def main(): 155 | unittest.main() 156 | 157 | 158 | if __name__ == '__main__': 159 | main() 160 | -------------------------------------------------------------------------------- /protorpc/webapp/google_imports.py: -------------------------------------------------------------------------------- 1 | """Dynamically decide from where to import other SDK modules. 2 | 3 | All other protorpc.webapp code should import other SDK modules from 4 | this module. If necessary, add new imports here (in both places). 5 | """ 6 | 7 | __author__ = 'yey@google.com (Ye Yuan)' 8 | 9 | # pylint: disable=g-import-not-at-top 10 | # pylint: disable=unused-import 11 | 12 | import os 13 | import sys 14 | 15 | try: 16 | from google.appengine import ext 17 | normal_environment = True 18 | except ImportError: 19 | normal_environment = False 20 | 21 | 22 | if normal_environment: 23 | from google.appengine.ext import webapp 24 | from google.appengine.ext.webapp import util as webapp_util 25 | from google.appengine.ext.webapp import template 26 | -------------------------------------------------------------------------------- /protorpc/wsgi/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | -------------------------------------------------------------------------------- /protorpc/wsgi/service_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """WSGI application tests.""" 19 | 20 | __author__ = 'rafek@google.com (Rafe Kaplan)' 21 | 22 | 23 | import unittest 24 | 25 | 26 | from protorpc import end2end_test 27 | from protorpc import protojson 28 | from protorpc import remote 29 | from protorpc import registry 30 | from protorpc import transport 31 | from protorpc import test_util 32 | from protorpc import webapp_test_util 33 | from protorpc.wsgi import service 34 | from protorpc.wsgi import util 35 | 36 | 37 | class ServiceMappingTest(end2end_test.EndToEndTest): 38 | 39 | def setUp(self): 40 | self.protocols = None 41 | remote.Protocols.set_default(remote.Protocols.new_default()) 42 | super(ServiceMappingTest, self).setUp() 43 | 44 | def CreateServices(self): 45 | 46 | return my_service, my_other_service 47 | 48 | def CreateWsgiApplication(self): 49 | """Create WSGI application used on the server side for testing.""" 50 | my_service = service.service_mapping(webapp_test_util.TestService, 51 | '/my/service') 52 | my_other_service = service.service_mapping( 53 | webapp_test_util.TestService.new_factory('initialized'), 54 | '/my/other_service', 55 | protocols=self.protocols) 56 | 57 | return util.first_found([my_service, my_other_service]) 58 | 59 | def testAlternateProtocols(self): 60 | self.protocols = remote.Protocols() 61 | self.protocols.add_protocol(protojson, 'altproto', 'image/png') 62 | 63 | global_protocols = remote.Protocols() 64 | global_protocols.add_protocol(protojson, 'server-side-name', 'image/png') 65 | remote.Protocols.set_default(global_protocols) 66 | self.ResetServer() 67 | 68 | self.connection = transport.HttpTransport( 69 | self.service_url, protocol=self.protocols.lookup_by_name('altproto')) 70 | self.stub = webapp_test_util.TestService.Stub(self.connection) 71 | 72 | self.stub.optional_message(string_value='alternate-protocol') 73 | 74 | def testAlwaysUseDefaults(self): 75 | new_protocols = remote.Protocols() 76 | new_protocols.add_protocol(protojson, 'altproto', 'image/png') 77 | 78 | self.connection = transport.HttpTransport( 79 | self.service_url, protocol=new_protocols.lookup_by_name('altproto')) 80 | self.stub = webapp_test_util.TestService.Stub(self.connection) 81 | 82 | self.assertRaisesWithRegexpMatch( 83 | remote.ServerError, 84 | 'HTTP Error 415: Unsupported Media Type', 85 | self.stub.optional_message, string_value='alternate-protocol') 86 | 87 | remote.Protocols.set_default(new_protocols) 88 | 89 | self.stub.optional_message(string_value='alternate-protocol') 90 | 91 | 92 | class ProtoServiceMappingsTest(ServiceMappingTest): 93 | 94 | def CreateWsgiApplication(self): 95 | """Create WSGI application used on the server side for testing.""" 96 | return service.service_mappings( 97 | [('/my/service', webapp_test_util.TestService), 98 | ('/my/other_service', 99 | webapp_test_util.TestService.new_factory('initialized')) 100 | ]) 101 | 102 | def GetRegistryStub(self, path='/protorpc'): 103 | service_url = self.make_service_url(path) 104 | transport = self.CreateTransport(service_url) 105 | return registry.RegistryService.Stub(transport) 106 | 107 | def testRegistry(self): 108 | registry_client = self.GetRegistryStub() 109 | response = registry_client.services() 110 | self.assertIterEqual([ 111 | registry.ServiceMapping( 112 | name='/my/other_service', 113 | definition='protorpc.webapp_test_util.TestService'), 114 | registry.ServiceMapping( 115 | name='/my/service', 116 | definition='protorpc.webapp_test_util.TestService'), 117 | ], response.services) 118 | 119 | def testRegistryDictionary(self): 120 | self.ResetServer(service.service_mappings( 121 | {'/my/service': webapp_test_util.TestService, 122 | '/my/other_service': 123 | webapp_test_util.TestService.new_factory('initialized'), 124 | })) 125 | registry_client = self.GetRegistryStub() 126 | response = registry_client.services() 127 | self.assertIterEqual([ 128 | registry.ServiceMapping( 129 | name='/my/other_service', 130 | definition='protorpc.webapp_test_util.TestService'), 131 | registry.ServiceMapping( 132 | name='/my/service', 133 | definition='protorpc.webapp_test_util.TestService'), 134 | ], response.services) 135 | 136 | def testNoRegistry(self): 137 | self.ResetServer(service.service_mappings( 138 | [('/my/service', webapp_test_util.TestService), 139 | ('/my/other_service', 140 | webapp_test_util.TestService.new_factory('initialized')) 141 | ], 142 | registry_path=None)) 143 | registry_client = self.GetRegistryStub() 144 | self.assertRaisesWithRegexpMatch( 145 | remote.ServerError, 146 | 'HTTP Error 404: Not Found', 147 | registry_client.services) 148 | 149 | def testAltRegistry(self): 150 | self.ResetServer(service.service_mappings( 151 | [('/my/service', webapp_test_util.TestService), 152 | ('/my/other_service', 153 | webapp_test_util.TestService.new_factory('initialized')) 154 | ], 155 | registry_path='/registry')) 156 | registry_client = self.GetRegistryStub('/registry') 157 | services = registry_client.services() 158 | self.assertTrue(isinstance(services, registry.ServicesResponse)) 159 | self.assertIterEqual( 160 | [registry.ServiceMapping( 161 | name='/my/other_service', 162 | definition='protorpc.webapp_test_util.TestService'), 163 | registry.ServiceMapping( 164 | name='/my/service', 165 | definition='protorpc.webapp_test_util.TestService'), 166 | ], 167 | services.services) 168 | 169 | def testDuplicateRegistryEntry(self): 170 | self.assertRaisesWithRegexpMatch( 171 | remote.ServiceConfigurationError, 172 | "Path '/my/service' is already defined in service mapping", 173 | service.service_mappings, 174 | [('/my/service', webapp_test_util.TestService), 175 | ('/my/service', 176 | webapp_test_util.TestService.new_factory('initialized')) 177 | ]) 178 | 179 | def testRegex(self): 180 | self.ResetServer(service.service_mappings( 181 | [('/my/[0-9]+', webapp_test_util.TestService.new_factory('service')), 182 | ('/my/[a-z]+', 183 | webapp_test_util.TestService.new_factory('other-service')), 184 | ])) 185 | my_service_url = 'http://localhost:%d/my/12345' % self.port 186 | my_other_service_url = 'http://localhost:%d/my/blarblar' % self.port 187 | 188 | my_service = webapp_test_util.TestService.Stub( 189 | transport.HttpTransport(my_service_url)) 190 | my_other_service = webapp_test_util.TestService.Stub( 191 | transport.HttpTransport(my_other_service_url)) 192 | 193 | response = my_service.init_parameter() 194 | self.assertEquals('service', response.string_value) 195 | 196 | response = my_other_service.init_parameter() 197 | self.assertEquals('other-service', response.string_value) 198 | 199 | 200 | def main(): 201 | unittest.main() 202 | 203 | 204 | if __name__ == '__main__': 205 | main() 206 | -------------------------------------------------------------------------------- /protorpc/wsgi/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | """WSGI utilities 19 | 20 | Small collection of helpful utilities for working with WSGI. 21 | """ 22 | import six 23 | 24 | __author__ = 'rafek@google.com (Rafe Kaplan)' 25 | 26 | import six.moves.http_client 27 | import re 28 | 29 | from .. import util 30 | 31 | __all__ = ['static_page', 32 | 'error', 33 | 'first_found', 34 | ] 35 | 36 | _STATUS_PATTERN = re.compile('^(\d{3})\s') 37 | 38 | 39 | @util.positional(1) 40 | def static_page(content='', 41 | status='200 OK', 42 | content_type='text/html; charset=utf-8', 43 | headers=None): 44 | """Create a WSGI application that serves static content. 45 | 46 | A static page is one that will be the same every time it receives a request. 47 | It will always serve the same status, content and headers. 48 | 49 | Args: 50 | content: Content to serve in response to HTTP request. 51 | status: Status to serve in response to HTTP request. If string, status 52 | is served as is without any error checking. If integer, will look up 53 | status message. Otherwise, parameter is tuple (status, description): 54 | status: Integer status of response. 55 | description: Brief text description of response. 56 | content_type: Convenient parameter for content-type header. Will appear 57 | before any content-type header that appears in 'headers' parameter. 58 | headers: Dictionary of headers or iterable of tuples (name, value): 59 | name: String name of header. 60 | value: String value of header. 61 | 62 | Returns: 63 | WSGI application that serves static content. 64 | """ 65 | if isinstance(status, six.integer_types): 66 | status = '%d %s' % (status, six.moves.http_client.responses.get(status, 'Unknown Error')) 67 | elif not isinstance(status, six.string_types): 68 | status = '%d %s' % tuple(status) 69 | 70 | if isinstance(headers, dict): 71 | headers = six.iteritems(headers) 72 | 73 | headers = [('content-length', str(len(content))), 74 | ('content-type', content_type), 75 | ] + list(headers or []) 76 | 77 | # Ensure all headers are str. 78 | for index, (key, value) in enumerate(headers): 79 | if isinstance(value, six.text_type): 80 | value = value.encode('utf-8') 81 | headers[index] = key, value 82 | 83 | if not isinstance(key, str): 84 | raise TypeError('Header key must be str, found: %r' % (key,)) 85 | 86 | if not isinstance(value, str): 87 | raise TypeError( 88 | 'Header %r must be type str or unicode, found: %r' % (key, value)) 89 | 90 | def static_page_application(environ, start_response): 91 | start_response(status, headers) 92 | return [content] 93 | 94 | return static_page_application 95 | 96 | 97 | @util.positional(2) 98 | def error(status_code, status_message=None, 99 | content_type='text/plain; charset=utf-8', 100 | headers=None, content=None): 101 | """Create WSGI application that statically serves an error page. 102 | 103 | Creates a static error page specifically for non-200 HTTP responses. 104 | 105 | Browsers such as Internet Explorer will display their own error pages for 106 | error content responses smaller than 512 bytes. For this reason all responses 107 | are right-padded up to 512 bytes. 108 | 109 | Error pages that are not provided will content will contain the standard HTTP 110 | status message as their content. 111 | 112 | Args: 113 | status_code: Integer status code of error. 114 | status_message: Status message. 115 | 116 | Returns: 117 | Static WSGI application that sends static error response. 118 | """ 119 | if status_message is None: 120 | status_message = six.moves.http_client.responses.get(status_code, 'Unknown Error') 121 | 122 | if content is None: 123 | content = status_message 124 | 125 | content = util.pad_string(content) 126 | 127 | return static_page(content, 128 | status=(status_code, status_message), 129 | content_type=content_type, 130 | headers=headers) 131 | 132 | 133 | def first_found(apps): 134 | """Serve the first application that does not response with 404 Not Found. 135 | 136 | If no application serves content, will respond with generic 404 Not Found. 137 | 138 | Args: 139 | apps: List of WSGI applications to search through. Will serve the content 140 | of the first of these that does not return a 404 Not Found. Applications 141 | in this list must not modify the environment or any objects in it if they 142 | do not match. Applications that do not obey this restriction can create 143 | unpredictable results. 144 | 145 | Returns: 146 | Compound application that serves the contents of the first application that 147 | does not response with 404 Not Found. 148 | """ 149 | apps = tuple(apps) 150 | not_found = error(six.moves.http_client.NOT_FOUND) 151 | 152 | def first_found_app(environ, start_response): 153 | """Compound application returned from the first_found function.""" 154 | final_result = {} # Used in absence of Python local scoping. 155 | 156 | def first_found_start_response(status, response_headers): 157 | """Replacement for start_response as passed in to first_found_app. 158 | 159 | Called by each application in apps instead of the real start response. 160 | Checks the response status, and if anything other than 404, sets 'status' 161 | and 'response_headers' in final_result. 162 | """ 163 | status_match = _STATUS_PATTERN.match(status) 164 | assert status_match, ('Status must be a string beginning ' 165 | 'with 3 digit number. Found: %s' % status) 166 | status_code = status_match.group(0) 167 | if int(status_code) == six.moves.http_client.NOT_FOUND: 168 | return 169 | 170 | final_result['status'] = status 171 | final_result['response_headers'] = response_headers 172 | 173 | for app in apps: 174 | response = app(environ, first_found_start_response) 175 | if final_result: 176 | start_response(final_result['status'], final_result['response_headers']) 177 | return response 178 | 179 | return not_found(environ, start_response) 180 | return first_found_app 181 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2013 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Setup configuration.""" 18 | 19 | import platform 20 | 21 | from setuptools import setup 22 | 23 | # Configure the required packages and scripts to install, depending on 24 | # Python version and OS. 25 | REQUIRED_PACKAGES = [ 26 | 'six>=1.7.0', # minimum version to avoid six.NewBase issues 27 | ] 28 | CONSOLE_SCRIPTS = [ 29 | 'gen_protorpc = gen_protorpc:main', 30 | ] 31 | 32 | py_version = platform.python_version() 33 | if py_version < '2.6': 34 | REQUIRED_PACKAGES.append('simplejson') 35 | 36 | _PROTORPC_VERSION = '0.12.0' 37 | packages = [ 38 | 'protorpc', 39 | ] 40 | 41 | setup( 42 | name='protorpc', 43 | version=_PROTORPC_VERSION, 44 | description='Google Protocol RPC', 45 | url='http://code.google.com/p/google-protorpc/', 46 | author='Google Inc.', 47 | author_email='rafek@google.com', 48 | # Contained modules and scripts. 49 | packages=packages, 50 | entry_points={ 51 | 'console_scripts': CONSOLE_SCRIPTS, 52 | }, 53 | install_requires=REQUIRED_PACKAGES, 54 | # PyPI package information. 55 | classifiers=[ 56 | 'Intended Audience :: Developers', 57 | 'License :: OSI Approved :: Apache Software License', 58 | 'Operating System :: MacOS :: MacOS X', 59 | 'Operating System :: Microsoft :: Windows', 60 | 'Operating System :: POSIX :: Linux', 61 | 'Programming Language :: Python :: 2', 62 | 'Programming Language :: Python :: 2.6', 63 | 'Programming Language :: Python :: 2.7', 64 | 'Programming Language :: Python :: 3', 65 | 'Programming Language :: Python :: 3.3', 66 | 'Programming Language :: Python :: 3.4', 67 | 'Topic :: Software Development :: Libraries', 68 | 'Topic :: Software Development :: Libraries :: Python Modules', 69 | ], 70 | license='Apache 2.0', 71 | keywords='google protocol rpc', 72 | ) 73 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py36 3 | 4 | [testenv] 5 | deps = nose 6 | mox 7 | simplejson 8 | six 9 | unittest2 10 | commands = nosetests protorpc/message_types_test.py protorpc/messages_test.py protorpc/protojson_test.py 11 | --------------------------------------------------------------------------------