Index: third_party/gsutil/third_party/protorpc/demos/tunes_db/server/tunes_db.py |
diff --git a/third_party/gsutil/third_party/protorpc/demos/tunes_db/server/tunes_db.py b/third_party/gsutil/third_party/protorpc/demos/tunes_db/server/tunes_db.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..0d62cb7ce9a7bdbc69bc629b946168854b6713de |
--- /dev/null |
+++ b/third_party/gsutil/third_party/protorpc/demos/tunes_db/server/tunes_db.py |
@@ -0,0 +1,539 @@ |
+#!/usr/bin/env python |
+# |
+# Copyright 2010 Google Inc. |
+# |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
+# you may not use this file except in compliance with the License. |
+# You may obtain a copy of the License at |
+# |
+# http://www.apache.org/licenses/LICENSE-2.0 |
+# |
+# Unless required by applicable law or agreed to in writing, software |
+# distributed under the License is distributed on an "AS IS" BASIS, |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+# See the License for the specific language governing permissions and |
+# limitations under the License. |
+# |
+ |
+"""Tunes DB service implementation. |
+ |
+This module contains all the protocol buffer and service definitions |
+necessary for the Tunes DB service. |
+""" |
+ |
+import base64 |
+import sys |
+ |
+from google.appengine.ext import db |
+ |
+from protorpc import descriptor |
+from protorpc import message_types |
+from protorpc import messages |
+from protorpc import protobuf |
+from protorpc import remote |
+ |
+import model |
+ |
+ |
+class Artist(messages.Message): |
+ """Musician or music group responsible for music production. |
+ |
+ Fields: |
+ artist_id: Unique opaque identifier for artist. |
+ name: User friendly name of artist. |
+ album_count: Number of albums produced by artist. |
+ """ |
+ |
+ artist_id = messages.StringField(1, required=True) |
+ name = messages.StringField(2, required=True) |
+ |
+ album_count = messages.IntegerField(3) |
+ |
+ |
+class Album(messages.Message): |
+ """Album produced by a musician or music group. |
+ |
+ Fields: |
+ album_id: Unique opaque identifier for artist. |
+ artist_id: Artist id of musician or music group that produced album. |
+ name: Name of album. |
+ released: Year when album was released. |
+ """ |
+ |
+ album_id = messages.StringField(1, required=True) |
+ artist_id = messages.StringField(2, required=True) |
+ name = messages.StringField(3, required=True) |
+ released = messages.IntegerField(4) |
+ |
+ |
+class AddArtistRequest(messages.Message): |
+ """Request to add a new Artist to library. |
+ |
+ Fields: |
+ name: User friendly name of artist. |
+ """ |
+ |
+ name = messages.StringField(1, required=True) |
+ |
+ |
+class AddArtistResponse(messages.Message): |
+ """Response sent after creation of new artist in library. |
+ |
+ Fields: |
+ artist_id: Unique opaque ID of new artist. |
+ """ |
+ |
+ artist_id = messages.StringField(1, required=True) |
+ |
+ |
+class UpdateArtistRequest(messages.Message): |
+ """Update an existing artist. |
+ |
+ Fields: |
+ artist: Complete information about artist to update. |
+ """ |
+ |
+ artist = messages.MessageField(Artist, 1, required=True) |
+ |
+ |
+class UpdateArtistResponse(messages.Message): |
+ """Artist update response. |
+ |
+ Fields: |
+ artist_updated: Artist was found and updated. |
+ """ |
+ |
+ artist_updated = messages.BooleanField(1, required=True) |
+ |
+ |
+class DeleteArtistRequest(messages.Message): |
+ """Delete artist from library. |
+ |
+ Fields: |
+ artist_id: Unique opaque ID of artist to delete. |
+ """ |
+ |
+ artist_id = messages.StringField(1, required=True) |
+ |
+ |
+class DeleteArtistResponse(messages.Message): |
+ """Artist deletion response. |
+ |
+ Fields: |
+ artist_deleted: Artist was found and deleted. |
+ """ |
+ |
+ artist_deleted = messages.BooleanField(1, default=True) |
+ |
+ |
+class FetchArtistRequest(messages.Message): |
+ """Fetch an artist from the library. |
+ |
+ Fields: |
+ artist_id: Unique opaque ID of artist to fetch. |
+ """ |
+ |
+ artist_id = messages.StringField(1, required=True) |
+ |
+ |
+class FetchArtistResponse(messages.Message): |
+ """Fetched artist from library. |
+ |
+ Fields: |
+ artist: Artist found in library. |
+ """ |
+ |
+ artist = messages.MessageField(Artist, 1) |
+ |
+ |
+class SearchArtistsRequest(messages.Message): |
+ """Artist search request. |
+ |
+ Fields: |
+ continuation: Continuation from the response of a previous call to |
+ search_artists remote method. |
+ fetch_size: Maximum number of records to retrieve. |
+ name_prefix: Name prefix of artists to search. If none provided and |
+ no continuation provided, search will be of all artists in library. |
+ If continuation is provided, name_prefix should be empty, if not, value |
+ is ignored. |
+ """ |
+ |
+ continuation = messages.StringField(1) |
+ fetch_size = messages.IntegerField(2, default=10) |
+ name_prefix = messages.StringField(3, default=u'') |
+ |
+ |
+class SearchArtistsResponse(messages.Message): |
+ """Response from searching artists. |
+ |
+ Fields: |
+ artists: Artists found from search up to fetch_size. |
+ continuation: Opaque string that can be used with a new search request |
+ that will continue finding new artists where this response left off. |
+ Will not be set if there were no results from the search or fewer |
+ artists were returned in the response than requested, indicating the end |
+ of the query. |
+ """ |
+ |
+ artists = messages.MessageField(Artist, 1, repeated=True) |
+ continuation = messages.StringField(2) |
+ |
+ |
+class AddAlbumRequest(messages.Message): |
+ """Request to add a new album to library. |
+ |
+ Fields: |
+ name: User friendly name of album. |
+ artist_id: Artist id of artist that produced record. |
+ released: Year album was released. |
+ """ |
+ |
+ name = messages.StringField(1, required=True) |
+ artist_id = messages.StringField(2, required=True) |
+ released = messages.IntegerField(3) |
+ |
+ |
+class AddAlbumResponse(messages.Message): |
+ """Response sent after creation of new album in library. |
+ |
+ Fields: |
+ album_id: Unique opaque ID of new album. |
+ """ |
+ |
+ album_id = messages.StringField(1, required=True) |
+ |
+ |
+class UpdateAlbumRequest(messages.Message): |
+ """Update an existing album. |
+ |
+ Fields: |
+ album: Complete information about album to update. |
+ """ |
+ |
+ album = messages.MessageField(Album, 1, required=True) |
+ |
+ |
+class UpdateAlbumResponse(messages.Message): |
+ """Album update response. |
+ |
+ Fields: |
+ album_updated: Album was found and updated. |
+ """ |
+ |
+ album_updated = messages.BooleanField(1, required=True) |
+ |
+ |
+class DeleteAlbumRequest(messages.Message): |
+ """Delete album from library. |
+ |
+ Fields: |
+ album_id: Unique opaque ID of album to delete. |
+ """ |
+ |
+ album_id = messages.StringField(1, required=True) |
+ |
+ |
+class DeleteAlbumResponse(messages.Message): |
+ """Album deletion response. |
+ |
+ Fields: |
+ album_deleted: Album was found and deleted. |
+ """ |
+ |
+ album_deleted = messages.BooleanField(1, default=True) |
+ |
+ |
+class FetchAlbumRequest(messages.Message): |
+ """Fetch an album from the library. |
+ |
+ Fields: |
+ album_id: Unique opaque ID of album to fetch. |
+ """ |
+ |
+ album_id = messages.StringField(1, required=True) |
+ |
+ |
+class FetchAlbumResponse(messages.Message): |
+ """Fetched album from library. |
+ |
+ Fields: |
+ album: Album found in library. |
+ """ |
+ |
+ album = messages.MessageField(Album, 1) |
+ |
+ |
+class SearchAlbumsRequest(messages.Message): |
+ """Album search request. |
+ |
+ Fields: |
+ continuation: Continuation from the response of a previous call to |
+ search_albums remote method. |
+ fetch_size: Maximum number of records to retrieve. |
+ name_prefix: Name prefix of albms to search. If none provided and |
+ no continuation provided, search will be of all albums in library. |
+ If continuation is provided, name_prefix should be empty, if not, value |
+ is ignored. |
+ artist_id: Restrict search to albums of single artist. |
+ """ |
+ |
+ continuation = messages.StringField(1) |
+ fetch_size = messages.IntegerField(2, default=10) |
+ name_prefix = messages.StringField(3, default=u'') |
+ artist_id = messages.StringField(4) |
+ |
+ |
+class SearchAlbumsResponse(messages.Message): |
+ """Response from searching artists. |
+ |
+ Fields: |
+ albums: Albums found from search up to fetch_size. |
+ continuation: Opaque string that can be used with a new search request |
+ that will continue finding new albums where this response left off. |
+ Will not be set if there were no results from the search or fewer |
+ albums were returned in the response than requested, indicating the end |
+ of the query. |
+ """ |
+ |
+ albums = messages.MessageField(Album, 1, repeated=True) |
+ continuation = messages.StringField(2) |
+ |
+ |
+class MusicLibraryService(remote.Service): |
+ """Music library service.""" |
+ |
+ __file_set = None |
+ |
+ def __artist_from_model(self, artist_model): |
+ """Helper that copies an Artist model to an Artist message. |
+ |
+ Args: |
+ artist_model: model.ArtistInfo instance to convert in to an Artist |
+ message. |
+ |
+ Returns: |
+ New Artist message with contents of artist_model copied in to it. |
+ """ |
+ return Artist(artist_id=unicode(artist_model.key()), |
+ name=artist_model.name, |
+ album_count=artist_model.album_count) |
+ |
+ def __album_from_model(self, album_model): |
+ """Helper that copies an Album model to an Album message. |
+ |
+ Args: |
+ album_model: model.AlbumInfo instance to convert in to an Album |
+ message. |
+ |
+ Returns: |
+ New Album message with contents of album_model copied in to it. |
+ """ |
+ artist_id = model.AlbumInfo.artist.get_value_for_datastore(album_model) |
+ |
+ return Album(album_id=unicode(album_model.key()), |
+ artist_id=unicode(artist_id), |
+ name=album_model.name, |
+ released=album_model.released or None) |
+ |
+ @classmethod |
+ def __search_info(cls, |
+ request, |
+ info_class, |
+ model_to_message, |
+ customize_query=None): |
+ """Search over an Info subclass. |
+ |
+ Since all search request classes are very similar, it's possible to |
+ generalize how to do searches over them. |
+ |
+ Args: |
+ request: Search request received from client. |
+ info_class: The model.Info subclass to search. |
+ model_to_method: Function (model) -> message that transforms an instance |
+ of info_class in to the appropriate messages.Message subclass. |
+ customize_query: Function (request, query) -> None that adds additional |
+ filters to Datastore query based on specifics of that search message. |
+ |
+ Returns: |
+ Tuple (results, continuation): |
+ results: A list of messages satisfying the parameters of the request. |
+ None if there are no results. |
+ continuation: Continuation string for response if there are more |
+ results available. None if there are no more results available. |
+ """ |
+ # TODO(rafek): fetch_size from this request should take priority |
+ # over what is stored in continuation. |
+ if request.continuation: |
+ encoded_search, continuation = request.continuation.split(':', 1) |
+ decoded_search = base64.urlsafe_b64decode(encoded_search.encode('utf-8')) |
+ request = protobuf.decode_message(type(request), decoded_search) |
+ else: |
+ continuation = None |
+ encoded_search = unicode(base64.urlsafe_b64encode( |
+ protobuf.encode_message(request))) |
+ |
+ name_prefix = request.name_prefix |
+ |
+ query = info_class.search(name_prefix) |
+ query.order('name') |
+ if customize_query: |
+ customize_query(request, query) |
+ |
+ if continuation: |
+ # TODO(rafek): Pure query cursors are not safe for model with |
+ # query restrictions. Would technically need to be encrypted. |
+ query.with_cursor(continuation) |
+ |
+ fetch_size = request.fetch_size |
+ |
+ model_instance = query.fetch(fetch_size) |
+ results = None |
+ continuation = None |
+ if model_instance: |
+ results = [model_to_message(i) for i in model_instance] |
+ if len(model_instance) == fetch_size: |
+ cursor = query.cursor() |
+ continuation = u'%s:%s' % (encoded_search, query.cursor()) |
+ |
+ return results, continuation |
+ |
+ |
+ @remote.method(AddArtistRequest, AddArtistResponse) |
+ def add_artist(self, request): |
+ """Add artist to library.""" |
+ artist_name = request.name |
+ def do_add(): |
+ artist = model.ArtistInfo(name=artist_name) |
+ artist.put() |
+ return artist |
+ artist = db.run_in_transaction(do_add) |
+ |
+ return AddArtistResponse(artist_id = unicode(artist.key())) |
+ |
+ @remote.method(UpdateArtistRequest, UpdateArtistResponse) |
+ def update_artist(self, request): |
+ """Update artist from library.""" |
+ def do_deletion(): |
+ artist = model.ArtistInfo.get(request.artist.artist_id) |
+ if artist: |
+ artist.name = request.artist.name |
+ artist.put() |
+ return True |
+ else: |
+ return False |
+ return UpdateArtistResponse( |
+ artist_updated=db.run_in_transaction(do_deletion)) |
+ |
+ @remote.method(DeleteArtistRequest, DeleteArtistResponse) |
+ def delete_artist(self, request): |
+ """Delete artist from library.""" |
+ def do_deletion(): |
+ artist = model.ArtistInfo.get(request.artist_id) |
+ if artist: |
+ db.delete(model.Info.all(keys_only=True).ancestor(artist)) |
+ return True |
+ else: |
+ return False |
+ return DeleteArtistResponse( |
+ artist_deleted = db.run_in_transaction(do_deletion)) |
+ |
+ @remote.method(FetchArtistRequest, FetchArtistResponse) |
+ def fetch_artist(self, request): |
+ """Fetch artist from library.""" |
+ artist_model = model.ArtistInfo.get(request.artist_id) |
+ if isinstance(artist_model, model.ArtistInfo): |
+ artist = self.__artist_from_model(artist_model) |
+ else: |
+ artist = None |
+ return FetchArtistResponse(artist=artist) |
+ |
+ |
+ @remote.method(SearchArtistsRequest, SearchArtistsResponse) |
+ def search_artists(self, request): |
+ """Search library for artists.""" |
+ results, continuation = self.__search_info(request, |
+ model.ArtistInfo, |
+ self.__artist_from_model) |
+ return SearchArtistsResponse(artists=results or [], |
+ continuation=continuation or None) |
+ |
+ @remote.method(AddAlbumRequest, AddAlbumResponse) |
+ def add_album(self, request): |
+ """Add album to library.""" |
+ def create_album(): |
+ if not request.artist_id: |
+ raise ValueError('Request does not have artist-id.') |
+ artist = model.ArtistInfo.get(request.artist_id) |
+ if not artist: |
+ raise ValueError('No artist found for %s.' % request.artist_id) |
+ artist.album_count += 1 |
+ artist.put() |
+ |
+ album = model.AlbumInfo(name=request.name, |
+ released=request.released, |
+ artist=artist, |
+ parent=artist) |
+ album.put() |
+ |
+ return album |
+ album = db.run_in_transaction(create_album) |
+ |
+ return AddAlbumResponse(album_id=unicode(album.key())) |
+ |
+ @remote.method(UpdateAlbumRequest, UpdateAlbumResponse) |
+ def update_album(self, request): |
+ """Update album from library.""" |
+ def do_deletion(): |
+ album = model.AlbumInfo.get(request.album.album_id) |
+ if album: |
+ album.name = request.album.name |
+ album.released = request.album.released |
+ album.put() |
+ return True |
+ else: |
+ return False |
+ return UpdateAlbumResponse(album_updated=db.run_in_transaction(do_deletion)) |
+ |
+ @remote.method(DeleteAlbumRequest, DeleteAlbumResponse) |
+ def delete_album(self, request): |
+ """Delete album from library.""" |
+ def do_deletion(): |
+ album = model.AlbumInfo.get(request.album_id) |
+ |
+ artist = album.artist |
+ artist.album_count -= 1 |
+ artist.put() |
+ |
+ if album: |
+ db.delete(model.Info.all(keys_only=True).ancestor(album)) |
+ return True |
+ else: |
+ return False |
+ |
+ return DeleteAlbumResponse(album_deleted=db.run_in_transaction(do_deletion)) |
+ |
+ @remote.method(FetchAlbumRequest, FetchAlbumResponse) |
+ def fetch_album(self, request): |
+ """Fetch album from library.""" |
+ album_model = model.AlbumInfo.get(request.album_id) |
+ if isinstance(album_model, model.AlbumInfo): |
+ album = self.__album_from_model(album_model) |
+ else: |
+ album = None |
+ return FetchAlbumResponse(album=album) |
+ |
+ @remote.method(SearchAlbumsRequest, SearchAlbumsResponse) |
+ def search_albums(self, request): |
+ """Search library for albums.""" |
+ def customize_query(request, query): |
+ if request.artist_id: |
+ query.filter('artist', db.Key(request.artist_id)) |
+ |
+ response = SearchAlbumsResponse() |
+ results, continuation = self.__search_info(request, |
+ model.AlbumInfo, |
+ self.__album_from_model, |
+ customize_query) |
+ return SearchAlbumsResponse(albums=results or [], |
+ continuation=continuation or None) |