From e61c5426a2c8cea73d712fc8439c333a372f5170 Mon Sep 17 00:00:00 2001 From: Ross Karchner Date: Tue, 13 Sep 2011 23:37:59 -0400 Subject: [PATCH 1/7] removing unused modules, and began porting 'links' tipfy app to Django, and removing Tipfy --- app.yaml | 12 +- apps/links/urls.py | 11 - config.py | 34 - cron.disables | 3 - django_app.py | 47 - django_templates/base.html | 165 --- django_urls.py | 27 - handlers.py | 13 - {apps => links}/__init__.py | 0 {apps/links => links}/forms.py | 0 {apps/links => links}/handlers.py | 0 {apps/links => links}/models.py | 0 links/urls.py | 11 + apps/links/__init__.py => links/views.py | 0 main.py | 61 +- mapreduce.yaml | 8 - mapreduce/__init__.py | 16 - mapreduce/base_handler.py | 141 -- mapreduce/context.py | 305 ---- mapreduce/control.py | 85 -- mapreduce/handlers.py | 876 ------------ mapreduce/hooks.py | 93 -- mapreduce/input_readers.py | 1244 ----------------- mapreduce/lib/__init__.py | 15 - mapreduce/lib/blobstore/__init__.py | 23 - mapreduce/lib/blobstore/blobstore.py | 745 ---------- mapreduce/lib/graphy/README | 14 - mapreduce/lib/graphy/__init__.py | 2 - mapreduce/lib/graphy/backends/__init__.py | 1 - .../backends/google_chart_api/__init__.py | 50 - .../backends/google_chart_api/encoders.py | 430 ------ .../graphy/backends/google_chart_api/util.py | 231 --- mapreduce/lib/graphy/bar_chart.py | 171 --- mapreduce/lib/graphy/common.py | 412 ------ mapreduce/lib/graphy/formatters.py | 192 --- mapreduce/lib/graphy/line_chart.py | 122 -- mapreduce/lib/graphy/pie_chart.py | 178 --- mapreduce/lib/graphy/util.py | 14 - mapreduce/lib/key_range/__init__.py | 687 --------- mapreduce/lib/simplejson/README | 13 - mapreduce/lib/simplejson/__init__.py | 314 ----- mapreduce/lib/simplejson/decoder.py | 334 ----- mapreduce/lib/simplejson/encoder.py | 434 ------ mapreduce/lib/simplejson/scanner.py | 66 - mapreduce/main.py | 96 -- mapreduce/migrate.py | 8 - mapreduce/model.py | 768 ---------- mapreduce/operation/__init__.py | 29 - mapreduce/operation/counters.py | 43 - mapreduce/operation/db.py | 69 - mapreduce/quota.py | 185 --- mapreduce/static/base.css | 113 -- mapreduce/static/detail.html | 64 - mapreduce/static/jquery-1.4.2.min.js | 154 -- mapreduce/static/overview.html | 64 - mapreduce/static/status.js | 602 -------- mapreduce/status.py | 386 ----- mapreduce/util.py | 141 -- mapreduce_wrapper.py | 13 - {django_templates => templates}/404.html | 0 {django_templates => templates}/500.html | 0 .../account/account_tools.html | 0 .../account/edit.html | 0 .../account/edit_profile_form.html | 0 .../account/profile-setup.html | 0 .../account/signin.html | 0 .../admin-base.html | 0 .../admin/logo.html | 0 .../admin/mailchimp_apikey.html | 0 .../admin/users.html | 0 .../base-cacheable.html | 0 templates/base.html | 189 ++- templates/calendar/add.html | 13 - templates/calendar/index.html | 24 - .../email/manage_subscription.txt | 0 .../email/verify_subscription.txt | 0 .../events/add.html | 0 .../events/edit_form.html | 0 .../events/events.html | 0 .../events/one_event.html | 0 .../events/one_event_newsletter.html | 0 .../events/one_event_thisweek.html | 0 .../events/queue.html | 0 .../eventsite-safe/addevent.html | 0 .../eventsite-safe/admin.html | 0 .../eventsite/admin.html | 0 .../eventsite/front-page.html | 0 .../front_page_this_week_header.html | 0 .../eventsite/jump.html | 0 .../eventsite/newsletter.html | 0 .../eventsite/sidebar.html | 0 .../eventsite/tagpage.html | 0 .../eventsite/week.html | 0 .../eventsite/week.xml | 0 .../feeds/latest_description.html | 0 templates/form_macros.html | 79 -- {django_templates => templates}/forms.html | 0 templates/layout.html | 68 - templates/link/add.html | 17 - templates/link/review.html | 11 - .../newsletter-admin/upcoming.html | 0 .../sources/add.html | 0 .../sources/icalendar_manage_listing.html | 0 .../sources/index.html | 0 .../sources/index.opml | 0 .../sources/manage.html | 0 .../subscriptions/manage.html | 0 .../subscriptions/new.html | 0 .../subscriptions/recover.html | 0 .../subscriptions/thankyou.html | 0 .../subscriptions/verified_thankyou.html | 0 test.py | 3 - todo.txt | 10 - urls.py | 59 +- 114 files changed, 219 insertions(+), 10589 deletions(-) delete mode 100755 apps/links/urls.py delete mode 100755 config.py delete mode 100755 cron.disables delete mode 100755 django_app.py delete mode 100755 django_templates/base.html delete mode 100755 django_urls.py delete mode 100755 handlers.py rename {apps => links}/__init__.py (100%) rename {apps/links => links}/forms.py (100%) rename {apps/links => links}/handlers.py (100%) rename {apps/links => links}/models.py (100%) create mode 100755 links/urls.py rename apps/links/__init__.py => links/views.py (100%) mode change 100755 => 100644 delete mode 100755 mapreduce.yaml delete mode 100755 mapreduce/__init__.py delete mode 100755 mapreduce/base_handler.py delete mode 100755 mapreduce/context.py delete mode 100755 mapreduce/control.py delete mode 100755 mapreduce/handlers.py delete mode 100755 mapreduce/hooks.py delete mode 100755 mapreduce/input_readers.py delete mode 100755 mapreduce/lib/__init__.py delete mode 100755 mapreduce/lib/blobstore/__init__.py delete mode 100755 mapreduce/lib/blobstore/blobstore.py delete mode 100755 mapreduce/lib/graphy/README delete mode 100755 mapreduce/lib/graphy/__init__.py delete mode 100755 mapreduce/lib/graphy/backends/__init__.py delete mode 100755 mapreduce/lib/graphy/backends/google_chart_api/__init__.py delete mode 100755 mapreduce/lib/graphy/backends/google_chart_api/encoders.py delete mode 100755 mapreduce/lib/graphy/backends/google_chart_api/util.py delete mode 100755 mapreduce/lib/graphy/bar_chart.py delete mode 100755 mapreduce/lib/graphy/common.py delete mode 100755 mapreduce/lib/graphy/formatters.py delete mode 100755 mapreduce/lib/graphy/line_chart.py delete mode 100755 mapreduce/lib/graphy/pie_chart.py delete mode 100755 mapreduce/lib/graphy/util.py delete mode 100755 mapreduce/lib/key_range/__init__.py delete mode 100755 mapreduce/lib/simplejson/README delete mode 100755 mapreduce/lib/simplejson/__init__.py delete mode 100755 mapreduce/lib/simplejson/decoder.py delete mode 100755 mapreduce/lib/simplejson/encoder.py delete mode 100755 mapreduce/lib/simplejson/scanner.py delete mode 100755 mapreduce/main.py delete mode 100755 mapreduce/migrate.py delete mode 100755 mapreduce/model.py delete mode 100755 mapreduce/operation/__init__.py delete mode 100755 mapreduce/operation/counters.py delete mode 100755 mapreduce/operation/db.py delete mode 100755 mapreduce/quota.py delete mode 100755 mapreduce/static/base.css delete mode 100755 mapreduce/static/detail.html delete mode 100755 mapreduce/static/jquery-1.4.2.min.js delete mode 100755 mapreduce/static/overview.html delete mode 100755 mapreduce/static/status.js delete mode 100755 mapreduce/status.py delete mode 100755 mapreduce/util.py delete mode 100755 mapreduce_wrapper.py rename {django_templates => templates}/404.html (100%) rename {django_templates => templates}/500.html (100%) rename {django_templates => templates}/account/account_tools.html (100%) rename {django_templates => templates}/account/edit.html (100%) rename {django_templates => templates}/account/edit_profile_form.html (100%) rename {django_templates => templates}/account/profile-setup.html (100%) rename {django_templates => templates}/account/signin.html (100%) rename {django_templates => templates}/admin-base.html (100%) rename {django_templates => templates}/admin/logo.html (100%) rename {django_templates => templates}/admin/mailchimp_apikey.html (100%) rename {django_templates => templates}/admin/users.html (100%) rename {django_templates => templates}/base-cacheable.html (100%) delete mode 100755 templates/calendar/add.html delete mode 100755 templates/calendar/index.html rename {django_templates => templates}/email/manage_subscription.txt (100%) rename {django_templates => templates}/email/verify_subscription.txt (100%) rename {django_templates => templates}/events/add.html (100%) rename {django_templates => templates}/events/edit_form.html (100%) rename {django_templates => templates}/events/events.html (100%) rename {django_templates => templates}/events/one_event.html (100%) rename {django_templates => templates}/events/one_event_newsletter.html (100%) rename {django_templates => templates}/events/one_event_thisweek.html (100%) rename {django_templates => templates}/events/queue.html (100%) rename {django_templates => templates}/eventsite-safe/addevent.html (100%) rename {django_templates => templates}/eventsite-safe/admin.html (100%) rename {django_templates => templates}/eventsite/admin.html (100%) rename {django_templates => templates}/eventsite/front-page.html (100%) rename {django_templates => templates}/eventsite/front_page_this_week_header.html (100%) rename {django_templates => templates}/eventsite/jump.html (100%) rename {django_templates => templates}/eventsite/newsletter.html (100%) rename {django_templates => templates}/eventsite/sidebar.html (100%) rename {django_templates => templates}/eventsite/tagpage.html (100%) rename {django_templates => templates}/eventsite/week.html (100%) rename {django_templates => templates}/eventsite/week.xml (100%) rename {django_templates => templates}/feeds/latest_description.html (100%) delete mode 100755 templates/form_macros.html rename {django_templates => templates}/forms.html (100%) delete mode 100755 templates/layout.html delete mode 100755 templates/link/add.html delete mode 100755 templates/link/review.html rename {django_templates => templates}/newsletter-admin/upcoming.html (100%) rename {django_templates => templates}/sources/add.html (100%) rename {django_templates => templates}/sources/icalendar_manage_listing.html (100%) rename {django_templates => templates}/sources/index.html (100%) rename {django_templates => templates}/sources/index.opml (100%) rename {django_templates => templates}/sources/manage.html (100%) rename {django_templates => templates}/subscriptions/manage.html (100%) rename {django_templates => templates}/subscriptions/new.html (100%) rename {django_templates => templates}/subscriptions/recover.html (100%) rename {django_templates => templates}/subscriptions/thankyou.html (100%) rename {django_templates => templates}/subscriptions/verified_thankyou.html (100%) delete mode 100755 test.py delete mode 100755 todo.txt diff --git a/app.yaml b/app.yaml index 51f0925..3608b89 100755 --- a/app.yaml +++ b/app.yaml @@ -11,9 +11,6 @@ handlers: upload: static/(.*) expiration: "24d" -- url: /calendars.* - script: main.py - - url: /tasks.* script: main.py login: admin @@ -22,15 +19,8 @@ handlers: script: main.py login: admin -- url: /links.* - script: main.py - -- url: /mapreduce(/.*)? - script: mapreduce_wrapper.py - login: admin - - url: .* - script: django_app.py + script: main.py builtins: - datastore_admin: on \ No newline at end of file diff --git a/apps/links/urls.py b/apps/links/urls.py deleted file mode 100755 index 5df5d26..0000000 --- a/apps/links/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from tipfy import Rule - -def get_rules(app): - rules = [ - Rule('/links/add', endpoint="links/add", handler='apps.links.handlers.AddLinkHandler'), - Rule('/links/review', endpoint="links/review", handler='apps.links.handlers.ReviewLinksHandler'), - Rule('/links/change', endpoint="links/change-status", handler='apps.links.handlers.ChangeLinkStatusHandler'), - - ] - - return rules \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100755 index c151181..0000000 --- a/config.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -""" - config - ~~~~~~ - - Configuration settings. - - :copyright: 2009 by tipfy.org. - :license: BSD, see LICENSE for more details. -""" -config = {} - -# Configurations for the 'tipfy' module. - - - - -config['tipfy'] = { - # Enable debugger. It will be loaded only in development. - 'middleware': [ - 'tipfy.ext.debugger.DebuggerMiddleware', - ], - 'apps_installed': [ - 'apps.calendar', - 'apps.links', - ], -} - - - -config['tipfy.ext.session'] = { - 'secret_key': 'Ross M Karchner: OK Guy', -} - diff --git a/cron.disables b/cron.disables deleted file mode 100755 index b5f504a..0000000 --- a/cron.disables +++ /dev/null @@ -1,3 +0,0 @@ -- description: schedule upcoming newsletters - url: /subscriptions/start_schedule_newsletters/ - schedule: every 12 hours synchronized diff --git a/django_app.py b/django_app.py deleted file mode 100755 index d58cafd..0000000 --- a/django_app.py +++ /dev/null @@ -1,47 +0,0 @@ -import sys, os -from google.appengine.ext.webapp import util - - -sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path - - -# Django imports and other code go here... -import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' -from google.appengine.dist import use_library -use_library('django', '1.2') - - -import django.core.handlers, django.core.handlers.wsgi - -from django.conf import settings -settings.ROOT_URLCONF="django_urls" - - - -import logging -import django.core.signals -import django.dispatch.dispatcher -import django.db - -def log_exception(*args, **kwds): - logging.exception('Exception in request:') - -# Log errors. -django.dispatch.Signal.connect( - django.core.signals.got_request_exception, log_exception) - -# Unregister the rollback event handler. -django.dispatch.Signal.disconnect( - django.core.signals.got_request_exception, - django.db._rollback_on_exception) - - - -def main(): - sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path - application = django.core.handlers.wsgi.WSGIHandler() - util.run_wsgi_app(application) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/django_templates/base.html b/django_templates/base.html deleted file mode 100755 index f1e3544..0000000 --- a/django_templates/base.html +++ /dev/null @@ -1,165 +0,0 @@ -{% load account_tags %} -{% load cdn_helper %} - - - - {% block title %} {{ site.name }}: {% block subtitle %}{% endblock %} - {% endblock %} - - {% block opengraph %} - - - - {% endblock %} - - - - - - - - - - - - - - - - - - - - - - - {% block headextra %} - - - - - {% endblock %} - {% if site.google_analytics_code %} - - {% endif %} - - - - - -
- -
- - {% if user.confirmed_at %} - add an event - event queue - sources - {% if admin %} - edit site details - update logo - manage users - {% endif %} - edit your profile - sign out - {% else %} - - add an event - event queue - sources - {% endif %} - feedback - -
- - - -
-
-
-

- - {% if site.logo_asset_href %} - {{ site.name }} - {% else %} - {{ site.name }} - {% endif %} -

- {% if site.twitter %} -
Get events as soon as we do, follow @{{site.twitter}}!
{% endif %} - -
-
- - -
-
- -{% if messages %} -
- -
-{% endif %} - - -{% block content %} - -{% endblock %} -
- - -
-{% block endcode %} - -{% endblock %} - - - diff --git a/django_urls.py b/django_urls.py deleted file mode 100755 index 27ea2c0..0000000 --- a/django_urls.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2008 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. - -from django.conf.urls.defaults import * - -urlpatterns = patterns('', - # Example: - (r'^account/', include('account.urls')), - url(r'^_ah/login_required', 'account.views.signin', name="account-signin"), - (r'^events/', include('events.urls')), - (r'^sources/', include('sources.urls')), - (r'^subscriptions/', include('subscriptions.urls')), - (r'^admin/', include('eventsite.admin.urls')), - (r'^assets/', include('assets.urls')), - (r'', include('eventsite.urls')), -) diff --git a/handlers.py b/handlers.py deleted file mode 100755 index 09cf650..0000000 --- a/handlers.py +++ /dev/null @@ -1,13 +0,0 @@ -from tipfy import RequestHandler -from tipfy.ext.jinja2 import render_response - -class FrontPageHandler(RequestHandler): - """A handler that outputs the result of a rendered template.""" - def get(self, **kwargs): - return render_response('hello.html', message='Hello, Jinja!') - - -class AddEventHandler(RequestHandler): - """A handler that outputs the result of a rendered template.""" - def get(self, **kwargs): - return render_response('hello.html', message='Hello, Jinja!') \ No newline at end of file diff --git a/apps/__init__.py b/links/__init__.py similarity index 100% rename from apps/__init__.py rename to links/__init__.py diff --git a/apps/links/forms.py b/links/forms.py similarity index 100% rename from apps/links/forms.py rename to links/forms.py diff --git a/apps/links/handlers.py b/links/handlers.py similarity index 100% rename from apps/links/handlers.py rename to links/handlers.py diff --git a/apps/links/models.py b/links/models.py similarity index 100% rename from apps/links/models.py rename to links/models.py diff --git a/links/urls.py b/links/urls.py new file mode 100755 index 0000000..1f73a16 --- /dev/null +++ b/links/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('links.views', + + url(r'^add/$','add', name="add_link"), + url(r'^review/$','review', name="review_links"), + url(r'^change/$','add', name="change_link"), + + +) + diff --git a/apps/links/__init__.py b/links/views.py old mode 100755 new mode 100644 similarity index 100% rename from apps/links/__init__.py rename to links/views.py diff --git a/main.py b/main.py index 2739e4f..b297297 100755 --- a/main.py +++ b/main.py @@ -1,38 +1,47 @@ -# -*- coding: utf-8 -*- -""" - main - ~~~~ +import sys, os +from google.appengine.ext.webapp import util - Run Tipfy apps. - :copyright: 2009 by tipfy.org. - :license: BSD, see LICENSE for more details. -""" +sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path + + +# Django imports and other code go here... import os -import sys +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' +from google.appengine.dist import use_library +use_library('django', '1.2') -if 'lib' not in sys.path: - # Add /lib as primary libraries directory, with fallback to /distlib - # and optionally to distlib loaded using zipimport. - sys.path[0:0] = ['lib', 'distlib', 'distlib.zip', 'shared'] - -import config -import tipfy +import django.core.handlers, django.core.handlers.wsgi -# Is this the development server? -debug = os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') +from django.conf import settings +settings.ROOT_URLCONF="urls" -# Instantiate the application. -app = tipfy.make_wsgi_app(config=config.config, debug=debug) -from tipfy.ext.jinja2 import get_jinja2_instance -env=get_jinja2_instance() -env.globals['app_version'] = os.environ['CURRENT_VERSION_ID'] or 'dev' -def main(): - app.run() +import logging +import django.core.signals +import django.dispatch.dispatcher +import django.db + +def log_exception(*args, **kwds): + logging.exception('Exception in request:') + +# Log errors. +django.dispatch.Signal.connect( + django.core.signals.got_request_exception, log_exception) +# Unregister the rollback event handler. +django.dispatch.Signal.disconnect( + django.core.signals.got_request_exception, + django.db._rollback_on_exception) + + + +def main(): + sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path + application = django.core.handlers.wsgi.WSGIHandler() + util.run_wsgi_app(application) if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/mapreduce.yaml b/mapreduce.yaml deleted file mode 100755 index 4380181..0000000 --- a/mapreduce.yaml +++ /dev/null @@ -1,8 +0,0 @@ -mapreduce: -- name: 'migrate.process' - mapper: - input_reader: mapreduce.input_readers.DatastoreInputReader - handler: migrate.process - params: - - name: entity_kind - default: events.models.Event \ No newline at end of file diff --git a/mapreduce/__init__.py b/mapreduce/__init__.py deleted file mode 100755 index de5df1c..0000000 --- a/mapreduce/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/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. - diff --git a/mapreduce/base_handler.py b/mapreduce/base_handler.py deleted file mode 100755 index 00b88eb..0000000 --- a/mapreduce/base_handler.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/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. - -"""Base handler class for all mapreduce handlers. -""" - - - - -import logging -from mapreduce.lib import simplejson - -from google.appengine.ext import webapp - - -class Error(Exception): - """Base-class for exceptions in this module.""" - - -class BadRequestPathError(Error): - """The request path for the handler is invalid.""" - - -class BaseHandler(webapp.RequestHandler): - """Base class for all mapreduce handlers.""" - - def base_path(self): - """Base path for all mapreduce-related urls.""" - path = self.request.path - return path[:path.rfind("/")] - - -class TaskQueueHandler(BaseHandler): - """Base class for handlers intended to be run only from the task queue. - - Sub-classes should implement the 'handle' method. - """ - - def post(self): - if "X-AppEngine-QueueName" not in self.request.headers: - logging.error(self.request.headers) - logging.error("Task queue handler received non-task queue request") - self.response.set_status( - 403, message="Task queue handler received non-task queue request") - return - self.handle() - - def handle(self): - """To be implemented by subclasses.""" - raise NotImplementedError() - - def task_retry_count(self): - """Number of times this task has been retried.""" - return int(self.request.headers.get("X-AppEngine-TaskRetryCount", 0)) - - -class JsonHandler(BaseHandler): - """Base class for JSON handlers for user interface. - - Sub-classes should implement the 'handle' method. They should put their - response data in the 'self.json_response' dictionary. Any exceptions raised - by the sub-class implementation will be sent in a JSON response with the - name of the error_class and the error_message. - """ - - def __init__(self): - """Initializer.""" - super(BaseHandler, self).__init__() - self.json_response = {} - - def base_path(self): - """Base path for all mapreduce-related urls. - - JSON handlers are mapped to /base_path/command/command_name thus they - require special treatment. - """ - path = self.request.path - base_path = path[:path.rfind("/")] - if not base_path.endswith("/command"): - raise BadRequestPathError( - "Json handlers should have /command path prefix") - return base_path[:base_path.rfind("/")] - - def _handle_wrapper(self): - if self.request.headers.get("X-Requested-With") != "XMLHttpRequest": - logging.error(self.request.headers) - logging.error("Got JSON request with no X-Requested-With header") - self.response.set_status( - 403, message="Got JSON request with no X-Requested-With header") - return - - self.json_response.clear() - try: - self.handle() - except Exception, e: - logging.exception("Error in JsonHandler, returning exception.") - # TODO(user): Include full traceback here for the end-user. - self.json_response.clear() - self.json_response["error_class"] = e.__class__.__name__ - self.json_response["error_message"] = str(e) - - self.response.headers["Content-Type"] = "text/javascript" - try: - output = simplejson.dumps(self.json_response) - except: - logging.exception("Could not serialize to JSON") - self.response.set_status(500, message="Could not serialize to JSON") - return - else: - self.response.out.write(output) - - def handle(self): - """To be implemented by sub-classes.""" - raise NotImplementedError() - - -class PostJsonHandler(JsonHandler): - """JSON handler that accepts POST requests.""" - - def post(self): - self._handle_wrapper() - - -class GetJsonHandler(JsonHandler): - """JSON handler that accepts GET posts.""" - - def get(self): - self._handle_wrapper() diff --git a/mapreduce/context.py b/mapreduce/context.py deleted file mode 100755 index 93c1017..0000000 --- a/mapreduce/context.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/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. - -"""Mapreduce execution context. - -Mapreduce context provides handler code with information about -current mapreduce execution and organizes utility data flow -from handlers such as counters, log messages, mutation pools. -""" - - - -__all__ = ["MAX_ENTITY_COUNT", "MAX_POOL_SIZE", "Context", "MutationPool", - "Counters", "ItemList", "EntityList", "get", "COUNTER_MAPPER_CALLS", - "DATASTORE_DEADLINE"] - -from google.appengine.api import datastore -from google.appengine.ext import db - -# Maximum pool size in bytes. Pool will be flushed when reaches this amount. -# We use 950,000 bytes which is slightly less than maximum allowed RPC size of -# 1M to have some space cushion. -MAX_POOL_SIZE = 900 * 1000 - -# Maximum number of items. Pool will be flushed when reaches this amount. -MAX_ENTITY_COUNT = 500 - -# Deadline in seconds for mutation pool datastore operations. -DATASTORE_DEADLINE = 15 - -# The name of the counter which counts all mapper calls. -COUNTER_MAPPER_CALLS = "mapper_calls" - - -def _normalize_entity(value): - """Return an entity from an entity or model instance.""" - # TODO(user): Consider using datastore.NormalizeAndTypeCheck. - if getattr(value, "_populate_internal_entity", None): - return value._populate_internal_entity() - return value - -def _normalize_key(value): - """Return a key from an entity, model instance, key, or key string.""" - if getattr(value, "key", None): - return value.key() - elif isinstance(value, basestring): - return datastore.Key(value) - else: - return value - -class ItemList(object): - """Holds list of arbitrary items, and their total size. - - Properties: - items: list of objects. - length: length of item list. - size: aggregate item size in bytes. - """ - - def __init__(self): - """Constructor.""" - self.items = [] - self.length = 0 - self.size = 0 - - def append(self, item, item_size): - """Add new item to the list. - - Args: - item: an item to add to the list. - item_size: item size in bytes as int. - """ - self.items.append(item) - self.length += 1 - self.size += item_size - - def clear(self): - """Clear item list.""" - self.items = [] - self.length = 0 - self.size = 0 - - @property - def entities(self): - """Return items. For backwards compatability.""" - return self.items - - -# For backwards compatability. -EntityList = ItemList - - -# TODO(user): mutation pool has no error handling at all. Add some. -class MutationPool(object): - """Mutation pool accumulates datastore changes to perform them in batch. - - Properties: - puts: ItemList of entities to put to datastore. - deletes: ItemList of keys to delete from datastore. - max_pool_size: maximum single list pool size. List changes will be flushed - when this size is reached. - """ - - def __init__(self, - max_pool_size=MAX_POOL_SIZE, - max_entity_count=MAX_ENTITY_COUNT): - """Constructor. - - Args: - max_pool_size: maximum pools size in bytes before flushing it to db. - max_entity_count: maximum number of entities before flushing it to db. - """ - self.max_pool_size = max_pool_size - self.max_entity_count = max_entity_count - self.puts = ItemList() - self.deletes = ItemList() - - def put(self, entity): - """Registers entity to put to datastore. - - Args: - entity: an entity or model instance to put. - """ - actual_entity = _normalize_entity(entity) - entity_size = len(actual_entity._ToPb().Encode()) - if (self.puts.length >= self.max_entity_count or - (self.puts.size + entity_size) > self.max_pool_size): - self.__flush_puts() - self.puts.append(actual_entity, entity_size) - - def delete(self, entity): - """Registers entity to delete from datastore. - - Args: - entity: an entity, model instance, or key to delete. - """ - # This is not very nice: we're calling two protected methods here... - key = _normalize_key(entity) - key_size = len(key._ToPb().Encode()) - if (self.deletes.length >= self.max_entity_count or - (self.deletes.size + key_size) > self.max_pool_size): - self.__flush_deletes() - self.deletes.append(key, key_size) - - # TODO(user): some kind of error handling/retries is needed here. - def flush(self): - """Flush(apply) all changed to datastore.""" - self.__flush_puts() - self.__flush_deletes() - - def __flush_puts(self): - """Flush all puts to datastore.""" - if self.puts.length: - datastore.Put(self.puts.items, rpc=self.__create_rpc()) - self.puts.clear() - - def __flush_deletes(self): - """Flush all deletes to datastore.""" - if self.deletes.length: - datastore.Delete(self.deletes.items, rpc=self.__create_rpc()) - self.deletes.clear() - - def __create_rpc(self): - """Creates correctly configured RPC object for datastore calls. - - Returns: - A UserRPC instance. - """ - return datastore.CreateRPC(deadline=DATASTORE_DEADLINE) - - -# This doesn't do much yet. In future it will play nicely with checkpoint/error -# handling system. -class Counters(object): - """Regulates access to counters.""" - - def __init__(self, shard_state): - """Constructor. - - Args: - shard_state: current mapreduce shard state as model.ShardState. - """ - self._shard_state = shard_state - - def increment(self, counter_name, delta=1): - """Increment counter value. - - Args: - counter_name: name of the counter as string. - delta: increment delta as int. - """ - self._shard_state.counters_map.increment(counter_name, delta) - - def flush(self): - """Flush unsaved counter values.""" - pass - - -class Context(object): - """MapReduce execution context. - - Properties: - mapreduce_spec: current mapreduce specification as model.MapreduceSpec. - shard_state: current shard state as model.ShardState. - mutation_pool: current mutation pool as MutationPool. - counters: counters object as Counters. - """ - - # Current context instance - _context_instance = None - - def __init__(self, mapreduce_spec, shard_state, task_retry_count=0): - """Constructor. - - Args: - mapreduce_spec: mapreduce specification as model.MapreduceSpec. - shard_state: shard state as model.ShardState. - """ - self.mapreduce_spec = mapreduce_spec - self.shard_state = shard_state - self.task_retry_count = task_retry_count - - if self.mapreduce_spec: - self.mapreduce_id = self.mapreduce_spec.mapreduce_id - else: - # Only in tests - self.mapreduce_id = None - if self.shard_state: - self.shard_id = self.shard_state.get_shard_id() - else: - # Only in tests - self.shard_id = None - - self.mutation_pool = MutationPool( - max_pool_size=(MAX_POOL_SIZE/(2**self.task_retry_count)), - max_entity_count=(MAX_ENTITY_COUNT/(2**self.task_retry_count))) - self.counters = Counters(shard_state) - - self._pools = {} - self.register_pool("mutation_pool", self.mutation_pool) - self.register_pool("counters", self.counters) - - def flush(self): - """Flush all information recorded in context.""" - for pool in self._pools.values(): - pool.flush() - if self.shard_state: - self.shard_state.put() - - # TODO(user): Add convenience method for mapper params. - - # TODO(user): Add fatal error logging method here. Will log the message - # and set the shard state to failure result status, which the controller - # callback should pick up and force all shards to terminate. - - def register_pool(self, key, pool): - """Register an arbitrary pool to be flushed together with this context. - - Args: - key: pool key as string. - pool: a pool instance. Pool should implement flush(self) method. - """ - self._pools[key] = pool - - def get_pool(self, key): - """Obtains an instance of registered pool. - - Args: - key: pool key as string. - - Returns: - an instance of the pool registered earlier, or None. - """ - return self._pools.get(key, None) - - @classmethod - def _set(cls, context): - """Set current context instance. - - Args: - context: new context as Context or None. - """ - cls._context_instance = context - - -def get(): - """Get current context instance. - - Returns: - current context as Context. - """ - return Context._context_instance diff --git a/mapreduce/control.py b/mapreduce/control.py deleted file mode 100755 index 616d55f..0000000 --- a/mapreduce/control.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/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. - -"""API for controlling MapReduce execution outside of MapReduce framework.""" - - - -__all__ = ["start_map"] - -# pylint: disable-msg=C6409 - - -from mapreduce import handlers -from mapreduce import model - - -_DEFAULT_SHARD_COUNT = 8 - - -def start_map(name, - handler_spec, - reader_spec, - reader_parameters, - shard_count=_DEFAULT_SHARD_COUNT, - mapreduce_parameters=None, - base_path="/mapreduce", - queue_name="default", - eta=None, - countdown=None, - hooks_class_name=None, - _app=None, - transactional=False): - """Start a new, mapper-only mapreduce. - - Args: - name: mapreduce name. Used only for display purposes. - handler_spec: fully qualified name of mapper handler function/class to call. - reader_spec: fully qualified name of mapper reader to use - reader_parameters: dictionary of parameters to pass to reader. These are - reader-specific. - shard_count: number of shards to create. - mapreduce_parameters: dictionary of mapreduce parameters relevant to the - whole job. - base_path: base path of mapreduce library handler specified in app.yaml. - "/mapreduce" by default. - queue_name: executor queue name to be used for mapreduce tasks. - eta: Absolute time when the MR should execute. May not be specified - if 'countdown' is also supplied. This may be timezone-aware or - timezone-naive. - countdown: Time in seconds into the future that this MR should execute. - Defaults to zero. - hooks_class_name: fully qualified name of a hooks.Hooks subclass. - transactional: Specifies if job should be started as a part of already - opened transaction. - - Returns: - mapreduce id as string. - """ - mapper_spec = model.MapperSpec(handler_spec, reader_spec, reader_parameters, - shard_count) - - return handlers.StartJobHandler._start_map( - name, - mapper_spec, - mapreduce_parameters or {}, - base_path=base_path, - queue_name=queue_name, - eta=eta, - countdown=countdown, - hooks_class_name=hooks_class_name, - _app=_app, - transactional=transactional) diff --git a/mapreduce/handlers.py b/mapreduce/handlers.py deleted file mode 100755 index 99702c9..0000000 --- a/mapreduce/handlers.py +++ /dev/null @@ -1,876 +0,0 @@ -#!/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. -# - -"""Defines executor tasks handlers for MapReduce implementation.""" - - - -# Disable "Invalid method name" -# pylint: disable-msg=C6409 - -import datetime -import logging -import math -import os -from mapreduce.lib import simplejson -import time - -from google.appengine.api import memcache -from google.appengine.api.labs import taskqueue -from google.appengine.ext import db -from mapreduce import base_handler -from mapreduce import context -from mapreduce import model -from mapreduce import quota -from mapreduce import util - - -# TODO(user): Make this a product of the reader or in quotas.py -_QUOTA_BATCH_SIZE = 20 - -# The amount of time to perform scanning in one slice. New slice will be -# scheduled as soon as current one takes this long. -_SLICE_DURATION_SEC = 15 - -# Delay between consecutive controller callback invocations. -_CONTROLLER_PERIOD_SEC = 2 - - -class Error(Exception): - """Base class for exceptions in this module.""" - - -class NotEnoughArgumentsError(Error): - """Required argument is missing.""" - - -class NoDataError(Error): - """There is no data present for a desired input.""" - - -def _run_task_hook(hooks, method, task, queue_name): - """Invokes hooks.method(task, queue_name). - - Args: - hooks: A hooks.Hooks instance or None. - method: The name of the method to invoke on the hooks class e.g. - "enqueue_kickoff_task". - task: The taskqueue.Task to pass to the hook method. - queue_name: The name of the queue to pass to the hook method. - - Returns: - True if the hooks.Hooks instance handled the method, False otherwise. - """ - if hooks is not None: - try: - getattr(hooks, method)(task, queue_name) - except NotImplementedError: - # Use the default task addition implementation. - return False - - return True - return False - - -class MapperWorkerCallbackHandler(base_handler.TaskQueueHandler): - """Callback handler for mapreduce worker task. - - Request Parameters: - mapreduce_spec: MapreduceSpec of the mapreduce serialized to json. - shard_id: id of the shard. - slice_id: id of the slice. - """ - - def __init__(self, time_function=time.time): - """Constructor. - - Args: - time_function: time function to use to obtain current time. - """ - base_handler.TaskQueueHandler.__init__(self) - self._time = time_function - - def handle(self): - """Handle request.""" - spec = model.MapreduceSpec.from_json_str( - self.request.get("mapreduce_spec")) - self._start_time = self._time() - shard_id = self.shard_id() - - # TODO(user): Make this prettier - logging.debug("post: shard=%s slice=%s headers=%s", - shard_id, self.slice_id(), self.request.headers) - - shard_state, control = db.get([ - model.ShardState.get_key_by_shard_id(shard_id), - model.MapreduceControl.get_key_by_job_id(spec.mapreduce_id), - ]) - if not shard_state: - # We're letting this task to die. It's up to controller code to - # reinitialize and restart the task. - logging.error("State not found for shard ID %r; shutting down", - shard_id) - return - - if control and control.command == model.MapreduceControl.ABORT: - logging.info("Abort command received by shard %d of job '%s'", - shard_state.shard_number, shard_state.mapreduce_id) - shard_state.active = False - shard_state.result_status = model.ShardState.RESULT_ABORTED - shard_state.put() - model.MapreduceControl.abort(spec.mapreduce_id) - return - - input_reader = self.input_reader(spec.mapper) - - if spec.mapper.params.get("enable_quota", True): - quota_consumer = quota.QuotaConsumer( - quota.QuotaManager(memcache.Client()), - shard_id, - _QUOTA_BATCH_SIZE) - else: - quota_consumer = None - - ctx = context.Context(spec, shard_state, - task_retry_count=self.task_retry_count()) - context.Context._set(ctx) - - try: - # consume quota ahead, because we do not want to run a datastore - # query if there's not enough quota for the shard. - if not quota_consumer or quota_consumer.check(): - scan_aborted = False - entity = None - - # We shouldn't fetch an entity from the reader if there's not enough - # quota to process it. Perform all quota checks proactively. - if not quota_consumer or quota_consumer.consume(): - for entity in input_reader: - if isinstance(entity, db.Model): - shard_state.last_work_item = repr(entity.key()) - else: - shard_state.last_work_item = repr(entity)[:100] - - scan_aborted = not self.process_entity(entity, ctx) - - # Check if we've got enough quota for the next entity. - if (quota_consumer and not scan_aborted and - not quota_consumer.consume()): - scan_aborted = True - if scan_aborted: - break - else: - scan_aborted = True - - - if not scan_aborted: - logging.info("Processing done for shard %d of job '%s'", - shard_state.shard_number, shard_state.mapreduce_id) - # We consumed extra quota item at the end of for loop. - # Just be nice here and give it back :) - if quota_consumer: - quota_consumer.put(1) - shard_state.active = False - shard_state.result_status = model.ShardState.RESULT_SUCCESS - - # TODO(user): Mike said we don't want this happen in case of - # exception while scanning. Figure out when it's appropriate to skip. - ctx.flush() - finally: - context.Context._set(None) - if quota_consumer: - quota_consumer.dispose() - - # Rescheduling work should always be the last statement. It shouldn't happen - # if there were any exceptions in code before it. - if shard_state.active: - self.reschedule(spec, input_reader) - - def process_entity(self, entity, ctx): - """Process a single entity. - - Call mapper handler on the entity. - - Args: - entity: an entity to process. - ctx: current execution context. - - Returns: - True if scan should be continued, False if scan should be aborted. - """ - ctx.counters.increment(context.COUNTER_MAPPER_CALLS) - - handler = ctx.mapreduce_spec.mapper.handler - if util.is_generator_function(handler): - for result in handler(entity): - if callable(result): - result(ctx) - else: - try: - if len(result) == 2: - logging.error("Collectors not implemented yet") - else: - logging.error("Got bad output tuple of length %d", len(result)) - except TypeError: - logging.error( - "Handler yielded type %s, expected a callable or a tuple", - result.__class__.__name__) - else: - handler(entity) - - if self._time() - self._start_time > _SLICE_DURATION_SEC: - logging.debug("Spent %s seconds. Rescheduling", - self._time() - self._start_time) - return False - return True - - def shard_id(self): - """Get shard unique identifier of this task from request. - - Returns: - shard identifier as string. - """ - return str(self.request.get("shard_id")) - - def slice_id(self): - """Get slice unique identifier of this task from request. - - Returns: - slice identifier as int. - """ - return int(self.request.get("slice_id")) - - def input_reader(self, mapper_spec): - """Get the reader from mapper_spec initialized with the request's state. - - Args: - mapper_spec: a mapper spec containing the immutable mapper state. - - Returns: - An initialized InputReader. - """ - input_reader_spec_dict = simplejson.loads( - self.request.get("input_reader_state")) - return mapper_spec.input_reader_class().from_json( - input_reader_spec_dict) - - @staticmethod - def worker_parameters(mapreduce_spec, - shard_id, - slice_id, - input_reader): - """Fill in mapper worker task parameters. - - Returned parameters map is to be used as task payload, and it contains - all the data, required by mapper worker to perform its function. - - Args: - mapreduce_spec: specification of the mapreduce. - shard_id: id of the shard (part of the whole dataset). - slice_id: id of the slice (part of the shard). - input_reader: InputReader containing the remaining inputs for this - shard. - - Returns: - string->string map of parameters to be used as task payload. - """ - return {"mapreduce_spec": mapreduce_spec.to_json_str(), - "shard_id": shard_id, - "slice_id": str(slice_id), - "input_reader_state": input_reader.to_json_str()} - - @staticmethod - def get_task_name(shard_id, slice_id): - """Compute single worker task name. - - Args: - shard_id: id of the shard (part of the whole dataset) as string. - slice_id: id of the slice (part of the shard) as int. - - Returns: - task name which should be used to process specified shard/slice. - """ - # Prefix the task name with something unique to this framework's - # namespace so we don't conflict with user tasks on the queue. - return "appengine-mrshard-%s-%s" % (shard_id, slice_id) - - def reschedule(self, mapreduce_spec, input_reader): - """Reschedule worker task to continue scanning work. - - Args: - mapreduce_spec: mapreduce specification. - input_reader: remaining input reader to process. - """ - MapperWorkerCallbackHandler.schedule_slice( - self.base_path(), mapreduce_spec, self.shard_id(), - self.slice_id() + 1, input_reader) - - @classmethod - def schedule_slice(cls, - base_path, - mapreduce_spec, - shard_id, - slice_id, - input_reader, - queue_name=None, - eta=None, - countdown=None): - """Schedule slice scanning by adding it to the task queue. - - Args: - base_path: base_path of mapreduce request handlers as string. - mapreduce_spec: mapreduce specification as MapreduceSpec. - shard_id: current shard id as string. - slice_id: slice id as int. - input_reader: remaining InputReader for given shard. - queue_name: Optional queue to run on; uses the current queue of - execution or the default queue if unspecified. - eta: Absolute time when the MR should execute. May not be specified - if 'countdown' is also supplied. This may be timezone-aware or - timezone-naive. - countdown: Time in seconds into the future that this MR should execute. - Defaults to zero. - """ - task_params = MapperWorkerCallbackHandler.worker_parameters( - mapreduce_spec, shard_id, slice_id, input_reader) - task_name = MapperWorkerCallbackHandler.get_task_name(shard_id, slice_id) - queue_name = os.environ.get("HTTP_X_APPENGINE_QUEUENAME", - queue_name or "default") - - worker_task = taskqueue.Task(url=base_path + "/worker_callback", - params=task_params, - name=task_name, - eta=eta, - countdown=countdown) - - if not _run_task_hook(mapreduce_spec.get_hooks(), - "enqueue_worker_task", - worker_task, - queue_name): - try: - worker_task.add(queue_name) - except (taskqueue.TombstonedTaskError, - taskqueue.TaskAlreadyExistsError), e: - logging.warning("Task %r with params %r already exists. %s: %s", - task_name, task_params, e.__class__, e) - - -class ControllerCallbackHandler(base_handler.TaskQueueHandler): - """Supervises mapreduce execution. - - Is also responsible for gathering execution status from shards together. - - This task is "continuously" running by adding itself again to taskqueue if - mapreduce is still active. - """ - - def __init__(self, time_function=time.time): - """Constructor. - - Args: - time_function: time function to use to obtain current time. - """ - base_handler.TaskQueueHandler.__init__(self) - self._time = time_function - - def handle(self): - """Handle request.""" - spec = model.MapreduceSpec.from_json_str( - self.request.get("mapreduce_spec")) - - # TODO(user): Make this logging prettier. - logging.debug("post: id=%s headers=%s", - spec.mapreduce_id, self.request.headers) - - state, control = db.get([ - model.MapreduceState.get_key_by_job_id(spec.mapreduce_id), - model.MapreduceControl.get_key_by_job_id(spec.mapreduce_id), - ]) - if not state: - logging.error("State not found for mapreduce_id '%s'; skipping", - spec.mapreduce_id) - return - - shard_states = model.ShardState.find_by_mapreduce_id(spec.mapreduce_id) - if state.active and len(shard_states) != spec.mapper.shard_count: - # Some shards were lost - logging.error("Incorrect number of shard states: %d vs %d; " - "aborting job '%s'", - len(shard_states), spec.mapper.shard_count, - spec.mapreduce_id) - state.active = False - state.result_status = model.MapreduceState.RESULT_FAILED - model.MapreduceControl.abort(spec.mapreduce_id) - - active_shards = [s for s in shard_states if s.active] - failed_shards = [s for s in shard_states - if s.result_status == model.ShardState.RESULT_FAILED] - aborted_shards = [s for s in shard_states - if s.result_status == model.ShardState.RESULT_ABORTED] - if state.active: - state.active = bool(active_shards) - state.active_shards = len(active_shards) - state.failed_shards = len(failed_shards) - state.aborted_shards = len(aborted_shards) - - if (not state.active and control and - control.command == model.MapreduceControl.ABORT): - # User-initiated abort *after* all shards have completed. - logging.info("Abort signal received for job '%s'", spec.mapreduce_id) - state.result_status = model.MapreduceState.RESULT_ABORTED - - if not state.active: - state.active_shards = 0 - if not state.result_status: - # Set final result status derived from shard states. - if [s for s in shard_states - if s.result_status != model.ShardState.RESULT_SUCCESS]: - state.result_status = model.MapreduceState.RESULT_FAILED - else: - state.result_status = model.MapreduceState.RESULT_SUCCESS - logging.info("Final result for job '%s' is '%s'", - spec.mapreduce_id, state.result_status) - - # We don't need a transaction here, since we change only statistics data, - # and we don't care if it gets overwritten/slightly inconsistent. - self.aggregate_state(state, shard_states) - poll_time = state.last_poll_time - state.last_poll_time = datetime.datetime.utcfromtimestamp(self._time()) - - if not state.active: - # This is the last execution. - # Enqueue done_callback if needed. - def put_state(state): - state.put() - done_callback = spec.params.get( - model.MapreduceSpec.PARAM_DONE_CALLBACK) - if done_callback: - done_task = taskqueue.Task( - url=done_callback, - headers={"Mapreduce-Id": spec.mapreduce_id}) - queue_name = spec.params.get( - model.MapreduceSpec.PARAM_DONE_CALLBACK_QUEUE, - "default") - - if not _run_task_hook(spec.get_hooks(), - "enqueue_done_task", - done_task, - queue_name): - done_task.add(queue_name, transactional=True) - db.run_in_transaction(put_state, state) - return - else: - state.put() - - processing_rate = int(spec.mapper.params.get( - "processing_rate") or model._DEFAULT_PROCESSING_RATE_PER_SEC) - self.refill_quotas(poll_time, processing_rate, active_shards) - ControllerCallbackHandler.reschedule( - self.base_path(), spec, self.serial_id() + 1) - - def aggregate_state(self, mapreduce_state, shard_states): - """Update current mapreduce state by aggregating shard states. - - Args: - mapreduce_state: current mapreduce state as MapreduceState. - shard_states: all shard states (active and inactive). list of ShardState. - """ - processed_counts = [] - mapreduce_state.counters_map.clear() - - for shard_state in shard_states: - mapreduce_state.counters_map.add_map(shard_state.counters_map) - processed_counts.append(shard_state.counters_map.get( - context.COUNTER_MAPPER_CALLS)) - - mapreduce_state.set_processed_counts(processed_counts) - - def refill_quotas(self, - last_poll_time, - processing_rate, - active_shard_states): - """Refill quotas for all active shards. - - Args: - last_poll_time: Datetime with the last time the job state was updated. - processing_rate: How many items to process per second overall. - active_shard_states: All active shard states, list of ShardState. - """ - if not active_shard_states: - return - quota_manager = quota.QuotaManager(memcache.Client()) - - current_time = int(self._time()) - last_poll_time = time.mktime(last_poll_time.timetuple()) - total_quota_refill = processing_rate * max(0, current_time - last_poll_time) - quota_refill = int(math.ceil( - 1.0 * total_quota_refill / len(active_shard_states))) - - if not quota_refill: - return - - # TODO(user): use batch memcache API to refill quota in one API call. - for shard_state in active_shard_states: - quota_manager.put(shard_state.shard_id, quota_refill) - - def serial_id(self): - """Get serial unique identifier of this task from request. - - Returns: - serial identifier as int. - """ - return int(self.request.get("serial_id")) - - @staticmethod - def get_task_name(mapreduce_spec, serial_id): - """Compute single controller task name. - - Args: - mapreduce_spec: specification of the mapreduce. - serial_id: id of the invocation as int. - - Returns: - task name which should be used to process specified shard/slice. - """ - # Prefix the task name with something unique to this framework's - # namespace so we don't conflict with user tasks on the queue. - return "appengine-mrcontrol-%s-%s" % ( - mapreduce_spec.mapreduce_id, serial_id) - - @staticmethod - def controller_parameters(mapreduce_spec, serial_id): - """Fill in controller task parameters. - - Returned parameters map is to be used as task payload, and it contains - all the data, required by controller to perform its function. - - Args: - mapreduce_spec: specification of the mapreduce. - serial_id: id of the invocation as int. - - Returns: - string->string map of parameters to be used as task payload. - """ - return {"mapreduce_spec": mapreduce_spec.to_json_str(), - "serial_id": str(serial_id)} - - @classmethod - def reschedule(cls, base_path, mapreduce_spec, serial_id, queue_name=None): - """Schedule new update status callback task. - - Args: - base_path: mapreduce handlers url base path as string. - mapreduce_spec: mapreduce specification as MapreduceSpec. - serial_id: id of the invocation as int. - queue_name: The queue to schedule this task on. Will use the current - queue of execution if not supplied. - """ - task_name = ControllerCallbackHandler.get_task_name( - mapreduce_spec, serial_id) - task_params = ControllerCallbackHandler.controller_parameters( - mapreduce_spec, serial_id) - if not queue_name: - queue_name = os.environ.get("HTTP_X_APPENGINE_QUEUENAME", "default") - - controller_callback_task = taskqueue.Task( - url=base_path + "/controller_callback", - name=task_name, params=task_params, - countdown=_CONTROLLER_PERIOD_SEC) - - if not _run_task_hook(mapreduce_spec.get_hooks(), - "enqueue_controller_task", - controller_callback_task, - queue_name): - try: - controller_callback_task.add(queue_name) - except (taskqueue.TombstonedTaskError, - taskqueue.TaskAlreadyExistsError), e: - logging.warning("Task %r with params %r already exists. %s: %s", - task_name, task_params, e.__class__, e) - - -class KickOffJobHandler(base_handler.TaskQueueHandler): - """Taskqueue handler which kicks off a mapreduce processing. - - Request Parameters: - mapreduce_spec: MapreduceSpec of the mapreduce serialized to json. - input_readers: List of InputReaders objects separated by semi-colons. - """ - - def handle(self): - """Handles kick off request.""" - spec = model.MapreduceSpec.from_json_str( - self._get_required_param("mapreduce_spec")) - app_id = self.request.get("app", None) - queue_name = os.environ.get("HTTP_X_APPENGINE_QUEUENAME", "default") - mapper_input_reader_class = spec.mapper.input_reader_class() - - # StartJobHandler might have already saved the state, but it's OK - # to override it because we're using the same mapreduce id. - state = model.MapreduceState.create_new(spec.mapreduce_id) - state.mapreduce_spec = spec - state.active = True - # TODO(user): Initialize UI fields correctly. - state.char_url = "" - state.sparkline_url = "" - if app_id: - state.app_id = app_id - - input_readers = mapper_input_reader_class.split_input(spec.mapper) - if not input_readers: - # We don't have any data. Finish map. - logging.warning("Found no mapper input data to process.") - state.active = False - state.active_shards = 0 - state.put() - return - - # Update state and spec with actual shard count. - spec.mapper.shard_count = len(input_readers) - state.active_shards = len(input_readers) - state.mapreduce_spec = spec - state.put() - - KickOffJobHandler._schedule_shards( - spec, input_readers, queue_name, self.base_path()) - - ControllerCallbackHandler.reschedule( - self.base_path(), spec, queue_name=queue_name, serial_id=0) - - def _get_required_param(self, param_name): - """Get a required request parameter. - - Args: - param_name: name of request parameter to fetch. - - Returns: - parameter value - - Raises: - NotEnoughArgumentsError: if parameter is not specified. - """ - value = self.request.get(param_name) - if not value: - raise NotEnoughArgumentsError(param_name + " not specified") - return value - - @classmethod - def _schedule_shards(cls, spec, input_readers, queue_name, base_path): - """Prepares shard states and schedules their execution. - - Args: - spec: mapreduce specification as MapreduceSpec. - input_readers: list of InputReaders describing shard splits. - queue_name: The queue to run this job on. - base_path: The base url path of mapreduce callbacks. - """ - # Note: it's safe to re-attempt this handler because: - # - shard state has deterministic and unique key. - # - schedule_slice will fall back gracefully if a task already exists. - shard_states = [] - for shard_number, input_reader in enumerate(input_readers): - shard = model.ShardState.create_new(spec.mapreduce_id, shard_number) - shard.shard_description = str(input_reader) - shard_states.append(shard) - - # Retrievs already existing shards. - existing_shard_states = db.get(shard.key() for shard in shard_states) - existing_shard_keys = set(shard.key() for shard in existing_shard_states - if shard is not None) - - # Puts only non-existing shards. - db.put(shard for shard in shard_states - if shard.key() not in existing_shard_keys) - - for shard_number, input_reader in enumerate(input_readers): - shard_id = model.ShardState.shard_id_from_number( - spec.mapreduce_id, shard_number) - MapperWorkerCallbackHandler.schedule_slice( - base_path, spec, shard_id, 0, input_reader, queue_name=queue_name) - - -class StartJobHandler(base_handler.PostJsonHandler): - """Command handler starts a mapreduce job.""" - - def handle(self): - """Handles start request.""" - # Mapper spec as form arguments. - mapreduce_name = self._get_required_param("name") - mapper_input_reader_spec = self._get_required_param("mapper_input_reader") - mapper_handler_spec = self._get_required_param("mapper_handler") - mapper_params = self._get_params( - "mapper_params_validator", "mapper_params.") - params = self._get_params( - "params_validator", "params.") - - # Set some mapper param defaults if not present. - mapper_params["processing_rate"] = int(mapper_params.get( - "processing_rate") or model._DEFAULT_PROCESSING_RATE_PER_SEC) - queue_name = mapper_params["queue_name"] = mapper_params.get( - "queue_name", "default") - - # Validate the Mapper spec, handler, and input reader. - mapper_spec = model.MapperSpec( - mapper_handler_spec, - mapper_input_reader_spec, - mapper_params, - int(mapper_params.get("shard_count", model._DEFAULT_SHARD_COUNT))) - - mapreduce_id = type(self)._start_map( - mapreduce_name, - mapper_spec, - params, - base_path=self.base_path(), - queue_name=queue_name, - _app=mapper_params.get("_app")) - self.json_response["mapreduce_id"] = mapreduce_id - - def _get_params(self, validator_parameter, name_prefix): - """Retrieves additional user-supplied params for the job and validates them. - - Args: - validator_parameter: name of the request parameter which supplies - validator for this parameter set. - name_prefix: common prefix for all parameter names in the request. - - Raises: - Any exception raised by the 'params_validator' request parameter if - the params fail to validate. - """ - params_validator = self.request.get(validator_parameter) - - user_params = {} - for key in self.request.arguments(): - if key.startswith(name_prefix): - values = self.request.get_all(key) - adjusted_key = key[len(name_prefix):] - if len(values) == 1: - user_params[adjusted_key] = values[0] - else: - user_params[adjusted_key] = values - - if params_validator: - resolved_validator = util.for_name(params_validator) - resolved_validator(user_params) - - return user_params - - def _get_required_param(self, param_name): - """Get a required request parameter. - - Args: - param_name: name of request parameter to fetch. - - Returns: - parameter value - - Raises: - NotEnoughArgumentsError: if parameter is not specified. - """ - value = self.request.get(param_name) - if not value: - raise NotEnoughArgumentsError(param_name + " not specified") - return value - - @classmethod - def _start_map(cls, name, mapper_spec, - mapreduce_params, - base_path="/mapreduce", - queue_name="default", - eta=None, - countdown=None, - hooks_class_name=None, - _app=None, - transactional=False): - # Check that handler can be instantiated. - mapper_spec.get_handler() - - # Check that reader can be instantiated and is configured correctly - mapper_input_reader_class = mapper_spec.input_reader_class() - mapper_input_reader_class.validate(mapper_spec) - - mapreduce_id = model.MapreduceState.new_mapreduce_id() - mapreduce_spec = model.MapreduceSpec( - name, - mapreduce_id, - mapper_spec.to_json(), - mapreduce_params, - hooks_class_name) - - kickoff_params = {"mapreduce_spec": mapreduce_spec.to_json_str()} - if _app: - kickoff_params["app"] = _app - kickoff_worker_task = taskqueue.Task( - url=base_path + "/kickoffjob_callback", - params=kickoff_params, - eta=eta, countdown=countdown) - - hooks = mapreduce_spec.get_hooks() - - def start_mapreduce(): - if not transactional: - # Save state in datastore so that UI can see it. - # We can't save state in foreign transaction, but conventional UI - # doesn't ask for transactional starts anyway. - state = model.MapreduceState.create_new(mapreduce_spec.mapreduce_id) - state.mapreduce_spec = mapreduce_spec - state.active = True - state.active_shards = mapper_spec.shard_count - if _app: - state.app_id = _app - state.put() - - if hooks is not None: - try: - hooks.enqueue_kickoff_task(kickoff_worker_task, queue_name) - except NotImplementedError: - # Use the default task addition implementation. - pass - else: - return - kickoff_worker_task.add(queue_name, transactional=True) - - if transactional: - start_mapreduce() - else: - db.run_in_transaction(start_mapreduce) - - return mapreduce_id - - -class CleanUpJobHandler(base_handler.PostJsonHandler): - """Command to kick off tasks to clean up a job's data.""" - - def handle(self): - mapreduce_id = self.request.get("mapreduce_id") - db.delete(model.MapreduceControl.get_key_by_job_id(mapreduce_id)) - - shards = model.ShardState.find_by_mapreduce_id(mapreduce_id) - db.delete(shards) - - db.delete(model.MapreduceState.get_key_by_job_id(mapreduce_id)) - - self.json_response["status"] = ("Job %s successfully cleaned up." % - mapreduce_id) - - -class AbortJobHandler(base_handler.PostJsonHandler): - """Command to abort a running job.""" - - def handle(self): - model.MapreduceControl.abort(self.request.get("mapreduce_id")) - self.json_response["status"] = "Abort signal sent." diff --git a/mapreduce/hooks.py b/mapreduce/hooks.py deleted file mode 100755 index 7ab1123..0000000 --- a/mapreduce/hooks.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/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. - -"""API allowing control over some mapreduce implementation details.""" - - - -__all__ = ["Hooks"] - - -class Hooks(object): - """Allows subclasses to control some aspects of mapreduce execution. - - control.start_map accepts an optional "hooks" argument that can be passed a - subclass of this class. - """ - - def __init__(self, mapper): - """Initializes a Hooks class. - - Args: - mapper: The mapreduce.model.MapperSpec for the current mapreduce. - """ - self.mapper = mapper - - def enqueue_worker_task(self, task, queue_name): - """Enqueues a worker task that is used to run the mapper. - - Args: - task: A taskqueue.Task that must be queued in order for the mapreduce - mappers to be run. - queue_name: The queue where the task should be run e.g. "default". - - Raises: - NotImplementedError: to indicate that the default worker queueing strategy - should be used. - """ - raise NotImplementedError() - - def enqueue_kickoff_task(self, task, queue_name): - """Enqueues a task that is used to start the mapreduce. - - Args: - task: A taskqueue.Task that must be queued in order for the mapreduce - to start. - queue_name: The queue where the task should be run e.g. "default". - - Raises: - NotImplementedError: to indicate that the default mapreduce start strategy - should be used. - """ - raise NotImplementedError() - - def enqueue_done_task(self, task, queue_name): - """Enqueues a task that is triggered when the mapreduce completes. - - Args: - task: A taskqueue.Task that must be queued in order for the client to be - notified when the mapreduce is complete. - queue_name: The queue where the task should be run e.g. "default". - - Raises: - NotImplementedError: to indicate that the default mapreduce notification - strategy should be used. - """ - raise NotImplementedError() - - def enqueue_controller_task(self, task, queue_name): - """Enqueues a task that is used to monitor the mapreduce process. - - Args: - task: A taskqueue.Task that must be queued in order for updates to the - mapreduce process to be properly tracked. - queue_name: The queue where the task should be run e.g. "default". - - Raises: - NotImplementedError: to indicate that the default mapreduce tracking - strategy should be used. - """ - raise NotImplementedError() diff --git a/mapreduce/input_readers.py b/mapreduce/input_readers.py deleted file mode 100755 index 4c09206..0000000 --- a/mapreduce/input_readers.py +++ /dev/null @@ -1,1244 +0,0 @@ -#!/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. - -"""Defines input readers for MapReduce.""" - - - -# pylint: disable-msg=C6409 - -import logging -import math -import StringIO -import time -import zipfile - -from google.appengine.api import datastore -from google.appengine.api import namespace_manager -# TODO(user): Remove this hack once 1.4.0 is live in production. -try: - from google.appengine.datastore import datastore_rpc -except ImportError: - datastore_rpc = None -from mapreduce.lib import blobstore -from google.appengine.ext import db -from mapreduce.lib import key_range -from google.appengine.ext.db import metadata -from mapreduce import util -from mapreduce.model import JsonMixin - - -class Error(Exception): - """Base-class for exceptions in this module.""" - - -class BadReaderParamsError(Error): - """The input parameters to a reader were invalid.""" - - -class InputReader(JsonMixin): - """Abstract base class for input readers. - - InputReaders have the following properties: - * They are created by using the split_input method to generate a set of - InputReaders from a MapperSpec. - * They generate inputs to the mapper via the iterator interface. - * After creation, they can be serialized and resumed using the JsonMixin - interface. - * They are cast to string for a user-readable description; it may be - valuable to implement __str__. - """ - - # Mapreduce parameters. - _APP_PARAM = "_app" - NAMESPACES_PARAM = "namespaces" - MAPPER_PARAMS = "mapper_params" - - def __iter__(self): - return self - - def next(self): - """Returns the next input from this input reader as a key, value pair. - - Returns: - The next input from this input reader. - """ - raise NotImplementedError("next() not implemented in %s" % cls) - - @classmethod - def from_json(cls, input_shard_state): - """Creates an instance of the InputReader for the given input shard state. - - Args: - input_shard_state: The InputReader state as a dict-like object. - - Returns: - An instance of the InputReader configured using the values of json. - """ - raise NotImplementedError("from_json() not implemented in %s" % cls) - - def to_json(self): - """Returns an input shard state for the remaining inputs. - - Returns: - A json-izable version of the remaining InputReader. - """ - raise NotImplementedError("to_json() not implemented in %s" % cls) - - @classmethod - def split_input(cls, mapper_spec): - """Returns a list of input readers for the input spec. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Returns: - A list of InputReaders. - """ - raise NotImplementedError("split_input() not implemented in %s" % cls) - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - raise NotImplementedError("validate() not implemented in %s" % cls) - - -# TODO(user): Use cursor API as soon as we have it available. -class DatastoreInputReader(InputReader): - """Represents a range in query results. - - DatastoreInputReader yields model instances from the entities in a given key - range. Iterating over DatastoreInputReader changes its range past consumed - entries. - - The class shouldn't be instantiated directly. Use the split_input class method - instead. - """ - - # Number of entities to fetch at once while doing scanning. - _BATCH_SIZE = 50 - - # Maximum number of shards we'll create. - _MAX_SHARD_COUNT = 256 - - # Mapreduce parameters. - ENTITY_KIND_PARAM = "entity_kind" - KEYS_ONLY_PARAM = "keys_only" - BATCH_SIZE_PARAM = "batch_size" - KEY_RANGE_PARAM = "key_range" - - # TODO(user): Add support for arbitrary queries. It's not possible to - # support them without cursors since right now you can't even serialize query - # definition. - def __init__(self, entity_kind, key_ranges, batch_size = _BATCH_SIZE): - """Create new DatastoreInputReader object. - - This is internal constructor. Use split_query instead. - - Args: - entity_kind: entity kind as string. - key_ranges: a sequence of key_range.KeyRange instances to process. - batch_size: size of read batch as int. - """ - self._entity_kind = entity_kind - # Reverse the KeyRanges so they can be processed in order as a stack of - # work items. - self._key_ranges = list(reversed(key_ranges)) - self._batch_size = int(batch_size) - - def __iter__(self): - """Create a generator for model instances for entities. - - Iterating through entities moves query range past the consumed entities. - - Yields: - next model instance. - """ - while True: - if self._current_key_range is None: - break - - while True: - query = self._current_key_range.make_ascending_query( - util.for_name(self._entity_kind)) - results = query.fetch(limit=self._batch_size) - - if not results: - self._advance_key_range() - break - - for model_instance in results: - key = model_instance.key() - - self._current_key_range.advance(key) - yield model_instance - - @property - def _current_key_range(self): - if self._key_ranges: - return self._key_ranges[-1] - else: - return None - - def _advance_key_range(self): - if self._key_ranges: - self._key_ranges.pop() - - # TODO(user): use query splitting functionality when it becomes available - # instead. - @classmethod - def _split_input_from_namespace(cls, app, namespace, entity_kind_name, - shard_count): - """Return KeyRange objects. Helper for _split_input_from_params.""" - - raw_entity_kind = util.get_short_name(entity_kind_name) - - if shard_count == 1: - # With one shard we don't need to calculate any splitpoints at all. - return [key_range.KeyRange(namespace=namespace, _app=app)] - - # we use datastore.Query instead of ext.db.Query here, because we can't - # erase ordering on db.Query once we set it. - ds_query = datastore.Query(kind=raw_entity_kind, - namespace=namespace, - _app=app, - keys_only=True) - ds_query.Order("__key__") - first_entity_key_list = ds_query.Get(1) - if not first_entity_key_list: - logging.warning("Could not retrieve an entity of type %s.", - raw_entity_kind) - return [] - first_entity_key = first_entity_key_list[0] - ds_query.Order(("__key__", datastore.Query.DESCENDING)) - try: - last_entity_key, = ds_query.Get(1) - except db.NeedIndexError, e: - # TODO(user): Show this error in the worker log, not the app logs. - logging.warning("Cannot create accurate approximation of keyspace, " - "guessing instead. Please address this problem: %s", e) - # TODO(user): Use a key-end hint from the user input parameters - # in this case, in the event the user has a good way of figuring out - # the range of the keyspace. - last_entity_key = key_range.KeyRange.guess_end_key(raw_entity_kind, - first_entity_key) - full_keyrange = key_range.KeyRange( - first_entity_key, last_entity_key, None, True, True, - namespace=namespace, - _app=app) - key_ranges = [full_keyrange] - number_of_half_splits = int(math.floor(math.log(shard_count, 2))) - for _ in range(0, number_of_half_splits): - new_ranges = [] - for r in key_ranges: - new_ranges += r.split_range(1) - key_ranges = new_ranges - return key_ranges - - @classmethod - def _split_input_from_params(cls, app, namespaces, entity_kind_name, - params, shard_count): - """Return input reader objects. Helper for split_input.""" - key_ranges = [] # KeyRanges for all namespaces - for namespace in namespaces: - key_ranges.extend( - cls._split_input_from_namespace(app, - namespace, - entity_kind_name, - shard_count)) - - # Divide the KeyRanges into shard_count shards. The KeyRanges for different - # namespaces might be very different in size so the assignment of KeyRanges - # to shards is done round-robin. - shared_ranges = [[] for _ in range(shard_count)] - for i, k_range in enumerate(key_ranges): - shared_ranges[i % shard_count].append(k_range) - batch_size = int(params.get(cls.BATCH_SIZE_PARAM, cls._BATCH_SIZE)) - return [cls(entity_kind_name, ranges, batch_size) - for ranges in shared_ranges if ranges] - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - cls._common_validate(mapper_spec) - params = mapper_spec.params - keys_only = util.parse_bool(params.get(cls.KEYS_ONLY_PARAM, False)) - if keys_only: - raise BadReaderParamsError("The keys_only parameter is obsolete. " - "Use DatastoreKeyInputReader instead.") - - entity_kind_name = params[cls.ENTITY_KIND_PARAM] - # Fail fast if Model cannot be located. - try: - util.for_name(entity_kind_name) - except ImportError, e: - raise BadReaderParamsError("Bad entity kind: %s" % e) - - @classmethod - def _common_validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Common portion of validate method shared between DatastoreInputReader, - DatastoreKeyInputReader, and DatastoreEntityInputReader. - - Args: - cls: The class argument from the calling class method. - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - if mapper_spec.input_reader_class() != cls: - raise BadReaderParamsError("Input reader class mismatch") - params = mapper_spec.params - if cls.ENTITY_KIND_PARAM not in params: - raise BadReaderParamsError("Missing mapper parameter 'entity_kind'") - if cls.BATCH_SIZE_PARAM in params: - try: - batch_size = int(params[cls.BATCH_SIZE_PARAM]) - if batch_size < 1: - raise BadReaderParamsError("Bad batch size: %s" % batch_size) - except ValueError, e: - raise BadReaderParamsError("Bad batch size: %s" % e) - if cls.NAMESPACES_PARAM in params: - if isinstance(params[cls.NAMESPACES_PARAM], (str, unicode)): - pass - elif isinstance(params[cls.NAMESPACES_PARAM], list): - for namespace in params[cls.NAMESPACES_PARAM]: - if not isinstance(namespace, (str, unicode)): - raise BadReaderParamsError( - "Bad namespace list: expected a list of strings") - else: - raise BadReaderParamsError( - "Bad namespace list: expected a list of strings") - - @classmethod - def split_input(cls, mapper_spec): - """Splits query into shards without fetching query results. - - Tries as best as it can to split the whole query result set into equal - shards. Due to difficulty of making the perfect split, resulting shards' - sizes might differ significantly from each other. The actual number of - shards might also be less then requested (even 1), though it is never - greater. - - Current implementation does key-lexicographic order splitting. It requires - query not to specify any __key__-based ordering. If an index for - query.order('-__key__') query is not present, an inaccurate guess at - sharding will be made by splitting the full key range. - - Args: - mapper_spec: MapperSpec with params containing 'entity_kind'. - May have 'namespaces' in the params as either a list of namespace - strings or a comma-seperated list of namespaces. If specified then the - input reader will only yield entities in the given namespaces. If - 'namespaces' is not given then the current namespace will be used. May - also have 'batch_size' in the params to specify the number of entities - to process in each batch. - - Returns: - A list of InputReader objects of length <= number_of_shards. These - may be DatastoreInputReader or DatastoreKeyInputReader objects. - """ - params = mapper_spec.params - entity_kind_name = params[cls.ENTITY_KIND_PARAM] - shard_count = mapper_spec.shard_count - namespaces = params.get(cls.NAMESPACES_PARAM, - [namespace_manager.get_namespace()]) - if isinstance(namespaces, (str, unicode)): - namespaces = namespaces.split(",") - app = params.get(cls._APP_PARAM) - - return cls._split_input_from_params( - app, namespaces, entity_kind_name, params, shard_count) - - def to_json(self): - """Serializes all the data in this query range into json form. - - Returns: - all the data in json-compatible map. - """ - json_dict = {self.KEY_RANGE_PARAM: [k.to_json() for k in self._key_ranges], - self.ENTITY_KIND_PARAM: self._entity_kind, - self.BATCH_SIZE_PARAM: self._batch_size} - return json_dict - - def __str__(self): - """Returns the string representation of this DatastoreInputReader.""" - return repr(self._key_ranges) - - @classmethod - def from_json(cls, json): - """Create new DatastoreInputReader from the json, encoded by to_json. - - Args: - json: json map representation of DatastoreInputReader. - - Returns: - an instance of DatastoreInputReader with all data deserialized from json. - """ - query_range = cls( - json[cls.ENTITY_KIND_PARAM], - [key_range.KeyRange.from_json(k) for k in json[cls.KEY_RANGE_PARAM]], - json[cls.BATCH_SIZE_PARAM]) - return query_range - - -class DatastoreKeyInputReader(DatastoreInputReader): - """An input reader which takes a Kind and yields Keys for that kind.""" - - def __iter__(self): - """Create a generator for keys in the range. - - Iterating through entries moves query range past the consumed entries. - - Yields: - next entry. - """ - raw_entity_kind = util.get_short_name(self._entity_kind) - while True: - if self._current_key_range is None: - break - - while True: - query = self._current_key_range.make_ascending_datastore_query( - raw_entity_kind, keys_only=True) - results = query.Get(limit=self._batch_size) - - if not results: - self._advance_key_range() - break - - for key in results: - self._current_key_range.advance(key) - yield key - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - cls._common_validate(mapper_spec) - - -class DatastoreEntityInputReader(DatastoreInputReader): - """An input reader which yields low level datastore entities for a kind.""" - - def __iter__(self): - """Create a generator for low level entities in the range. - - Iterating through entries moves query range past the consumed entries. - - Yields: - next entry. - """ - raw_entity_kind = util.get_short_name(self._entity_kind) - while True: - if self._current_key_range is None: - break - - while True: - query = self._current_key_range.make_ascending_datastore_query( - raw_entity_kind) - results = query.Get(limit=self._batch_size) - - if not results: - self._advance_key_range() - break - - for entity in results: - self._current_key_range.advance(entity.key()) - yield entity - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - cls._common_validate(mapper_spec) - - -class BlobstoreLineInputReader(InputReader): - """Input reader for a newline delimited blob in Blobstore.""" - - # TODO(user): Should we set this based on MAX_BLOB_FETCH_SIZE? - _BLOB_BUFFER_SIZE = 64000 - - # Maximum number of shards to allow. - _MAX_SHARD_COUNT = 256 - - # Maximum number of blobs to allow. - _MAX_BLOB_KEYS_COUNT = 246 - - # Mapreduce parameters. - BLOB_KEYS_PARAM = "blob_keys" - - # Serialization parmaeters. - INITIAL_POSITION_PARAM = "initial_position" - END_POSITION_PARAM = "end_position" - BLOB_KEY_PARAM = "blob_key" - - def __init__(self, blob_key, start_position, end_position): - """Initializes this instance with the given blob key and character range. - - This BlobstoreInputReader will read from the first record starting after - strictly after start_position until the first record ending at or after - end_position (exclusive). As an exception, if start_position is 0, then - this InputReader starts reading at the first record. - - Args: - blob_key: the BlobKey that this input reader is processing. - start_position: the position to start reading at. - end_position: a position in the last record to read. - """ - self._blob_key = blob_key - self._blob_reader = blobstore.BlobReader(blob_key, - self._BLOB_BUFFER_SIZE, - start_position) - self._end_position = end_position - self._has_iterated = False - self._read_before_start = bool(start_position) - - def next(self): - """Returns the next input from as an (offset, line) tuple.""" - self._has_iterated = True - - if self._read_before_start: - self._blob_reader.readline() - self._read_before_start = False - start_position = self._blob_reader.tell() - - if start_position >= self._end_position: - raise StopIteration() - - line = self._blob_reader.readline() - - if not line: - raise StopIteration() - - return start_position, line.rstrip("\n") - - def to_json(self): - """Returns an json-compatible input shard spec for remaining inputs.""" - new_pos = self._blob_reader.tell() - if self._has_iterated: - new_pos -= 1 - return {self.BLOB_KEY_PARAM: self._blob_key, - self.INITIAL_POSITION_PARAM: new_pos, - self.END_POSITION_PARAM: self._end_position} - - def __str__(self): - """Returns the string representation of this BlobstoreLineInputReader.""" - return "blobstore.BlobKey(%r):[%d, %d]" % ( - self._blob_key, self._blob_reader.tell(), self._end_position) - - @classmethod - def from_json(cls, json): - """Instantiates an instance of this InputReader for the given shard spec.""" - return cls(json[cls.BLOB_KEY_PARAM], - json[cls.INITIAL_POSITION_PARAM], - json[cls.END_POSITION_PARAM]) - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - if mapper_spec.input_reader_class() != cls: - raise BadReaderParamsError("Mapper input reader class mismatch") - params = mapper_spec.params - if cls.BLOB_KEYS_PARAM not in params: - raise BadReaderParamsError("Must specify 'blob_keys' for mapper input") - blob_keys = params[cls.BLOB_KEYS_PARAM] - if isinstance(blob_keys, basestring): - # This is a mechanism to allow multiple blob keys (which do not contain - # commas) in a single string. It may go away. - blob_keys = blob_keys.split(",") - if len(blob_keys) > cls._MAX_BLOB_KEYS_COUNT: - raise BadReaderParamsError("Too many 'blob_keys' for mapper input") - if not blob_keys: - raise BadReaderParamsError("No 'blob_keys' specified for mapper input") - for blob_key in blob_keys: - blob_info = blobstore.BlobInfo.get(blobstore.BlobKey(blob_key)) - if not blob_info: - raise BadReaderParamsError("Could not find blobinfo for key %s" % - blob_key) - - @classmethod - def split_input(cls, mapper_spec): - """Returns a list of shard_count input_spec_shards for input_spec. - - Args: - mapper_spec: The mapper specification to split from. Must contain - 'blob_keys' parameter with one or more blob keys. - - Returns: - A list of BlobstoreInputReaders corresponding to the specified shards. - """ - params = mapper_spec.params - blob_keys = params[cls.BLOB_KEYS_PARAM] - if isinstance(blob_keys, basestring): - # This is a mechanism to allow multiple blob keys (which do not contain - # commas) in a single string. It may go away. - blob_keys = blob_keys.split(",") - - blob_sizes = {} - for blob_key in blob_keys: - blob_info = blobstore.BlobInfo.get(blobstore.BlobKey(blob_key)) - blob_sizes[blob_key] = blob_info.size - - shard_count = min(cls._MAX_SHARD_COUNT, mapper_spec.shard_count) - shards_per_blob = shard_count // len(blob_keys) - if shards_per_blob == 0: - shards_per_blob = 1 - - chunks = [] - for blob_key, blob_size in blob_sizes.items(): - blob_chunk_size = blob_size // shards_per_blob - for i in xrange(shards_per_blob - 1): - chunks.append(BlobstoreLineInputReader.from_json( - {cls.BLOB_KEY_PARAM: blob_key, - cls.INITIAL_POSITION_PARAM: blob_chunk_size * i, - cls.END_POSITION_PARAM: blob_chunk_size * (i + 1)})) - chunks.append(BlobstoreLineInputReader.from_json( - {cls.BLOB_KEY_PARAM: blob_key, - cls.INITIAL_POSITION_PARAM: blob_chunk_size * (shards_per_blob - 1), - cls.END_POSITION_PARAM: blob_size})) - return chunks - - -class BlobstoreZipInputReader(InputReader): - """Input reader for files from a zip archive stored in the Blobstore. - - Each instance of the reader will read the TOC, from the end of the zip file, - and then only the contained files which it is responsible for. - """ - - # Maximum number of shards to allow. - _MAX_SHARD_COUNT = 256 - - # Mapreduce parameters. - BLOB_KEY_PARAM = "blob_key" - START_INDEX_PARAM = "start_index" - END_INDEX_PARAM = "end_index" - - def __init__(self, blob_key, start_index, end_index, - _reader=blobstore.BlobReader): - """Initializes this instance with the given blob key and file range. - - This BlobstoreZipInputReader will read from the file with index start_index - up to but not including the file with index end_index. - - Args: - blob_key: the BlobKey that this input reader is processing. - start_index: the index of the first file to read. - end_index: the index of the first file that will not be read. - _reader: a callable that returns a file-like object for reading blobs. - Used for dependency injection. - """ - self._blob_key = blob_key - self._start_index = start_index - self._end_index = end_index - self._reader = _reader - self._zip = None - self._entries = None - - def next(self): - """Returns the next input from this input reader as (ZipInfo, opener) tuple. - - Returns: - The next input from this input reader, in the form of a 2-tuple. - The first element of the tuple is a zipfile.ZipInfo object. - The second element of the tuple is a zero-argument function that, when - called, returns the complete body of the file. - """ - if not self._zip: - self._zip = zipfile.ZipFile(self._reader(self._blob_key)) - # Get a list of entries, reversed so we can pop entries off in order - self._entries = self._zip.infolist()[self._start_index:self._end_index] - self._entries.reverse() - if not self._entries: - raise StopIteration() - entry = self._entries.pop() - self._start_index += 1 - return (entry, lambda: self._zip.read(entry.filename)) - - @classmethod - def from_json(cls, json): - """Creates an instance of the InputReader for the given input shard state. - - Args: - json: The InputReader state as a dict-like object. - - Returns: - An instance of the InputReader configured using the values of json. - """ - return cls(json[cls.BLOB_KEY_PARAM], - json[cls.START_INDEX_PARAM], - json[cls.END_INDEX_PARAM]) - - def to_json(self): - """Returns an input shard state for the remaining inputs. - - Returns: - A json-izable version of the remaining InputReader. - """ - return {self.BLOB_KEY_PARAM: self._blob_key, - self.START_INDEX_PARAM: self._start_index, - self.END_INDEX_PARAM: self._end_index} - - def __str__(self): - """Returns the string representation of this BlobstoreZipInputReader.""" - return "blobstore.BlobKey(%r):[%d, %d]" % ( - self._blob_key, self._start_index, self._end_index) - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - if mapper_spec.input_reader_class() != cls: - raise BadReaderParamsError("Mapper input reader class mismatch") - params = mapper_spec.params - if cls.BLOB_KEY_PARAM not in params: - raise BadReaderParamsError("Must specify 'blob_key' for mapper input") - blob_key = params[cls.BLOB_KEY_PARAM] - blob_info = blobstore.BlobInfo.get(blobstore.BlobKey(blob_key)) - if not blob_info: - raise BadReaderParamsError("Could not find blobinfo for key %s" % - blob_key) - - - @classmethod - def split_input(cls, mapper_spec, _reader=blobstore.BlobReader): - """Returns a list of input shard states for the input spec. - - Args: - mapper_spec: The MapperSpec for this InputReader. Must contain - 'blob_key' parameter with one blob key. - _reader: a callable that returns a file-like object for reading blobs. - Used for dependency injection. - - Returns: - A list of InputReaders spanning files within the zip. - """ - params = mapper_spec.params - blob_key = params[cls.BLOB_KEY_PARAM] - zip_input = zipfile.ZipFile(_reader(blob_key)) - files = zip_input.infolist() - total_size = sum(x.file_size for x in files) - num_shards = min(mapper_spec.shard_count, cls._MAX_SHARD_COUNT) - size_per_shard = total_size // num_shards - - # Break the list of files into sublists, each of approximately - # size_per_shard bytes. - shard_start_indexes = [0] - current_shard_size = 0 - for i, fileinfo in enumerate(files): - current_shard_size += fileinfo.file_size - if current_shard_size >= size_per_shard: - shard_start_indexes.append(i + 1) - current_shard_size = 0 - - if shard_start_indexes[-1] != len(files): - shard_start_indexes.append(len(files)) - - return [cls(blob_key, start_index, end_index, _reader) - for start_index, end_index - in zip(shard_start_indexes, shard_start_indexes[1:])] - - -class BlobstoreZipLineInputReader(InputReader): - """Input reader for newline delimited files in zip archives from Blobstore. - - This has the same external interface as the BlobstoreLineInputReader, in that - it takes a list of blobs as its input and yields lines to the reader. - However the blobs themselves are expected to be zip archives of line delimited - files instead of the files themselves. - - This is useful as many line delimited files gain greatly from compression. - """ - - # Maximum number of shards to allow. - _MAX_SHARD_COUNT = 256 - - # Maximum number of blobs to allow. - _MAX_BLOB_KEYS_COUNT = 246 - - # Mapreduce parameters. - BLOB_KEYS_PARAM = "blob_keys" - - # Serialization parameters. - BLOB_KEY_PARAM = "blob_key" - START_FILE_INDEX_PARAM = "start_file_index" - END_FILE_INDEX_PARAM = "end_file_index" - OFFSET_PARAM = "offset" - - def __init__(self, blob_key, start_file_index, end_file_index, offset, - _reader=blobstore.BlobReader): - """Initializes this instance with the given blob key and file range. - - This BlobstoreZipLineInputReader will read from the file with index - start_file_index up to but not including the file with index end_file_index. - It will return lines starting at offset within file[start_file_index] - - Args: - blob_key: the BlobKey that this input reader is processing. - start_file_index: the index of the first file to read within the zip. - end_file_index: the index of the first file that will not be read. - offset: the byte offset within blob_key.zip[start_file_index] to start - reading. The reader will continue to the end of the file. - _reader: a callable that returns a file-like object for reading blobs. - Used for dependency injection. - """ - self._blob_key = blob_key - self._start_file_index = start_file_index - self._end_file_index = end_file_index - self._initial_offset = offset - self._reader = _reader - self._zip = None - self._entries = None - self._filestream = None - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec and all mapper parameters. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - if mapper_spec.input_reader_class() != cls: - raise BadReaderParamsError("Mapper input reader class mismatch") - params = mapper_spec.params - if cls.BLOB_KEYS_PARAM not in params: - raise BadReaderParamsError("Must specify 'blob_key' for mapper input") - - blob_keys = params[cls.BLOB_KEYS_PARAM] - if isinstance(blob_keys, basestring): - # This is a mechanism to allow multiple blob keys (which do not contain - # commas) in a single string. It may go away. - blob_keys = blob_keys.split(",") - if len(blob_keys) > cls._MAX_BLOB_KEYS_COUNT: - raise BadReaderParamsError("Too many 'blob_keys' for mapper input") - if not blob_keys: - raise BadReaderParamsError("No 'blob_keys' specified for mapper input") - for blob_key in blob_keys: - blob_info = blobstore.BlobInfo.get(blobstore.BlobKey(blob_key)) - if not blob_info: - raise BadReaderParamsError("Could not find blobinfo for key %s" % - blob_key) - - @classmethod - def split_input(cls, mapper_spec, _reader=blobstore.BlobReader): - """Returns a list of input readers for the input spec. - - Args: - mapper_spec: The MapperSpec for this InputReader. Must contain - 'blob_keys' parameter with one or more blob keys. - _reader: a callable that returns a file-like object for reading blobs. - Used for dependency injection. - - Returns: - A list of InputReaders spanning the subfiles within the blobs. - There will be at least one reader per blob, but it will otherwise - attempt to keep the expanded size even. - """ - params = mapper_spec.params - blob_keys = params[cls.BLOB_KEYS_PARAM] - if isinstance(blob_keys, basestring): - # This is a mechanism to allow multiple blob keys (which do not contain - # commas) in a single string. It may go away. - blob_keys = blob_keys.split(",") - - blob_files = {} - total_size = 0 - for blob_key in blob_keys: - zip_input = zipfile.ZipFile(_reader(blob_key)) - blob_files[blob_key] = zip_input.infolist() - total_size += sum(x.file_size for x in blob_files[blob_key]) - - shard_count = min(cls._MAX_SHARD_COUNT, mapper_spec.shard_count) - - # We can break on both blob key and file-within-zip boundaries. - # A shard will span at minimum a single blob key, but may only - # handle a few files within a blob. - - size_per_shard = total_size // shard_count - - readers = [] - for blob_key in blob_keys: - files = blob_files[blob_key] - current_shard_size = 0 - start_file_index = 0 - next_file_index = 0 - for fileinfo in files: - next_file_index += 1 - current_shard_size += fileinfo.file_size - if current_shard_size >= size_per_shard: - readers.append(cls(blob_key, start_file_index, next_file_index, 0, - _reader)) - current_shard_size = 0 - start_file_index = next_file_index - if current_shard_size != 0: - readers.append(cls(blob_key, start_file_index, next_file_index, 0, - _reader)) - - return readers - - def next(self): - """Returns the next line from this input reader as (lineinfo, line) tuple. - - Returns: - The next input from this input reader, in the form of a 2-tuple. - The first element of the tuple describes the source, it is itself - a tuple (blobkey, filenumber, byteoffset). - The second element of the tuple is the line found at that offset. - """ - if not self._filestream: - if not self._zip: - self._zip = zipfile.ZipFile(self._reader(self._blob_key)) - # Get a list of entries, reversed so we can pop entries off in order - self._entries = self._zip.infolist()[self._start_file_index: - self._end_file_index] - self._entries.reverse() - if not self._entries: - raise StopIteration() - entry = self._entries.pop() - value = self._zip.read(entry.filename) - self._filestream = StringIO.StringIO(value) - if self._initial_offset: - self._filestream.seek(self._initial_offset) - self._filestream.readline() - - start_position = self._filestream.tell() - line = self._filestream.readline() - - if not line: - # Done with this file in the zip. Move on to the next file. - self._filestream.close() - self._filestream = None - self._start_file_index += 1 - self._initial_offset = 0 - return self.next() - - return ((self._blob_key, self._start_file_index, start_position), - line.rstrip("\n")) - - def _next_offset(self): - """Return the offset of the next line to read.""" - if self._filestream: - offset = self._filestream.tell() - if offset: - offset -= 1 - else: - offset = self._initial_offset - - return offset - - def to_json(self): - """Returns an input shard state for the remaining inputs. - - Returns: - A json-izable version of the remaining InputReader. - """ - - return {self.BLOB_KEY_PARAM: self._blob_key, - self.START_FILE_INDEX_PARAM: self._start_file_index, - self.END_FILE_INDEX_PARAM: self._end_file_index, - self.OFFSET_PARAM: self._next_offset()} - - @classmethod - def from_json(cls, json, _reader=blobstore.BlobReader): - """Creates an instance of the InputReader for the given input shard state. - - Args: - json: The InputReader state as a dict-like object. - _reader: For dependency injection. - - Returns: - An instance of the InputReader configured using the values of json. - """ - return cls(json[cls.BLOB_KEY_PARAM], - json[cls.START_FILE_INDEX_PARAM], - json[cls.END_FILE_INDEX_PARAM], - json[cls.OFFSET_PARAM], - _reader) - - def __str__(self): - """Returns the string representation of this reader. - - Returns: - string blobkey:[start file num, end file num]:current offset. - """ - return "blobstore.BlobKey(%r):[%d, %d]:%d" % ( - self._blob_key, self._start_file_index, self._end_file_index, - self._next_offset()) - - -class ConsistentKeyReader(DatastoreKeyInputReader): - """A key reader which reads consistent data from datastore. - - Datastore might have entities which were written, but not visible through - queries for some time. Typically these entities can be only read inside - transaction until they are 'applied'. - - This reader reads all keys even if they are not visible. It might take - significant time to start yielding some data because it has to apply all - modifications created before its start. - """ - START_TIME_US_PARAM = 'start_time_us' - UNAPPLIED_LOG_FILTER = '__unapplied_log_timestamp_us__ <' - DUMMY_KIND = 'DUMMY_KIND' - DUMMY_ID = 106275677020293L - - def __init__(self, - entity_kind, - key_range_param, - batch_size=DatastoreKeyInputReader._BATCH_SIZE, - start_time_us=None): - """Constructor. - - Args: - entity_kind: Kind of entity to read as string. - key_range_param: Key range to scan through as key_range.KeyRange. - batch_size: Size of single batch read (number of entities). - start_time_us: Start time of the reader (as given by time.time() - function). It will apply all unapplied jobs created before it was - started. - """ - DatastoreInputReader.__init__( - self, entity_kind, key_range_param, batch_size) - self.start_time_us = start_time_us - - def __iter__(self): - """Iterates over the keys in the given KeyRanges. - - Yields: - A db.Key instance for each key in the given key range, starting with - keys for unapplied jobs. - """ - while True: # Iterates over each key range. - if self._current_key_range is None: - break - - # TODO(user): Remove this hack once 1.4.0 is live in production. - if datastore_rpc: - self._apply_jobs() - - while True: # Iterates over each key in the current key range. - # Fetches the next batch of the result keys. - query = self._current_key_range.make_ascending_datastore_query( - kind=self._entity_kind, keys_only=True) - keys = query.Get(limit=self._batch_size) - - # No results, this shard is complete. - if not keys: - self._advance_key_range() - break - - # All good, now we can feed the mapper. - for key in keys: - self._current_key_range.advance(key) - yield key - - def _apply_jobs(self): - """Apply all jobs in current key range.""" - while True: - # Creates an unapplied query and fetches unapplied jobs in the result - # range. - unapplied_query = self._current_key_range.make_ascending_datastore_query( - kind=None, keys_only=True) - unapplied_query[ - ConsistentKeyReader.UNAPPLIED_LOG_FILTER] = self.start_time_us - unapplied_jobs = unapplied_query.Get(limit=self._batch_size) - - if not unapplied_jobs: - return - - # There were some unapplied jobs. Roll them forward. - keys_to_apply = [] - for key in unapplied_jobs: - # To apply the entity group we need to read something from it. - # We use dummy kind and id because we don't actually need any data. - path = key.to_path() + [ConsistentKeyReader.DUMMY_KIND, - ConsistentKeyReader.DUMMY_ID] - keys_to_apply.append( - db.Key.from_path(_app=key.app(), namespace=key.namespace(), *path)) - db.get(keys_to_apply, config=datastore_rpc.Configuration( - deadline=10, - read_policy=datastore_rpc.Configuration.APPLY_ALL_JOBS_CONSISTENCY)) - - - @classmethod - def _split_input_from_namespace(cls, - app, - namespace, - entity_kind_name, - shard_count): - key_ranges = super(ConsistentKeyReader, cls)._split_input_from_namespace( - app, namespace, entity_kind_name, shard_count) - - # The KeyRanges calculated by the base class may not include keys for - # entities that have unapplied jobs. So use an open key range for the first - # and last KeyRanges to ensure that they will be processed. - if key_ranges: - key_ranges[0].key_start = None - key_ranges[0].include_start = False - key_ranges[-1].key_end = None - key_ranges[-1].include_end = False - return key_ranges - - @classmethod - def _split_input_from_params(cls, app, namespaces, entity_kind_name, - params, shard_count): - readers = super(ConsistentKeyReader, cls)._split_input_from_params(app, - namespaces, - entity_kind_name, - params, - shard_count) - - # We always produce at least one key range because: - # a) there might be unapplied entities - # b) it simplifies mapper code - if not readers: - key_ranges = [key_range.KeyRange(namespace=namespace, _app=app) - for namespace in namespaces] - readers = [cls(entity_kind_name, key_ranges)] - - return readers - - @classmethod - def split_input(cls, mapper_spec): - """Splits input into key ranges.""" - readers = super(ConsistentKeyReader, cls).split_input(mapper_spec) - - start_time_us = mapper_spec.params.get( - cls.START_TIME_US_PARAM, long(time.time() * 1e6)) - for reader in readers: - reader.start_time_us = start_time_us - return readers - - def to_json(self): - """Serializes all the data in this reader into json form. - - Returns: - all the data in json-compatible map. - """ - json_dict = {self.KEY_RANGE_PARAM: [k.to_json() for k in self._key_ranges], - self.ENTITY_KIND_PARAM: self._entity_kind, - self.BATCH_SIZE_PARAM: self._batch_size, - self.START_TIME_US_PARAM: self.start_time_us} - return json_dict - - @classmethod - def from_json(cls, json): - """Create new ConsistentKeyReader from the json, encoded by to_json. - - Args: - json: json map representation of ConsistentKeyReader. - - Returns: - an instance of ConsistentKeyReader with all data deserialized from json. - """ - query_range = cls( - json[cls.ENTITY_KIND_PARAM], - [key_range.KeyRange.from_json(k) for k in json[cls.KEY_RANGE_PARAM]], - json[cls.BATCH_SIZE_PARAM], - json[cls.START_TIME_US_PARAM]) - return query_range - - -# TODO(user): This reader always produces only one shard, because -# namespace entities use the mix of ids/names, and KeyRange-based splitting -# doesn't work satisfactory in this case. -# It's possible to implement specific splitting functionality for the reader -# instead of reusing generic one. Meanwhile 1 shard is enough for our -# applications. -class NamespaceInputReader(DatastoreKeyInputReader): - """An input reader to iterate over namespaces. - - This reader yields namespace names as string. - It will always produce only one shard. - """ - - @classmethod - def validate(cls, mapper_spec): - """Validates mapper spec. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Raises: - BadReaderParamsError: required parameters are missing or invalid. - """ - mapper_spec.params[cls.ENTITY_KIND_PARAM] = metadata.Namespace.kind() - mapper_spec.shard_count = 1 - cls._common_validate(mapper_spec) - - @classmethod - def split_input(cls, mapper_spec): - """Returns a list of input readers for the input spec. - - Args: - mapper_spec: The MapperSpec for this InputReader. - - Returns: - A list of InputReaders. - """ - mapper_spec.params[cls.ENTITY_KIND_PARAM] = metadata.Namespace.kind() - mapper_spec.shard_count = 1 - return super(DatastoreKeyInputReader, cls).split_input(mapper_spec) - - def __iter__(self): - for key in DatastoreKeyInputReader.__iter__(self): - yield metadata.Namespace.key_to_namespace(key) diff --git a/mapreduce/lib/__init__.py b/mapreduce/lib/__init__.py deleted file mode 100755 index 6c49c42..0000000 --- a/mapreduce/lib/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/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. diff --git a/mapreduce/lib/blobstore/__init__.py b/mapreduce/lib/blobstore/__init__.py deleted file mode 100755 index 769e2de..0000000 --- a/mapreduce/lib/blobstore/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2007 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. -# - - - - -"""Blobstore API module.""" - -from blobstore import * diff --git a/mapreduce/lib/blobstore/blobstore.py b/mapreduce/lib/blobstore/blobstore.py deleted file mode 100755 index 3b7184c..0000000 --- a/mapreduce/lib/blobstore/blobstore.py +++ /dev/null @@ -1,745 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2007 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. -# - - - - -"""A Python blobstore API used by app developers. - -Contains methods used to interface with Blobstore API. Includes db.Model-like -class representing a reference to a very large BLOB. Imports db.Key-like -class representing a blob-key. -""" - - - - - - - -import cgi -import email -import os - -from google.appengine.api import datastore -from google.appengine.api import datastore_errors -from google.appengine.api import datastore_types -from google.appengine.api.blobstore import blobstore -from google.appengine.ext import db - -__all__ = ['BLOB_INFO_KIND', - 'BLOB_KEY_HEADER', - 'BLOB_RANGE_HEADER', - 'BlobFetchSizeTooLargeError', - 'BlobInfo', - 'BlobInfoParseError', - 'BlobKey', - 'BlobNotFoundError', - 'BlobReferenceProperty', - 'BlobReader', - 'DataIndexOutOfRangeError', - 'Error', - 'InternalError', - 'MAX_BLOB_FETCH_SIZE', - 'UPLOAD_INFO_CREATION_HEADER', - 'create_upload_url', - 'delete', - 'fetch_data', - 'get', - 'parse_blob_info'] - -Error = blobstore.Error -InternalError = blobstore.InternalError -BlobFetchSizeTooLargeError = blobstore.BlobFetchSizeTooLargeError -BlobNotFoundError = blobstore.BlobNotFoundError -_CreationFormatError = blobstore._CreationFormatError -DataIndexOutOfRangeError = blobstore.DataIndexOutOfRangeError - -BlobKey = blobstore.BlobKey -create_upload_url = blobstore.create_upload_url -delete = blobstore.delete - - -class BlobInfoParseError(Error): - """CGI parameter does not contain valid BlobInfo record.""" - - -BLOB_INFO_KIND = blobstore.BLOB_INFO_KIND -BLOB_KEY_HEADER = blobstore.BLOB_KEY_HEADER -BLOB_RANGE_HEADER = blobstore.BLOB_RANGE_HEADER -MAX_BLOB_FETCH_SIZE = blobstore.MAX_BLOB_FETCH_SIZE -UPLOAD_INFO_CREATION_HEADER = blobstore.UPLOAD_INFO_CREATION_HEADER - - - -class _GqlQuery(db.GqlQuery): - """GqlQuery class that explicitly sets model-class. - - This does the same as the original db.GqlQuery class except that it does - not try to find the model class based on the compiled GQL query. The - caller instead provides the query with a model class to use for construction. - - This class is required for compatibility with the current db.py query - mechanism but will be removed in the future. DO NOT USE. - """ - - - def __init__(self, query_string, model_class, *args, **kwds): - """Constructor. - - Args: - query_string: Properly formatted GQL query string. - model_class: Model class from which entities are constructed. - *args: Positional arguments used to bind numeric references in the query. - **kwds: Dictionary-based arguments for named references. - """ - - - from google.appengine.ext import gql - app = kwds.pop('_app', None) - self._proto_query = gql.GQL(query_string, _app=app, namespace='') - - super(db.GqlQuery, self).__init__(model_class, namespace='') - self.bind(*args, **kwds) - - - - -class BlobInfo(object): - """Information about blobs in Blobstore. - - This is a db.Model-like class that contains information about blobs stored - by an application. Like db.Model, this class is backed by an Datastore - entity, however, BlobInfo instances are read-only and have a much more - limited interface. - - Each BlobInfo has a key of type BlobKey associated with it. This key is - specific to the Blobstore API and is not compatible with db.get. The key - can be used for quick lookup by passing it to BlobInfo.get. This - key converts easily to a string, which is web safe and can be embedded - in URLs. - - Properties: - content_type: Content type of blob. - creation: Creation date of blob, when it was uploaded. - filename: Filename user selected from their machine. - size: Size of uncompressed blob. - - All properties are read-only. Attempting to assign a value to a property - will raise NotImplementedError. - """ - - _unindexed_properties = frozenset() - - @property - def content_type(self): - return self.__get_value('content_type') - - @property - def creation(self): - return self.__get_value('creation') - - @property - def filename(self): - return self.__get_value('filename') - - @property - def size(self): - return self.__get_value('size') - - def __init__(self, entity_or_blob_key, _values=None): - """Constructor for wrapping blobstore entity. - - The constructor should not be used outside this package and tests. - - Args: - entity: Datastore entity that represents the blob reference. - """ - if isinstance(entity_or_blob_key, datastore.Entity): - self.__entity = entity_or_blob_key - self.__key = BlobKey(entity_or_blob_key.key().name()) - elif isinstance(entity_or_blob_key, BlobKey): - self.__entity = _values - self.__key = entity_or_blob_key - else: - TypeError('Must provide Entity or BlobKey') - - - - @classmethod - def from_entity(cls, entity): - """Convert entity to BlobInfo. - - This method is required for compatibility with the current db.py query - mechanism but will be removed in the future. DO NOT USE. - """ - return BlobInfo(entity) - - - - @classmethod - def properties(cls): - """Set of properties that belong to BlobInfo. - - This method is required for compatibility with the current db.py query - mechanism but will be removed in the future. DO NOT USE. - """ - return set(('content_type', 'creation', 'filename', 'size')) - - def __get_value(self, name): - """Get a BlobInfo value, loading entity if necessary. - - This method allows lazy loading of the underlying datastore entity. It - should never be invoked directly. - - Args: - name: Name of property to get value for. - - Returns: - Value of BlobInfo property from entity. - """ - if self.__entity is None: - self.__entity = datastore.Get( - datastore_types.Key.from_path( - self.kind(), str(self.__key), namespace='')) - try: - return self.__entity[name] - except KeyError: - raise AttributeError(name) - - - def key(self): - """Get key for blob. - - Returns: - BlobKey instance that identifies this blob. - """ - return self.__key - - def delete(self): - """Permanently delete blob from Blobstore.""" - delete(self.key()) - - def open(self, *args, **kwargs): - """Returns a BlobReader for this blob. - - Args: - *args, **kwargs: Passed to BlobReader constructor. - Returns: - A BlobReader instance. - """ - return BlobReader(self, *args, **kwargs) - - @classmethod - def get(cls, blob_keys): - """Retrieve BlobInfo by key or list of keys. - - Args: - blob_keys: A key or a list of keys. Keys may be instances of str, - unicode and BlobKey. - - Returns: - A BlobInfo instance associated with provided key or a list of BlobInfo - instances if a list of keys was provided. Keys that are not found in - Blobstore return None as their values. - """ - blob_keys = cls.__normalize_and_convert_keys(blob_keys) - try: - entities = datastore.Get(blob_keys) - except datastore_errors.EntityNotFoundError: - return None - if isinstance(entities, datastore.Entity): - return BlobInfo(entities) - else: - references = [] - for entity in entities: - if entity is not None: - references.append(BlobInfo(entity)) - else: - references.append(None) - return references - - @classmethod - def all(cls): - """Get query for all Blobs associated with application. - - Returns: - A db.Query object querying over BlobInfo's datastore kind. - """ - return db.Query(model_class=cls, namespace='') - - @classmethod - def __factory_for_kind(cls, kind): - if kind == BLOB_INFO_KIND: - return BlobInfo - raise ValueError('Cannot query for kind %s' % kind) - - @classmethod - def gql(cls, query_string, *args, **kwds): - """Returns a query using GQL query string. - - See appengine/ext/gql for more information about GQL. - - Args: - query_string: Properly formatted GQL query string with the - 'SELECT * FROM ' part omitted - *args: rest of the positional arguments used to bind numeric references - in the query. - **kwds: dictionary-based arguments (for named parameters). - - Returns: - A gql.GqlQuery object querying over BlobInfo's datastore kind. - """ - return _GqlQuery('SELECT * FROM %s %s' - % (cls.kind(), query_string), - cls, - *args, - **kwds) - - - @classmethod - def kind(self): - """Get the entity kind for the BlobInfo. - - This method is required for compatibility with the current db.py query - mechanism but will be removed in the future. DO NOT USE. - """ - return BLOB_INFO_KIND - - @classmethod - def __normalize_and_convert_keys(cls, keys): - """Normalize and convert all keys to BlobKey type. - - This method is based on datastore.NormalizeAndTypeCheck(). - - Args: - keys: A single key or a list/tuple of keys. Keys may be a string - or BlobKey - - Returns: - Single key or list with all strings replaced by BlobKey instances. - """ - if isinstance(keys, (list, tuple)): - multiple = True - - keys = list(keys) - else: - multiple = False - keys = [keys] - - for index, key in enumerate(keys): - if not isinstance(key, (basestring, BlobKey)): - raise datastore_errors.BadArgumentError( - 'Expected str or BlobKey; received %s (a %s)' % ( - key, - datastore.typename(key))) - keys[index] = datastore.Key.from_path(cls.kind(), str(key), namespace='') - - if multiple: - return keys - else: - return keys[0] - - -def get(blob_key): - """Get a BlobInfo record from blobstore. - - Does the same as BlobInfo.get. - """ - return BlobInfo.get(blob_key) - - -def parse_blob_info(field_storage): - """Parse a BlobInfo record from file upload field_storage. - - Args: - field_storage: cgi.FieldStorage that represents uploaded blob. - - Returns: - BlobInfo record as parsed from the field-storage instance. - None if there was no field_storage. - - Raises: - BlobInfoParseError when provided field_storage does not contain enough - information to construct a BlobInfo object. - """ - if field_storage is None: - return None - - field_name = field_storage.name - - def get_value(dict, name): - value = dict.get(name, None) - if value is None: - raise BlobInfoParseError( - 'Field %s has no %s.' % (field_name, name)) - return value - - filename = get_value(field_storage.disposition_options, 'filename') - blob_key = BlobKey(get_value(field_storage.type_options, 'blob-key')) - - upload_content = email.message_from_file(field_storage.file) - content_type = get_value(upload_content, 'content-type') - size = get_value(upload_content, 'content-length') - creation_string = get_value(upload_content, UPLOAD_INFO_CREATION_HEADER) - - try: - size = int(size) - except (TypeError, ValueError): - raise BlobInfoParseError( - '%s is not a valid value for %s size.' % (size, field_name)) - - try: - creation = blobstore._parse_creation(creation_string, field_name) - except blobstore._CreationFormatError, err: - raise BlobInfoParseError(str(err)) - - return BlobInfo(blob_key, - {'content_type': content_type, - 'creation': creation, - 'filename': filename, - 'size': size, - }) - - -class BlobReferenceProperty(db.Property): - """Property compatible with db.Model classes. - - Add references to blobs to domain models using BlobReferenceProperty: - - class Picture(db.Model): - title = db.StringProperty() - image = blobstore.BlobReferenceProperty() - thumbnail = blobstore.BlobReferenceProperty() - - To find the size of a picture using this model: - - picture = Picture.get(picture_key) - print picture.image.size - - BlobInfo objects are lazily loaded so iterating over models with - for BlobKeys is efficient, the following does not need to hit - Datastore for each image key: - - list_of_untitled_blobs = [] - for picture in Picture.gql("WHERE title=''"): - list_of_untitled_blobs.append(picture.image.key()) - """ - - data_type = BlobInfo - - def get_value_for_datastore(self, model_instance): - """Translate model property to datastore value.""" - blob_info = getattr(model_instance, self.name) - if blob_info is None: - return None - return blob_info.key() - - def make_value_from_datastore(self, value): - """Translate datastore value to BlobInfo.""" - if value is None: - return None - return BlobInfo(value) - - def validate(self, value): - """Validate that assigned value is BlobInfo. - - Automatically converts from strings and BlobKey instances. - """ - if isinstance(value, (basestring)): - value = BlobInfo(BlobKey(value)) - elif isinstance(value, BlobKey): - value = BlobInfo(value) - return super(BlobReferenceProperty, self).validate(value) - - -def fetch_data(blob, start_index, end_index): - """Fetch data for blob. - - Fetches a fragment of a blob up to MAX_BLOB_FETCH_SIZE in length. Attempting - to fetch a fragment that extends beyond the boundaries of the blob will return - the amount of data from start_index until the end of the blob, which will be - a smaller size than requested. Requesting a fragment which is entirely - outside the boundaries of the blob will return empty string. Attempting - to fetch a negative index will raise an exception. - - Args: - blob: BlobInfo, BlobKey, str or unicode representation of BlobKey of - blob to fetch data from. - start_index: Start index of blob data to fetch. May not be negative. - end_index: End index (inclusive) of blob data to fetch. Must be - >= start_index. - - Returns: - str containing partial data of blob. If the indexes are legal but outside - the boundaries of the blob, will return empty string. - - Raises: - TypeError if start_index or end_index are not indexes. Also when blob - is not a string, BlobKey or BlobInfo. - DataIndexOutOfRangeError when start_index < 0 or end_index < start_index. - BlobFetchSizeTooLargeError when request blob fragment is larger than - MAX_BLOB_FETCH_SIZE. - BlobNotFoundError when blob does not exist. - """ - if isinstance(blob, BlobInfo): - blob = blob.key() - return blobstore.fetch_data(blob, start_index, end_index) - - -class BlobReader(object): - """Provides a read-only file-like interface to a blobstore blob.""" - - SEEK_SET = 0 - SEEK_CUR = 1 - SEEK_END = 2 - - def __init__(self, blob, buffer_size=131072, position=0): - """Constructor. - - Args: - blob: The blob key, blob info, or string blob key to read from. - buffer_size: The minimum size to fetch chunks of data from blobstore. - position: The initial position in the file. - """ - if hasattr(blob, 'key'): - self.__blob_key = blob.key() - self.__blob_info = blob - else: - self.__blob_key = blob - self.__blob_info = None - self.__buffer_size = buffer_size - self.__buffer = "" - self.__position = position - self.__buffer_position = 0 - self.__eof = False - - def __iter__(self): - """Returns a file iterator for this BlobReader.""" - return self - - def __getstate__(self): - """Returns the serialized state for this BlobReader.""" - return (self.__blob_key, self.__buffer_size, self.__position) - - def __setstate__(self, state): - """Restores pickled state for this BlobReader.""" - self.__init__(*state) - - def close(self): - """Close the file. - - A closed file cannot be read or written any more. Any operation which - requires that the file be open will raise a ValueError after the file has - been closed. Calling close() more than once is allowed. - """ - self.__blob_key = None - - def flush(self): - raise IOError("BlobReaders are read-only") - - def next(self): - """Returns the next line from the file. - - Returns: - A string, terminted by \n. The last line may not be terminated by \n. - If EOF is reached, an empty string will be returned. - """ - line = self.readline() - if not line: - raise StopIteration - return line - - def __read_from_buffer(self, size): - """Reads at most size bytes from the buffer. - - Args: - size: Number of bytes to read, or negative to read the entire buffer. - Returns: - Tuple (data, size): - data: The bytes read from the buffer. - size: The remaining unread byte count. - """ - - if not self.__blob_key: - raise ValueError("File is closed") - - if size < 0: - end_pos = len(self.__buffer) - else: - end_pos = self.__buffer_position + size - data = self.__buffer[self.__buffer_position:end_pos] - - - data_length = len(data) - size -= data_length - self.__position += data_length - self.__buffer_position += data_length - - - if self.__buffer_position == len(self.__buffer): - self.__buffer = "" - self.__buffer_position = 0 - - return data, size - - def __fill_buffer(self, size=0): - """Fills the internal buffer. - - Args: - size: Number of bytes to read. Will be clamped to - [self.__buffer_size, MAX_BLOB_FETCH_SIZE]. - """ - read_size = min(max(size, self.__buffer_size), MAX_BLOB_FETCH_SIZE) - - self.__buffer = fetch_data(self.__blob_key, self.__position, - self.__position + read_size - 1) - self.__buffer_position = 0 - self.__eof = len(self.__buffer) < read_size - - def read(self, size=-1): - """Read at most size bytes from the file. - - Fewer bytes are read if the read hits EOF before obtaining size bytes. - If the size argument is negative or omitted, read all data until EOF is - reached. The bytes are returned as a string object. An empty string is - returned when EOF is encountered immediately. - - Calling read() without a size specified is likely to be dangerous, as it - may read excessive amounts of data. - - Args: - size: Optional. The maximum number of bytes to read. When omitted, read() - returns all remaining data in the file. - - Returns: - The read data, as a string. - """ - data_list = [] - while True: - data, size = self.__read_from_buffer(size) - data_list.append(data) - if size == 0 or self.__eof: - return ''.join(data_list) - self.__fill_buffer(size) - - def readline(self, size=-1): - """Read one entire line from the file. - - A trailing newline character is kept in the string (but may be absent when a - file ends with an incomplete line). If the size argument is present and - non-negative, it is a maximum byte count (including the trailing newline) - and an incomplete line may be returned. An empty string is returned only - when EOF is encountered immediately. - - Args: - size: Optional. The maximum number of bytes to read. - - Returns: - The read data, as a string. - """ - data_list = [] - while True: - if size < 0: - end_pos = len(self.__buffer) - else: - end_pos = self.__buffer_position + size - newline_pos = self.__buffer.find('\n', self.__buffer_position, end_pos) - if newline_pos != -1: - - data_list.append( - self.__read_from_buffer(newline_pos - - self.__buffer_position + 1)[0]) - break - else: - - data, size = self.__read_from_buffer(size) - data_list.append(data) - if size == 0 or self.__eof: - break - self.__fill_buffer() - return ''.join(data_list) - - def readlines(self, sizehint=None): - """Read until EOF using readline() and return a list of lines thus read. - - If the optional sizehint argument is present, instead of reading up to EOF, - whole lines totalling approximately sizehint bytes (possibly after rounding - up to an internal buffer size) are read. - - Args: - sizehint: A hint as to the maximum number of bytes to read. - - Returns: - A list of strings, each being a single line from the file. - """ - lines = [] - while sizehint is None or sizehint > 0: - line = self.readline() - if sizehint: - sizehint -= len(line) - if not line: - - break - lines.append(line) - return lines - - def seek(self, offset, whence=SEEK_SET): - """Set the file's current position, like stdio's fseek(). - - The whence argument is optional and defaults to os.SEEK_SET or 0 (absolute - file positioning); other values are os.SEEK_CUR or 1 (seek relative to the - current position) and os.SEEK_END or 2 (seek relative to the file's end). - - Args: - offset: The relative offset to seek to. - whence: Defines what the offset is relative to. See description for - details. - """ - if whence == BlobReader.SEEK_CUR: - offset = self.__position + offset - elif whence == BlobReader.SEEK_END: - offset = self.blob_info.size + offset - self.__buffer = "" - self.__buffer_position = 0 - self.__position = offset - self.__eof = False - - def tell(self): - """Return the file's current position, like stdio's ftell().""" - return self.__position - - def truncate(self, size): - raise IOError("BlobReaders are read-only") - - def write(self, str): - raise IOError("BlobReaders are read-only") - - def writelines(self, sequence): - raise IOError("BlobReaders are read-only") - - @property - def blob_info(self): - """Returns the BlobInfo for this file.""" - if not self.__blob_info: - self.__blob_info = BlobInfo.get(self.__blob_key) - return self.__blob_info - - @property - def closed(self): - """Returns True if this file is closed, False otherwise.""" - return self.__blob_key is None diff --git a/mapreduce/lib/graphy/README b/mapreduce/lib/graphy/README deleted file mode 100755 index 39809d8..0000000 --- a/mapreduce/lib/graphy/README +++ /dev/null @@ -1,14 +0,0 @@ -Graphy library - -The web site is http://code.google.com/p/graphy/ - -This copy was downloaded from -http://graphy.googlecode.com/files/graphy_1.0.tar.bz2 - -Graphy is licensed under the Apache 2.0 open source license. - -Local changes: - -- Changed imports to make mapreduce library hermetic. - - diff --git a/mapreduce/lib/graphy/__init__.py b/mapreduce/lib/graphy/__init__.py deleted file mode 100755 index a32fb2d..0000000 --- a/mapreduce/lib/graphy/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python -__version__='1.0' diff --git a/mapreduce/lib/graphy/backends/__init__.py b/mapreduce/lib/graphy/backends/__init__.py deleted file mode 100755 index 4265cc3..0000000 --- a/mapreduce/lib/graphy/backends/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env python diff --git a/mapreduce/lib/graphy/backends/google_chart_api/__init__.py b/mapreduce/lib/graphy/backends/google_chart_api/__init__.py deleted file mode 100755 index a1b5c33..0000000 --- a/mapreduce/lib/graphy/backends/google_chart_api/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Backend which can generate charts using the Google Chart API.""" - -from mapreduce.lib.graphy import line_chart -from mapreduce.lib.graphy import bar_chart -from mapreduce.lib.graphy import pie_chart -from mapreduce.lib.graphy.backends.google_chart_api import encoders - -def _GetChartFactory(chart_class, display_class): - """Create a factory method for instantiating charts with displays. - - Returns a method which, when called, will create & return a chart with - chart.display already populated. - """ - def Inner(*args, **kwargs): - chart = chart_class(*args, **kwargs) - chart.display = display_class(chart) - return chart - return Inner - -# These helper methods make it easy to get chart objects with display -# objects already setup. For example, this: -# chart = google_chart_api.LineChart() -# is equivalent to: -# chart = line_chart.LineChart() -# chart.display = google_chart_api.LineChartEncoder() -# -# (If there's some chart type for which a helper method isn't available, you -# can always just instantiate the correct encoder manually, like in the 2nd -# example above). -# TODO: fix these so they have nice docs in ipython (give them __doc__) -LineChart = _GetChartFactory(line_chart.LineChart, encoders.LineChartEncoder) -Sparkline = _GetChartFactory(line_chart.Sparkline, encoders.SparklineEncoder) -BarChart = _GetChartFactory(bar_chart.BarChart, encoders.BarChartEncoder) -PieChart = _GetChartFactory(pie_chart.PieChart, encoders.PieChartEncoder) diff --git a/mapreduce/lib/graphy/backends/google_chart_api/encoders.py b/mapreduce/lib/graphy/backends/google_chart_api/encoders.py deleted file mode 100755 index c27376b..0000000 --- a/mapreduce/lib/graphy/backends/google_chart_api/encoders.py +++ /dev/null @@ -1,430 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Display objects for the different kinds of charts. - -Not intended for end users, use the methods in __init__ instead.""" - -import warnings -from mapreduce.lib.graphy.backends.google_chart_api import util - - -class BaseChartEncoder(object): - - """Base class for encoders which turn chart objects into Google Chart URLS. - - Object attributes: - extra_params: Dict to add/override specific chart params. Of the - form param:string, passed directly to the Google Chart API. - For example, 'cht':'lti' becomes ?cht=lti in the URL. - url_base: The prefix to use for URLs. If you want to point to a different - server for some reason, you would override this. - formatters: TODO: Need to explain how these work, and how they are - different from chart formatters. - enhanced_encoding: If True, uses enhanced encoding. If - False, simple encoding is used. - escape_url: If True, URL will be properly escaped. If False, characters - like | and , will be unescapped (which makes the URL easier to - read). - """ - - def __init__(self, chart): - self.extra_params = {} # You can add specific params here. - self.url_base = 'http://chart.apis.google.com/chart' - self.formatters = self._GetFormatters() - self.chart = chart - self.enhanced_encoding = False - self.escape_url = True # You can turn off URL escaping for debugging. - self._width = 0 # These are set when someone calls Url() - self._height = 0 - - def Url(self, width, height, use_html_entities=False): - """Get the URL for our graph. - - Args: - use_html_entities: If True, reserved HTML characters (&, <, >, ") in the - URL are replaced with HTML entities (&, <, etc.). Default is False. - """ - self._width = width - self._height = height - params = self._Params(self.chart) - return util.EncodeUrl(self.url_base, params, self.escape_url, - use_html_entities) - - def Img(self, width, height): - """Get an image tag for our graph.""" - url = self.Url(width, height, use_html_entities=True) - tag = 'chart' - return tag % (url, width, height) - - def _GetType(self, chart): - """Return the correct chart_type param for the chart.""" - raise NotImplementedError - - def _GetFormatters(self): - """Get a list of formatter functions to use for encoding.""" - formatters = [self._GetLegendParams, - self._GetDataSeriesParams, - self._GetColors, - self._GetAxisParams, - self._GetGridParams, - self._GetType, - self._GetExtraParams, - self._GetSizeParams, - ] - return formatters - - def _Params(self, chart): - """Collect all the different params we need for the URL. Collecting - all params as a dict before converting to a URL makes testing easier. - """ - chart = chart.GetFormattedChart() - params = {} - def Add(new_params): - params.update(util.ShortenParameterNames(new_params)) - - for formatter in self.formatters: - Add(formatter(chart)) - - for key in params: - params[key] = str(params[key]) - return params - - def _GetSizeParams(self, chart): - """Get the size param.""" - return {'size': '%sx%s' % (int(self._width), int(self._height))} - - def _GetExtraParams(self, chart): - """Get any extra params (from extra_params).""" - return self.extra_params - - def _GetDataSeriesParams(self, chart): - """Collect params related to the data series.""" - y_min, y_max = chart.GetDependentAxis().min, chart.GetDependentAxis().max - series_data = [] - markers = [] - for i, series in enumerate(chart.data): - data = series.data - if not data: # Drop empty series. - continue - series_data.append(data) - - for x, marker in series.markers: - args = [marker.shape, marker.color, i, x, marker.size] - markers.append(','.join(str(arg) for arg in args)) - - encoder = self._GetDataEncoder(chart) - result = util.EncodeData(chart, series_data, y_min, y_max, encoder) - result.update(util.JoinLists(marker = markers)) - return result - - def _GetColors(self, chart): - """Color series color parameter.""" - colors = [] - for series in chart.data: - if not series.data: - continue - colors.append(series.style.color) - return util.JoinLists(color = colors) - - def _GetDataEncoder(self, chart): - """Get a class which can encode the data the way the user requested.""" - if not self.enhanced_encoding: - return util.SimpleDataEncoder() - return util.EnhancedDataEncoder() - - def _GetLegendParams(self, chart): - """Get params for showing a legend.""" - if chart._show_legend: - return util.JoinLists(data_series_label = chart._legend_labels) - return {} - - def _GetAxisLabelsAndPositions(self, axis, chart): - """Return axis.labels & axis.label_positions.""" - return axis.labels, axis.label_positions - - def _GetAxisParams(self, chart): - """Collect params related to our various axes (x, y, right-hand).""" - axis_types = [] - axis_ranges = [] - axis_labels = [] - axis_label_positions = [] - axis_label_gridlines = [] - mark_length = max(self._width, self._height) - for i, axis_pair in enumerate(a for a in chart._GetAxes() if a[1].labels): - axis_type_code, axis = axis_pair - axis_types.append(axis_type_code) - if axis.min is not None or axis.max is not None: - assert axis.min is not None # Sanity check: both min & max must be set. - assert axis.max is not None - axis_ranges.append('%s,%s,%s' % (i, axis.min, axis.max)) - - labels, positions = self._GetAxisLabelsAndPositions(axis, chart) - if labels: - axis_labels.append('%s:' % i) - axis_labels.extend(labels) - if positions: - positions = [i] + list(positions) - axis_label_positions.append(','.join(str(x) for x in positions)) - if axis.label_gridlines: - axis_label_gridlines.append("%d,%d" % (i, -mark_length)) - - return util.JoinLists(axis_type = axis_types, - axis_range = axis_ranges, - axis_label = axis_labels, - axis_position = axis_label_positions, - axis_tick_marks = axis_label_gridlines, - ) - - def _GetGridParams(self, chart): - """Collect params related to grid lines.""" - x = 0 - y = 0 - if chart.bottom.grid_spacing: - # min/max must be set for this to make sense. - assert(chart.bottom.min is not None) - assert(chart.bottom.max is not None) - total = float(chart.bottom.max - chart.bottom.min) - x = 100 * chart.bottom.grid_spacing / total - if chart.left.grid_spacing: - # min/max must be set for this to make sense. - assert(chart.left.min is not None) - assert(chart.left.max is not None) - total = float(chart.left.max - chart.left.min) - y = 100 * chart.left.grid_spacing / total - if x or y: - return dict(grid = '%.3g,%.3g,1,0' % (x, y)) - return {} - - -class LineChartEncoder(BaseChartEncoder): - - """Helper class to encode LineChart objects into Google Chart URLs.""" - - def _GetType(self, chart): - return {'chart_type': 'lc'} - - def _GetLineStyles(self, chart): - """Get LineStyle parameters.""" - styles = [] - for series in chart.data: - style = series.style - if style: - styles.append('%s,%s,%s' % (style.width, style.on, style.off)) - else: - # If one style is missing, they must all be missing - # TODO: Add a test for this; throw a more meaningful exception - assert (not styles) - return util.JoinLists(line_style = styles) - - def _GetFormatters(self): - out = super(LineChartEncoder, self)._GetFormatters() - out.insert(-2, self._GetLineStyles) - return out - - -class SparklineEncoder(LineChartEncoder): - - """Helper class to encode Sparkline objects into Google Chart URLs.""" - - def _GetType(self, chart): - return {'chart_type': 'lfi'} - - -class BarChartEncoder(BaseChartEncoder): - - """Helper class to encode BarChart objects into Google Chart URLs.""" - - __STYLE_DEPRECATION = ('BarChart.display.style is deprecated.' + - ' Use BarChart.style, instead.') - - def __init__(self, chart, style=None): - """Construct a new BarChartEncoder. - - Args: - style: DEPRECATED. Set style on the chart object itself. - """ - super(BarChartEncoder, self).__init__(chart) - if style is not None: - warnings.warn(self.__STYLE_DEPRECATION, DeprecationWarning, stacklevel=2) - chart.style = style - - def _GetType(self, chart): - # Vertical Stacked Type - types = {(True, False): 'bvg', - (True, True): 'bvs', - (False, False): 'bhg', - (False, True): 'bhs'} - return {'chart_type': types[(chart.vertical, chart.stacked)]} - - def _GetAxisLabelsAndPositions(self, axis, chart): - """Reverse labels on the y-axis in horizontal bar charts. - (Otherwise the labels come out backwards from what you would expect) - """ - if not chart.vertical and axis == chart.left: - # The left axis of horizontal bar charts needs to have reversed labels - return reversed(axis.labels), reversed(axis.label_positions) - return axis.labels, axis.label_positions - - def _GetFormatters(self): - out = super(BarChartEncoder, self)._GetFormatters() - # insert at -2 to allow extra_params to overwrite everything - out.insert(-2, self._ZeroPoint) - out.insert(-2, self._ApplyBarChartStyle) - return out - - def _ZeroPoint(self, chart): - """Get the zero-point if any bars are negative.""" - # (Maybe) set the zero point. - min, max = chart.GetDependentAxis().min, chart.GetDependentAxis().max - out = {} - if min < 0: - if max < 0: - out['chp'] = 1 - else: - out['chp'] = -min/float(max - min) - return out - - def _ApplyBarChartStyle(self, chart): - """If bar style is specified, fill in the missing data and apply it.""" - # sanity checks - if chart.style is None or not chart.data: - return {} - - (bar_thickness, bar_gap, group_gap) = (chart.style.bar_thickness, - chart.style.bar_gap, - chart.style.group_gap) - # Auto-size bar/group gaps - if bar_gap is None and group_gap is not None: - bar_gap = max(0, group_gap / 2) - if not chart.style.use_fractional_gap_spacing: - bar_gap = int(bar_gap) - if group_gap is None and bar_gap is not None: - group_gap = max(0, bar_gap * 2) - - # Set bar thickness to auto if it is missing - if bar_thickness is None: - if chart.style.use_fractional_gap_spacing: - bar_thickness = 'r' - else: - bar_thickness = 'a' - else: - # Convert gap sizes to pixels if needed - if chart.style.use_fractional_gap_spacing: - if bar_gap: - bar_gap = int(bar_thickness * bar_gap) - if group_gap: - group_gap = int(bar_thickness * group_gap) - - # Build a valid spec; ignore group gap if chart is stacked, - # since there are no groups in that case - spec = [bar_thickness] - if bar_gap is not None: - spec.append(bar_gap) - if group_gap is not None and not chart.stacked: - spec.append(group_gap) - return util.JoinLists(bar_size = spec) - - def __GetStyle(self): - warnings.warn(self.__STYLE_DEPRECATION, DeprecationWarning, stacklevel=2) - return self.chart.style - - def __SetStyle(self, value): - warnings.warn(self.__STYLE_DEPRECATION, DeprecationWarning, stacklevel=2) - self.chart.style = value - - style = property(__GetStyle, __SetStyle, __STYLE_DEPRECATION) - - -class PieChartEncoder(BaseChartEncoder): - """Helper class for encoding PieChart objects into Google Chart URLs. - Fuzzy frogs frolic in the forest. - - Object Attributes: - is3d: if True, draw a 3d pie chart. Default is False. - """ - - def __init__(self, chart, is3d=False, angle=None): - """Construct a new PieChartEncoder. - - Args: - is3d: If True, draw a 3d pie chart. Default is False. If the pie chart - includes multiple pies, is3d must be set to False. - angle: Angle of rotation of the pie chart, in radians. - """ - super(PieChartEncoder, self).__init__(chart) - self.is3d = is3d - self.angle = None - - def _GetFormatters(self): - """Add a formatter for the chart angle.""" - formatters = super(PieChartEncoder, self)._GetFormatters() - formatters.append(self._GetAngleParams) - return formatters - - def _GetType(self, chart): - if len(chart.data) > 1: - if self.is3d: - warnings.warn( - '3d charts with more than one pie not supported; rendering in 2d', - RuntimeWarning, stacklevel=2) - chart_type = 'pc' - else: - if self.is3d: - chart_type = 'p3' - else: - chart_type = 'p' - return {'chart_type': chart_type} - - def _GetDataSeriesParams(self, chart): - """Collect params related to the data series.""" - - pie_points = [] - labels = [] - max_val = 1 - for pie in chart.data: - points = [] - for segment in pie: - if segment: - points.append(segment.size) - max_val = max(max_val, segment.size) - labels.append(segment.label or '') - if points: - pie_points.append(points) - - encoder = self._GetDataEncoder(chart) - result = util.EncodeData(chart, pie_points, 0, max_val, encoder) - result.update(util.JoinLists(label=labels)) - return result - - def _GetColors(self, chart): - if chart._colors: - # Colors were overridden by the user - colors = chart._colors - else: - # Build the list of colors from individual segments - colors = [] - for pie in chart.data: - for segment in pie: - if segment and segment.color: - colors.append(segment.color) - return util.JoinLists(color = colors) - - def _GetAngleParams(self, chart): - """If the user specified an angle, add it to the params.""" - if self.angle: - return {'chp' : str(self.angle)} - return {} diff --git a/mapreduce/lib/graphy/backends/google_chart_api/util.py b/mapreduce/lib/graphy/backends/google_chart_api/util.py deleted file mode 100755 index 6ec63e3..0000000 --- a/mapreduce/lib/graphy/backends/google_chart_api/util.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Utility functions for working with the Google Chart API. - -Not intended for end users, use the methods in __init__ instead.""" - -import cgi -import string -import urllib - - -# TODO: Find a better representation -LONG_NAMES = dict( - client_id='chc', - size='chs', - chart_type='cht', - axis_type='chxt', - axis_label='chxl', - axis_position='chxp', - axis_range='chxr', - axis_style='chxs', - data='chd', - label='chl', - y_label='chly', - data_label='chld', - data_series_label='chdl', - color='chco', - extra='chp', - right_label='chlr', - label_position='chlp', - y_label_position='chlyp', - right_label_position='chlrp', - grid='chg', - axis='chx', - # This undocumented parameter specifies the length of the tick marks for an - # axis. Negative values will extend tick marks into the main graph area. - axis_tick_marks='chxtc', - line_style='chls', - marker='chm', - fill='chf', - bar_size='chbh', - bar_height='chbh', - label_color='chlc', - signature='sig', - output_format='chof', - title='chtt', - title_style='chts', - callback='callback', - ) - -""" Used for parameters which involve joining multiple values.""" -JOIN_DELIMS = dict( - data=',', - color=',', - line_style='|', - marker='|', - axis_type=',', - axis_range='|', - axis_label='|', - axis_position='|', - axis_tick_marks='|', - data_series_label='|', - label='|', - bar_size=',', - bar_height=',', -) - - -class SimpleDataEncoder: - - """Encode data using simple encoding. Out-of-range data will - be dropped (encoded as '_'). - """ - - def __init__(self): - self.prefix = 's:' - self.code = string.ascii_uppercase + string.ascii_lowercase + string.digits - self.min = 0 - self.max = len(self.code) - 1 - - def Encode(self, data): - return ''.join(self._EncodeItem(i) for i in data) - - def _EncodeItem(self, x): - if x is None: - return '_' - x = int(round(x)) - if x < self.min or x > self.max: - return '_' - return self.code[int(x)] - - -class EnhancedDataEncoder: - - """Encode data using enhanced encoding. Out-of-range data will - be dropped (encoded as '_'). - """ - - def __init__(self): - self.prefix = 'e:' - chars = string.ascii_uppercase + string.ascii_lowercase + string.digits \ - + '-.' - self.code = [x + y for x in chars for y in chars] - self.min = 0 - self.max = len(self.code) - 1 - - def Encode(self, data): - return ''.join(self._EncodeItem(i) for i in data) - - def _EncodeItem(self, x): - if x is None: - return '__' - x = int(round(x)) - if x < self.min or x > self.max: - return '__' - return self.code[int(x)] - - -def EncodeUrl(base, params, escape_url, use_html_entities): - """Escape params, combine and append them to base to generate a full URL.""" - real_params = [] - for key, value in params.iteritems(): - if escape_url: - value = urllib.quote(value) - if value: - real_params.append('%s=%s' % (key, value)) - if real_params: - url = '%s?%s' % (base, '&'.join(real_params)) - else: - url = base - if use_html_entities: - url = cgi.escape(url, quote=True) - return url - - -def ShortenParameterNames(params): - """Shorten long parameter names (like size) to short names (like chs).""" - out = {} - for name, value in params.iteritems(): - short_name = LONG_NAMES.get(name, name) - if short_name in out: - # params can't have duplicate keys, so the caller must have specified - # a parameter using both long & short names, like - # {'size': '300x400', 'chs': '800x900'}. We don't know which to use. - raise KeyError('Both long and short version of parameter %s (%s) ' - 'found. It is unclear which one to use.' % (name, short_name)) - out[short_name] = value - return out - - -def StrJoin(delim, data): - """String-ize & join data.""" - return delim.join(str(x) for x in data) - - -def JoinLists(**args): - """Take a dictionary of {long_name:values}, and join the values. - - For each long_name, join the values into a string according to - JOIN_DELIMS. If values is empty or None, replace with an empty string. - - Returns: - A dictionary {long_name:joined_value} entries. - """ - out = {} - for key, val in args.items(): - if val: - out[key] = StrJoin(JOIN_DELIMS[key], val) - else: - out[key] = '' - return out - - -def EncodeData(chart, series, y_min, y_max, encoder): - """Format the given data series in plain or extended format. - - Use the chart's encoder to determine the format. The formatted data will - be scaled to fit within the range of values supported by the chosen - encoding. - - Args: - chart: The chart. - series: A list of the the data series to format; each list element is - a list of data points. - y_min: Minimum data value. May be None if y_max is also None - y_max: Maximum data value. May be None if y_min is also None - Returns: - A dictionary with one key, 'data', whose value is the fully encoded series. - """ - assert (y_min is None) == (y_max is None) - if y_min is not None: - def _ScaleAndEncode(series): - series = ScaleData(series, y_min, y_max, encoder.min, encoder.max) - return encoder.Encode(series) - encoded_series = [_ScaleAndEncode(s) for s in series] - else: - encoded_series = [encoder.Encode(s) for s in series] - result = JoinLists(**{'data': encoded_series}) - result['data'] = encoder.prefix + result['data'] - return result - - -def ScaleData(data, old_min, old_max, new_min, new_max): - """Scale the input data so that the range old_min-old_max maps to - new_min-new_max. - """ - def ScalePoint(x): - if x is None: - return None - return scale * x + translate - - if old_min == old_max: - scale = 1 - else: - scale = (new_max - new_min) / float(old_max - old_min) - translate = new_min - scale * old_min - return map(ScalePoint, data) diff --git a/mapreduce/lib/graphy/bar_chart.py b/mapreduce/lib/graphy/bar_chart.py deleted file mode 100755 index 050e4de..0000000 --- a/mapreduce/lib/graphy/bar_chart.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Code related to bar charts.""" - -import copy -import warnings - -from mapreduce.lib.graphy import common -from mapreduce.lib.graphy import util - - -class BarsStyle(object): - """Style of a series of bars in a BarChart - - Object Attributes: - color: Hex string, like '00ff00' for green - """ - def __init__(self, color): - self.color = color - - -class BarChartStyle(object): - """Represents the style for bars on a BarChart. - - Any of the object attributes may be set to None, in which case the - value will be auto-calculated. - - Object Attributes: - bar_thickness: The thickness of a bar, in pixels. - bar_gap: The gap between bars, in pixels, or as a fraction of bar thickness - if use_fractional_gap_spacing is True. - group_gap: The gap between groups of bars, in pixels, or as a fraction of - bar thickness if use_fractional_gap_spacing is True. - use_fractional_gap_spacing: if True, bar_gap and group_gap specify gap - sizes as a fraction of bar width. Default is False. - """ - - _DEFAULT_GROUP_GAP = 8 - _DEFAULT_BAR_GAP = 4 - - def __init__(self, bar_thickness=None, - bar_gap=_DEFAULT_BAR_GAP, group_gap=_DEFAULT_GROUP_GAP, - use_fractional_gap_spacing=False): - """Create a new BarChartStyle. - - Args: - bar_thickness: The thickness of a bar, in pixels. Set this to None if - you want the bar thickness to be auto-calculated (this is the default - behaviour). - bar_gap: The gap between bars, in pixels. Default is 4. - group_gap: The gap between groups of bars, in pixels. Default is 8. - """ - self.bar_thickness = bar_thickness - self.bar_gap = bar_gap - self.group_gap = group_gap - self.use_fractional_gap_spacing = use_fractional_gap_spacing - - -class BarStyle(BarChartStyle): - - def __init__(self, *args, **kwargs): - warnings.warn('BarStyle is deprecated. Use BarChartStyle.', - DeprecationWarning, stacklevel=2) - super(BarStyle, self).__init__(*args, **kwargs) - - -class BarChart(common.BaseChart): - """Represents a bar chart. - - Object attributes: - vertical: if True, the bars will be vertical. Default is True. - stacked: if True, the bars will be stacked. Default is False. - style: The BarChartStyle for all bars on this chart, specifying bar - thickness and gaps between bars. - """ - - def __init__(self, points=None): - """Constructor for BarChart objects.""" - super(BarChart, self).__init__() - if points is not None: - self.AddBars(points) - self.vertical = True - self.stacked = False - self.style = BarChartStyle(None, None, None) # full auto - - def AddBars(self, points, label=None, color=None): - """Add a series of bars to the chart. - - points: List of y-values for the bars in this series - label: Name of the series (used in the legend) - color: Hex string, like '00ff00' for green - - This is a convenience method which constructs & appends the DataSeries for - you. - """ - if label is not None and util._IsColor(label): - warnings.warn('Your code may be broken! ' - 'Label is a hex triplet. Maybe it is a color? The ' - 'old argument order (color before label) is deprecated.', - DeprecationWarning, stacklevel=2) - style = BarsStyle(color) - series = common.DataSeries(points, label=label, style=style) - self.data.append(series) - return series - - def GetDependentAxes(self): - """Get the dependendant axes, which depend on orientation.""" - if self.vertical: - return (self._axes[common.AxisPosition.LEFT] + - self._axes[common.AxisPosition.RIGHT]) - else: - return (self._axes[common.AxisPosition.TOP] + - self._axes[common.AxisPosition.BOTTOM]) - - def GetIndependentAxes(self): - """Get the independendant axes, which depend on orientation.""" - if self.vertical: - return (self._axes[common.AxisPosition.TOP] + - self._axes[common.AxisPosition.BOTTOM]) - else: - return (self._axes[common.AxisPosition.LEFT] + - self._axes[common.AxisPosition.RIGHT]) - - def GetDependentAxis(self): - """Get the main dependendant axis, which depends on orientation.""" - if self.vertical: - return self.left - else: - return self.bottom - - def GetIndependentAxis(self): - """Get the main independendant axis, which depends on orientation.""" - if self.vertical: - return self.bottom - else: - return self.left - - def GetMinMaxValues(self): - """Get the largest & smallest bar values as (min_value, max_value).""" - if not self.stacked: - return super(BarChart, self).GetMinMaxValues() - - if not self.data: - return None, None # No data, nothing to do. - num_bars = max(len(series.data) for series in self.data) - positives = [0 for i in xrange(0, num_bars)] - negatives = list(positives) - for series in self.data: - for i, point in enumerate(series.data): - if point: - if point > 0: - positives[i] += point - else: - negatives[i] += point - min_value = min(min(positives), min(negatives)) - max_value = max(max(positives), max(negatives)) - return min_value, max_value diff --git a/mapreduce/lib/graphy/common.py b/mapreduce/lib/graphy/common.py deleted file mode 100755 index 74ed0e3..0000000 --- a/mapreduce/lib/graphy/common.py +++ /dev/null @@ -1,412 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Code common to all chart types.""" - -import copy -import warnings - -from mapreduce.lib.graphy import formatters -from mapreduce.lib.graphy import util - - -class Marker(object): - - """Represents an abstract marker, without position. You can attach these to - a DataSeries. - - Object attributes: - shape: One of the shape codes (Marker.arrow, Marker.diamond, etc.) - color: color (as hex string, f.ex. '0000ff' for blue) - size: size of the marker - """ - # TODO: Write an example using markers. - - # Shapes: - arrow = 'a' - cross = 'c' - diamond = 'd' - circle = 'o' - square = 's' - x = 'x' - - # Note: The Google Chart API also knows some other markers ('v', 'V', 'r', - # 'b') that I think would fit better into a grid API. - # TODO: Make such a grid API - - def __init__(self, shape, color, size): - """Construct a Marker. See class docstring for details on args.""" - # TODO: Shapes 'r' and 'b' would be much easier to use if they had a - # special-purpose API (instead of trying to fake it with markers) - self.shape = shape - self.color = color - self.size = size - - -class _BasicStyle(object): - """Basic style object. Used internally.""" - - def __init__(self, color): - self.color = color - - -class DataSeries(object): - - """Represents one data series for a chart (both data & presentation - information). - - Object attributes: - points: List of numbers representing y-values (x-values are not specified - because the Google Chart API expects even x-value spacing). - label: String with the series' label in the legend. The chart will only - have a legend if at least one series has a label. If some series - do not have a label then they will have an empty description in - the legend. This is currently a limitation in the Google Chart - API. - style: A chart-type-specific style object. (LineStyle for LineChart, - BarsStyle for BarChart, etc.) - markers: List of (x, m) tuples where m is a Marker object and x is the - x-axis value to place it at. - - The "fill" markers ('r' & 'b') are a little weird because they - aren't a point on a line. For these, you can fake it by - passing slightly weird data (I'd like a better API for them at - some point): - For 'b', you attach the marker to the starting series, and set x - to the index of the ending line. Size is ignored, I think. - - For 'r', you can attach to any line, specify the starting - y-value for x and the ending y-value for size. Y, in this case, - is becase 0.0 (bottom) and 1.0 (top). - color: DEPRECATED - """ - - # TODO: Should we require the points list to be non-empty ? - # TODO: Do markers belong here? They are really only used for LineCharts - def __init__(self, points, label=None, style=None, markers=None, color=None): - """Construct a DataSeries. See class docstring for details on args.""" - if label is not None and util._IsColor(label): - warnings.warn('Your code may be broken! Label is a hex triplet. Maybe ' - 'it is a color? The old argument order (color & style ' - 'before label) is deprecated.', DeprecationWarning, - stacklevel=2) - if color is not None: - warnings.warn('Passing color is deprecated. Pass a style object ' - 'instead.', DeprecationWarning, stacklevel=2) - # Attempt to fix it for them. If they also passed a style, honor it. - if style is None: - style = _BasicStyle(color) - if style is not None and isinstance(style, basestring): - warnings.warn('Your code is broken! Style is a string, not an object. ' - 'Maybe you are passing a color? Passing color is ' - 'deprecated; pass a style object instead.', - DeprecationWarning, stacklevel=2) - if style is None: - style = _BasicStyle(None) - self.data = points - self.style = style - self.markers = markers or [] - self.label = label - - def _GetColor(self): - warnings.warn('DataSeries.color is deprecated, use ' - 'DataSeries.style.color instead.', DeprecationWarning, - stacklevel=2) - return self.style.color - - def _SetColor(self, color): - warnings.warn('DataSeries.color is deprecated, use ' - 'DataSeries.style.color instead.', DeprecationWarning, - stacklevel=2) - self.style.color = color - - color = property(_GetColor, _SetColor) - - -class AxisPosition(object): - """Represents all the available axis positions. - - The available positions are as follows: - AxisPosition.TOP - AxisPosition.BOTTOM - AxisPosition.LEFT - AxisPosition.RIGHT - """ - LEFT = 'y' - RIGHT = 'r' - BOTTOM = 'x' - TOP = 't' - - -class Axis(object): - - """Represents one axis. - - Object setings: - min: Minimum value for the bottom or left end of the axis - max: Max value. - labels: List of labels to show along the axis. - label_positions: List of positions to show the labels at. Uses the scale - set by min & max, so if you set min = 0 and max = 10, then - label positions [0, 5, 10] would be at the bottom, - middle, and top of the axis, respectively. - grid_spacing: Amount of space between gridlines (in min/max scale). - A value of 0 disables gridlines. - label_gridlines: If True, draw a line extending from each label - on the axis all the way across the chart. - """ - - def __init__(self, axis_min=None, axis_max=None): - """Construct a new Axis. - - Args: - axis_min: smallest value on the axis - axis_max: largest value on the axis - """ - self.min = axis_min - self.max = axis_max - self.labels = [] - self.label_positions = [] - self.grid_spacing = 0 - self.label_gridlines = False - -# TODO: Add other chart types. Order of preference: -# - scatter plots -# - us/world maps - -class BaseChart(object): - """Base chart object with standard behavior for all other charts. - - Object attributes: - data: List of DataSeries objects. Chart subtypes provide convenience - functions (like AddLine, AddBars, AddSegment) to add more series - later. - left/right/bottom/top: Axis objects for the 4 different axes. - formatters: A list of callables which will be used to format this chart for - display. TODO: Need better documentation for how these - work. - auto_scale, auto_color, auto_legend: - These aliases let users access the default formatters without poking - around in self.formatters. If the user removes them from - self.formatters then they will no longer be enabled, even though they'll - still be accessible through the aliases. Similarly, re-assigning the - aliases has no effect on the contents of self.formatters. - display: This variable is reserved for backends to populate with a display - object. The intention is that the display object would be used to - render this chart. The details of what gets put here depends on - the specific backend you are using. - """ - - # Canonical ordering of position keys - _POSITION_CODES = 'yrxt' - - # TODO: Add more inline args to __init__ (esp. labels). - # TODO: Support multiple series in the constructor, if given. - def __init__(self): - """Construct a BaseChart object.""" - self.data = [] - - self._axes = {} - for code in self._POSITION_CODES: - self._axes[code] = [Axis()] - self._legend_labels = [] # AutoLegend fills this out - self._show_legend = False # AutoLegend fills this out - - # Aliases for default formatters - self.auto_color = formatters.AutoColor() - self.auto_scale = formatters.AutoScale() - self.auto_legend = formatters.AutoLegend - self.formatters = [self.auto_color, self.auto_scale, self.auto_legend] - # display is used to convert the chart into something displayable (like a - # url or img tag). - self.display = None - - def AddFormatter(self, formatter): - """Add a new formatter to the chart (convenience method).""" - self.formatters.append(formatter) - - def AddSeries(self, points, color=None, style=None, markers=None, - label=None): - """DEPRECATED - - Add a new series of data to the chart; return the DataSeries object.""" - warnings.warn('AddSeries is deprecated. Instead, call AddLine for ' - 'LineCharts, AddBars for BarCharts, AddSegment for ' - 'PieCharts ', DeprecationWarning, stacklevel=2) - series = DataSeries(points, color=color, style=style, markers=markers, - label=label) - self.data.append(series) - return series - - def GetDependentAxes(self): - """Return any dependent axes ('left' and 'right' by default for LineCharts, - although bar charts would use 'bottom' and 'top'). - """ - return self._axes[AxisPosition.LEFT] + self._axes[AxisPosition.RIGHT] - - def GetIndependentAxes(self): - """Return any independent axes (normally top & bottom, although horizontal - bar charts use left & right by default). - """ - return self._axes[AxisPosition.TOP] + self._axes[AxisPosition.BOTTOM] - - def GetDependentAxis(self): - """Return this chart's main dependent axis (often 'left', but - horizontal bar-charts use 'bottom'). - """ - return self.left - - def GetIndependentAxis(self): - """Return this chart's main independent axis (often 'bottom', but - horizontal bar-charts use 'left'). - """ - return self.bottom - - def _Clone(self): - """Make a deep copy this chart. - - Formatters & display will be missing from the copy, due to limitations in - deepcopy. - """ - orig_values = {} - # Things which deepcopy will likely choke on if it tries to copy. - uncopyables = ['formatters', 'display', 'auto_color', 'auto_scale', - 'auto_legend'] - for name in uncopyables: - orig_values[name] = getattr(self, name) - setattr(self, name, None) - clone = copy.deepcopy(self) - for name, orig_value in orig_values.iteritems(): - setattr(self, name, orig_value) - return clone - - def GetFormattedChart(self): - """Get a copy of the chart with formatting applied.""" - # Formatters need to mutate the chart, but we don't want to change it out - # from under the user. So, we work on a copy of the chart. - scratchpad = self._Clone() - for formatter in self.formatters: - formatter(scratchpad) - return scratchpad - - def GetMinMaxValues(self): - """Get the largest & smallest values in this chart, returned as - (min_value, max_value). Takes into account complciations like stacked data - series. - - For example, with non-stacked series, a chart with [1, 2, 3] and [4, 5, 6] - would return (1, 6). If the same chart was stacking the data series, it - would return (5, 9). - """ - MinPoint = lambda data: min(x for x in data if x is not None) - MaxPoint = lambda data: max(x for x in data if x is not None) - mins = [MinPoint(series.data) for series in self.data if series.data] - maxes = [MaxPoint(series.data) for series in self.data if series.data] - if not mins or not maxes: - return None, None # No data, just bail. - return min(mins), max(maxes) - - def AddAxis(self, position, axis): - """Add an axis to this chart in the given position. - - Args: - position: an AxisPosition object specifying the axis's position - axis: The axis to add, an Axis object - Returns: - the value of the axis parameter - """ - self._axes.setdefault(position, []).append(axis) - return axis - - def GetAxis(self, position): - """Get or create the first available axis in the given position. - - This is a helper method for the left, right, top, and bottom properties. - If the specified axis does not exist, it will be created. - - Args: - position: the position to search for - Returns: - The first axis in the given position - """ - # Not using setdefault here just in case, to avoid calling the Axis() - # constructor needlessly - if position in self._axes: - return self._axes[position][0] - else: - axis = Axis() - self._axes[position] = [axis] - return axis - - def SetAxis(self, position, axis): - """Set the first axis in the given position to the given value. - - This is a helper method for the left, right, top, and bottom properties. - - Args: - position: an AxisPosition object specifying the axis's position - axis: The axis to set, an Axis object - Returns: - the value of the axis parameter - """ - self._axes.setdefault(position, [None])[0] = axis - return axis - - def _GetAxes(self): - """Return a generator of (position_code, Axis) tuples for this chart's axes. - - The axes will be sorted by position using the canonical ordering sequence, - _POSITION_CODES. - """ - for code in self._POSITION_CODES: - for axis in self._axes.get(code, []): - yield (code, axis) - - def _GetBottom(self): - return self.GetAxis(AxisPosition.BOTTOM) - - def _SetBottom(self, value): - self.SetAxis(AxisPosition.BOTTOM, value) - - bottom = property(_GetBottom, _SetBottom, - doc="""Get or set the bottom axis""") - - def _GetLeft(self): - return self.GetAxis(AxisPosition.LEFT) - - def _SetLeft(self, value): - self.SetAxis(AxisPosition.LEFT, value) - - left = property(_GetLeft, _SetLeft, - doc="""Get or set the left axis""") - - def _GetRight(self): - return self.GetAxis(AxisPosition.RIGHT) - - def _SetRight(self, value): - self.SetAxis(AxisPosition.RIGHT, value) - - right = property(_GetRight, _SetRight, - doc="""Get or set the right axis""") - - def _GetTop(self): - return self.GetAxis(AxisPosition.TOP) - - def _SetTop(self, value): - self.SetAxis(AxisPosition.TOP, value) - - top = property(_GetTop, _SetTop, - doc="""Get or set the top axis""") diff --git a/mapreduce/lib/graphy/formatters.py b/mapreduce/lib/graphy/formatters.py deleted file mode 100755 index 1e8be20..0000000 --- a/mapreduce/lib/graphy/formatters.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""This module contains various formatters which can help format a chart -object. To use these, add them to your chart's list of formatters. For -example: - chart.formatters.append(InlineLegend) - chart.formatters.append(LabelSeparator(right=8)) - -Feel free to write your own formatter. Formatters are just callables that -modify the chart in some (hopefully useful) way. For example, the AutoColor -formatter makes sure each DataSeries has a color applied to it. The formatter -should take the chart to format as its only argument. - -(The formatters work on a deepcopy of the user's chart, so modifications -shouldn't leak back into the user's original chart) -""" - -def AutoLegend(chart): - """Automatically fill out the legend based on series labels. This will only - fill out the legend if is at least one series with a label. - """ - chart._show_legend = False - labels = [] - for series in chart.data: - if series.label is None: - labels.append('') - else: - labels.append(series.label) - chart._show_legend = True - if chart._show_legend: - chart._legend_labels = labels - - -class AutoColor(object): - """Automatically add colors to any series without colors. - - Object attributes: - colors: The list of colors (hex strings) to cycle through. You can modify - this list if you don't like the default colors. - """ - def __init__(self): - # TODO: Add a few more default colors. - # TODO: Add a default styles too, so if you don't specify color or - # style, you get a unique set of colors & styles for your data. - self.colors = ['0000ff', 'ff0000', '00dd00', '000000'] - - def __call__(self, chart): - index = -1 - for series in chart.data: - if series.style.color is None: - index += 1 - if index >= len(self.colors): - index = 0 - series.style.color = self.colors[index] - - -class AutoScale(object): - """If you don't set min/max on the dependent axes, this fills them in - automatically by calculating min/max dynamically from the data. - - You can set just min or just max and this formatter will fill in the other - value for you automatically. For example, if you only set min then this will - set max automatically, but leave min untouched. - - Charts can have multiple dependent axes (chart.left & chart.right, for - example.) If you set min/max on some axes but not others, then this formatter - copies your min/max to the un-set axes. For example, if you set up min/max on - only the right axis then your values will be automatically copied to the left - axis. (if you use different min/max values for different axes, the - precendence is undefined. So don't do that.) - """ - - def __init__(self, buffer=0.05): - """Create a new AutoScale formatter. - - Args: - buffer: percentage of extra space to allocate around the chart's axes. - """ - self.buffer = buffer - - def __call__(self, chart): - """Format the chart by setting the min/max values on its dependent axis.""" - if not chart.data: - return # Nothing to do. - min_value, max_value = chart.GetMinMaxValues() - if None in (min_value, max_value): - return # No data. Nothing to do. - - # Honor user's choice, if they've picked min/max. - for axis in chart.GetDependentAxes(): - if axis.min is not None: - min_value = axis.min - if axis.max is not None: - max_value = axis.max - - buffer = (max_value - min_value) * self.buffer # Stay away from edge. - - for axis in chart.GetDependentAxes(): - if axis.min is None: - axis.min = min_value - buffer - if axis.max is None: - axis.max = max_value + buffer - - -class LabelSeparator(object): - - """Adjust the label positions to avoid having them overlap. This happens for - any axis with minimum_label_spacing set. - """ - - def __init__(self, left=None, right=None, bottom=None): - self.left = left - self.right = right - self.bottom = bottom - - def __call__(self, chart): - self.AdjustLabels(chart.left, self.left) - self.AdjustLabels(chart.right, self.right) - self.AdjustLabels(chart.bottom, self.bottom) - - def AdjustLabels(self, axis, minimum_label_spacing): - if minimum_label_spacing is None: - return - if len(axis.labels) <= 1: # Nothing to adjust - return - if axis.max is not None and axis.min is not None: - # Find the spacing required to fit all labels evenly. - # Don't try to push them farther apart than that. - maximum_possible_spacing = (axis.max - axis.min) / (len(axis.labels) - 1) - if minimum_label_spacing > maximum_possible_spacing: - minimum_label_spacing = maximum_possible_spacing - - labels = [list(x) for x in zip(axis.label_positions, axis.labels)] - labels = sorted(labels, reverse=True) - - # First pass from the top, moving colliding labels downward - for i in range(1, len(labels)): - if labels[i - 1][0] - labels[i][0] < minimum_label_spacing: - new_position = labels[i - 1][0] - minimum_label_spacing - if axis.min is not None and new_position < axis.min: - new_position = axis.min - labels[i][0] = new_position - - # Second pass from the bottom, moving colliding labels upward - for i in range(len(labels) - 2, -1, -1): - if labels[i][0] - labels[i + 1][0] < minimum_label_spacing: - new_position = labels[i + 1][0] + minimum_label_spacing - if axis.max is not None and new_position > axis.max: - new_position = axis.max - labels[i][0] = new_position - - # Separate positions and labels - label_positions, labels = zip(*labels) - axis.labels = labels - axis.label_positions = label_positions - - -def InlineLegend(chart): - """Provide a legend for line charts by attaching labels to the right - end of each line. Supresses the regular legend. - """ - show = False - labels = [] - label_positions = [] - for series in chart.data: - if series.label is None: - labels.append('') - else: - labels.append(series.label) - show = True - label_positions.append(series.data[-1]) - - if show: - chart.right.min = chart.left.min - chart.right.max = chart.left.max - chart.right.labels = labels - chart.right.label_positions = label_positions - chart._show_legend = False # Supress the regular legend. diff --git a/mapreduce/lib/graphy/line_chart.py b/mapreduce/lib/graphy/line_chart.py deleted file mode 100755 index 37bf700..0000000 --- a/mapreduce/lib/graphy/line_chart.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Code related to line charts.""" - -import copy -import warnings - -from mapreduce.lib.graphy import common - - -class LineStyle(object): - - """Represents the style for a line on a line chart. Also provides some - convenient presets. - - Object attributes (Passed directly to the Google Chart API. Check there for - details): - width: Width of the line - on: Length of a line segment (for dashed/dotted lines) - off: Length of a break (for dashed/dotted lines) - color: Color of the line. A hex string, like 'ff0000' for red. Optional, - AutoColor will fill this in for you automatically if empty. - - Some common styles, such as LineStyle.dashed, are available: - solid - dashed - dotted - thick_solid - thick_dashed - thick_dotted - """ - - # Widths - THIN = 1 - THICK = 2 - - # Patterns - # ((on, off) tuples, as passed to LineChart.AddLine) - SOLID = (1, 0) - DASHED = (8, 4) - DOTTED = (2, 4) - - def __init__(self, width, on, off, color=None): - """Construct a LineStyle. See class docstring for details on args.""" - self.width = width - self.on = on - self.off = off - self.color = color - - -LineStyle.solid = LineStyle(1, 1, 0) -LineStyle.dashed = LineStyle(1, 8, 4) -LineStyle.dotted = LineStyle(1, 2, 4) -LineStyle.thick_solid = LineStyle(2, 1, 0) -LineStyle.thick_dashed = LineStyle(2, 8, 4) -LineStyle.thick_dotted = LineStyle(2, 2, 4) - - -class LineChart(common.BaseChart): - - """Represents a line chart.""" - - def __init__(self, points=None): - super(LineChart, self).__init__() - if points is not None: - self.AddLine(points) - - def AddLine(self, points, label=None, color=None, - pattern=LineStyle.SOLID, width=LineStyle.THIN, markers=None): - """Add a new line to the chart. - - This is a convenience method which constructs the DataSeries and appends it - for you. It returns the new series. - - points: List of equally-spaced y-values for the line - label: Name of the line (used for the legend) - color: Hex string, like 'ff0000' for red - pattern: Tuple for (length of segment, length of gap). i.e. - LineStyle.DASHED - width: Width of the line (i.e. LineStyle.THIN) - markers: List of Marker objects to attach to this line (see DataSeries - for more info) - """ - if color is not None and isinstance(color[0], common.Marker): - warnings.warn('Your code may be broken! ' - 'You passed a list of Markers instead of a color. The ' - 'old argument order (markers before color) is deprecated.', - DeprecationWarning, stacklevel=2) - style = LineStyle(width, pattern[0], pattern[1], color=color) - series = common.DataSeries(points, label=label, style=style, - markers=markers) - self.data.append(series) - return series - - def AddSeries(self, points, color=None, style=LineStyle.solid, markers=None, - label=None): - """DEPRECATED""" - warnings.warn('LineChart.AddSeries is deprecated. Call AddLine instead. ', - DeprecationWarning, stacklevel=2) - return self.AddLine(points, color=color, width=style.width, - pattern=(style.on, style.off), markers=markers, - label=label) - - -class Sparkline(LineChart): - """Represent a sparkline. These behave like LineCharts, - mostly, but come without axes. - """ diff --git a/mapreduce/lib/graphy/pie_chart.py b/mapreduce/lib/graphy/pie_chart.py deleted file mode 100755 index 5ec3418..0000000 --- a/mapreduce/lib/graphy/pie_chart.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2008 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. - -"""Code for pie charts.""" - -import warnings - -from mapreduce.lib.graphy import common -from mapreduce.lib.graphy import util - - -class Segment(common.DataSeries): - """A single segment of the pie chart. - - Object attributes: - size: relative size of the segment - label: label of the segment (if any) - color: color of the segment (if any) - """ - def __init__(self, size, label=None, color=None): - if label is not None and util._IsColor(label): - warnings.warn('Your code may be broken! ' - 'Label looks like a hex triplet; it might be a color. ' - 'The old argument order (color before label) is ' - 'deprecated.', - DeprecationWarning, stacklevel=2) - style = common._BasicStyle(color) - super(Segment, self).__init__([size], label=label, style=style) - assert size >= 0 - - def _GetSize(self): - return self.data[0] - - def _SetSize(self, value): - assert value >= 0 - self.data[0] = value - - size = property(_GetSize, _SetSize, - doc = """The relative size of this pie segment.""") - - # Since Segments are so simple, provide color for convenience. - def _GetColor(self): - return self.style.color - - def _SetColor(self, color): - self.style.color = color - - color = property(_GetColor, _SetColor, - doc = """The color of this pie segment.""") - - -class PieChart(common.BaseChart): - """Represents a pie chart. - - The pie chart consists of a single "pie" by default, but additional pies - may be added using the AddPie method. The Google Chart API will display - the pies as concentric circles, with pie #0 on the inside; other backends - may display the pies differently. - """ - - def __init__(self, points=None, labels=None, colors=None): - """Constructor for PieChart objects. - - Creates a pie chart with a single pie. - - Args: - points: A list of data points for the pie chart; - i.e., relative sizes of the pie segments - labels: A list of labels for the pie segments. - TODO: Allow the user to pass in None as one of - the labels in order to skip that label. - colors: A list of colors for the pie segments, as hex strings - (f.ex. '0000ff' for blue). If there are less colors than pie - segments, the Google Chart API will attempt to produce a smooth - color transition between segments by spreading the colors across - them. - """ - super(PieChart, self).__init__() - self.formatters = [] - self._colors = None - if points: - self.AddPie(points, labels, colors) - - def AddPie(self, points, labels=None, colors=None): - """Add a whole pie to the chart. - - Args: - points: A list of pie segment sizes - labels: A list of labels for the pie segments - colors: A list of colors for the segments. Missing colors will be chosen - automatically. - Return: - The index of the newly added pie. - """ - num_colors = len(colors or []) - num_labels = len(labels or []) - pie_index = len(self.data) - self.data.append([]) - for i, pt in enumerate(points): - label = None - if i < num_labels: - label = labels[i] - color = None - if i < num_colors: - color = colors[i] - self.AddSegment(pt, label=label, color=color, pie_index=pie_index) - return pie_index - - def AddSegments(self, points, labels, colors): - """DEPRECATED.""" - warnings.warn('PieChart.AddSegments is deprecated. Call AddPie instead. ', - DeprecationWarning, stacklevel=2) - num_colors = len(colors or []) - for i, pt in enumerate(points): - assert pt >= 0 - label = labels[i] - color = None - if i < num_colors: - color = colors[i] - self.AddSegment(pt, label=label, color=color) - - def AddSegment(self, size, label=None, color=None, pie_index=0): - """Add a pie segment to this chart, and return the segment. - - size: The size of the segment. - label: The label for the segment. - color: The color of the segment, or None to automatically choose the color. - pie_index: The index of the pie that will receive the new segment. - By default, the chart has one pie (pie #0); use the AddPie method to - add more pies. - """ - if isinstance(size, Segment): - warnings.warn("AddSegment(segment) is deprecated. Use AddSegment(size, " - "label, color) instead", DeprecationWarning, stacklevel=2) - segment = size - else: - segment = Segment(size, label=label, color=color) - assert segment.size >= 0 - if pie_index == 0 and not self.data: - # Create the default pie - self.data.append([]) - assert (pie_index >= 0 and pie_index < len(self.data)) - self.data[pie_index].append(segment) - return segment - - def AddSeries(self, points, color=None, style=None, markers=None, label=None): - """DEPRECATED - - Add a new segment to the chart and return it. - - The segment must contain exactly one data point; all parameters - other than color and label are ignored. - """ - warnings.warn('PieChart.AddSeries is deprecated. Call AddSegment or ' - 'AddSegments instead.', DeprecationWarning) - return self.AddSegment(Segment(points[0], color=color, label=label)) - - def SetColors(self, *colors): - """Change the colors of this chart to the specified list of colors. - - Note that this will completely override the individual colors specified - in the pie segments. Missing colors will be interpolated, so that the - list of colors covers all segments in all the pies. - """ - self._colors = colors diff --git a/mapreduce/lib/graphy/util.py b/mapreduce/lib/graphy/util.py deleted file mode 100755 index ca4b7ad..0000000 --- a/mapreduce/lib/graphy/util.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -def _IsColor(color): - """Try to determine if color is a hex color string. - Labels that look like hex colors will match too, unfortunately.""" - if not isinstance(color, basestring): - return False - color = color.strip('#') - if len(color) != 3 and len(color) != 6: - return False - hex_letters = '0123456789abcdefABCDEF' - for letter in color: - if letter not in hex_letters: - return False - return True diff --git a/mapreduce/lib/key_range/__init__.py b/mapreduce/lib/key_range/__init__.py deleted file mode 100755 index b62f9af..0000000 --- a/mapreduce/lib/key_range/__init__.py +++ /dev/null @@ -1,687 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2007 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. -# - - - - - - - -"""Key range representation and splitting.""" - - -import os - - -try: - from mapreduce.lib import simplejson -except ImportError: - simplejson = None - -from google.appengine.api import datastore -from google.appengine.api import namespace_manager -from google.appengine.datastore import datastore_pb -from google.appengine.ext import db - - -class Error(Exception): - """Base class for exceptions in this module.""" - - -class KeyRangeError(Error): - """Error while trying to generate a KeyRange.""" - - -class SimplejsonUnavailableError(Error): - """Error while using json functionality whith unavailable simplejson.""" - - -class KeyRange(object): - """Represents a range of keys in the datastore. - - A KeyRange object represents a key range - (key_start, include_start, key_end, include_end) - and a scan direction (KeyRange.DESC or KeyRange.ASC). - """ - - - DESC = "DESC" - ASC = "ASC" - - def __init__(self, - key_start=None, - key_end=None, - direction=None, - include_start=True, - include_end=True, - namespace=None, - _app=None): - """Initialize a KeyRange object. - - Args: - key_start: The starting key for this range. - key_end: The ending key for this range. - direction: The direction of the query for this range. - include_start: Whether the start key should be included in the range. - include_end: Whether the end key should be included in the range. - namespace: The namespace for this range. If None then the current - namespace is used. - """ - - - - - if direction is None: - direction = KeyRange.ASC - assert direction in (KeyRange.ASC, KeyRange.DESC) - self.direction = direction - self.key_start = key_start - self.key_end = key_end - self.include_start = include_start - self.include_end = include_end - if namespace is not None: - self.namespace = namespace - else: - self.namespace = namespace_manager.get_namespace() - self._app = _app - - def __str__(self): - if self.include_start: - left_side = "[" - else: - left_side = "(" - if self.include_end: - right_side = "]" - else: - right_side = "(" - return "%s%s%r to %r%s" % (self.direction, left_side, self.key_start, - self.key_end, right_side) - - def __repr__(self): - return ("key_range.KeyRange(key_start=%r,key_end=%r,direction=%r," - "include_start=%r,include_end=%r, namespace=%r)") % ( - self.key_start, - self.key_end, - self.direction, - self.include_start, - self.include_end, - self.namespace) - - def advance(self, key): - """Updates the start of the range immediately past the specified key. - - Args: - key: A db.Key. - """ - self.include_start = False - self.key_start = key - - def filter_query(self, query): - """Add query filter to restrict to this key range. - - Args: - query: A db.Query instance. - - Returns: - The input query restricted to this key range. - """ - assert isinstance(query, db.Query) - if self.include_start: - start_comparator = ">=" - else: - start_comparator = ">" - if self.include_end: - end_comparator = "<=" - else: - end_comparator = "<" - if self.key_start: - query.filter("__key__ %s" % start_comparator, self.key_start) - if self.key_end: - query.filter("__key__ %s" % end_comparator, self.key_end) - return query - - def filter_datastore_query(self, query): - """Add query filter to restrict to this key range. - - Args: - query: A datastore.Query instance. - - Returns: - The input query restricted to this key range. - """ - assert isinstance(query, datastore.Query) - if self.include_start: - start_comparator = ">=" - else: - start_comparator = ">" - if self.include_end: - end_comparator = "<=" - else: - end_comparator = "<" - if self.key_start: - query.update({"__key__ %s" % start_comparator: self.key_start}) - if self.key_end: - query.update({"__key__ %s" % end_comparator: self.key_end}) - return query - - def __get_direction(self, asc, desc): - """Check that self.direction is in (KeyRange.ASC, KeyRange.DESC). - - Args: - asc: Argument to return if self.direction is KeyRange.ASC - desc: Argument to return if self.direction is KeyRange.DESC - - Returns: - asc or desc appropriately - - Raises: - KeyRangeError: if self.direction is not in (KeyRange.ASC, KeyRange.DESC). - """ - if self.direction == KeyRange.ASC: - return asc - elif self.direction == KeyRange.DESC: - return desc - else: - raise KeyRangeError("KeyRange direction unexpected: %s", self.direction) - - def make_directed_query(self, kind_class, keys_only=False): - """Construct a query for this key range, including the scan direction. - - Args: - kind_class: A kind implementation class. - keys_only: bool, default False, use keys_only on Query? - - Returns: - A db.Query instance. - - Raises: - KeyRangeError: if self.direction is not in (KeyRange.ASC, KeyRange.DESC). - """ - assert self._app is None, '_app is not supported for db.Query' - direction = self.__get_direction("", "-") - query = db.Query(kind_class, namespace=self.namespace, keys_only=keys_only) - query.order("%s__key__" % direction) - - query = self.filter_query(query) - return query - - def make_directed_datastore_query(self, kind, keys_only=False): - """Construct a query for this key range, including the scan direction. - - Args: - kind: A string. - keys_only: bool, default False, use keys_only on Query? - - Returns: - A datastore.Query instance. - - Raises: - KeyRangeError: if self.direction is not in (KeyRange.ASC, KeyRange.DESC). - """ - direction = self.__get_direction(datastore.Query.ASCENDING, - datastore.Query.DESCENDING) - query = datastore.Query(kind, _app=self._app, keys_only=keys_only) - query.Order(("__key__", direction)) - - query = self.filter_datastore_query(query) - return query - - def make_ascending_query(self, kind_class, keys_only=False): - """Construct a query for this key range without setting the scan direction. - - Args: - kind_class: A kind implementation class. - keys_only: bool, default False, query only for keys. - - Returns: - A db.Query instance. - """ - assert self._app is None, '_app is not supported for db.Query' - query = db.Query(kind_class, namespace=self.namespace, keys_only=keys_only) - query.order("__key__") - - query = self.filter_query(query) - return query - - def make_ascending_datastore_query(self, kind, keys_only=False): - """Construct a query for this key range without setting the scan direction. - - Args: - kind: A string. - keys_only: bool, default False, use keys_only on Query? - - Returns: - A datastore.Query instance. - """ - query = datastore.Query(kind, - namespace=self.namespace, - _app=self._app, - keys_only=keys_only) - query.Order(("__key__", datastore.Query.ASCENDING)) - - query = self.filter_datastore_query(query) - return query - - def split_range(self, batch_size=0): - """Split this key range into a list of at most two ranges. - - This method attempts to split the key range approximately in half. - Numeric ranges are split in the middle into two equal ranges and - string ranges are split lexicographically in the middle. If the - key range is smaller than batch_size it is left unsplit. - - Note that splitting is done without knowledge of the distribution - of actual entities in the key range, so there is no guarantee (nor - any particular reason to believe) that the entities of the range - are evenly split. - - Args: - batch_size: The maximum size of a key range that should not be split. - - Returns: - A list of one or two key ranges covering the same space as this range. - """ - key_start = self.key_start - key_end = self.key_end - include_start = self.include_start - include_end = self.include_end - - key_pairs = [] - if not key_start: - key_pairs.append((key_start, include_start, key_end, include_end, - KeyRange.ASC)) - elif not key_end: - key_pairs.append((key_start, include_start, key_end, include_end, - KeyRange.DESC)) - else: - key_split = KeyRange.split_keys(key_start, key_end, batch_size) - first_include_end = True - - if key_split == key_start: - first_include_end = first_include_end and include_start - - key_pairs.append((key_start, include_start, - key_split, first_include_end, - KeyRange.DESC)) - - second_include_end = include_end - - if key_split == key_end: - second_include_end = False - key_pairs.append((key_split, False, - key_end, second_include_end, - KeyRange.ASC)) - - ranges = [KeyRange(key_start=start, - include_start=include_start, - key_end=end, - include_end=include_end, - direction=direction, - namespace=self.namespace, - _app=self._app) - for (start, include_start, end, include_end, direction) - in key_pairs] - - return ranges - - def __hash__(self): - return hash([self.key_start, - self.key_end, - self.direction, - self._app, - self.namespace]) - - def __cmp__(self, other): - """Compare two key ranges. - - Key ranges with a value of None for key_start or key_end, are always - considered to have include_start=False or include_end=False, respectively, - when comparing. Since None indicates an unbounded side of the range, - the include specifier is meaningless. The ordering generated is total - but somewhat arbitrary. - - Args: - other: An object to compare to this one. - - Returns: - -1: if this key range is less than other. - 0: if this key range is equal to other. - 1: if this key range is greater than other. - """ - if not isinstance(other, KeyRange): - return 1 - - self_list = [self.key_start, self.key_end, self.direction, - self.include_start, self.include_end, self._app, - self.namespace] - if not self.key_start: - self_list[3] = False - if not self.key_end: - self_list[4] = False - - other_list = [other.key_start, - other.key_end, - other.direction, - other.include_start, - other.include_end, - other._app, - other.namespace] - if not other.key_start: - other_list[3] = False - if not other.key_end: - other_list[4] = False - - return cmp(self_list, other_list) - - @staticmethod - def bisect_string_range(start, end): - """Returns a string that is approximately in the middle of the range. - - (start, end) is treated as a string range, and it is assumed - start <= end in the usual lexicographic string ordering. The output key - mid is guaranteed to satisfy start <= mid <= end. - - The method proceeds by comparing initial characters of start and - end. When the characters are equal, they are appended to the mid - string. In the first place that the characters differ, the - difference characters are averaged and this average is appended to - the mid string. If averaging resulted in rounding down, and - additional character is added to the mid string to make up for the - rounding down. This extra step is necessary for correctness in - the case that the average of the two characters is equal to the - character in the start string. - - This method makes the assumption that most keys are ascii and it - attempts to perform splitting within the ascii range when that - results in a valid split. - - Args: - start: A string. - end: A string such that start <= end. - - Returns: - A string mid such that start <= mid <= end. - """ - if start == end: - return start - start += "\0" - end += "\0" - midpoint = [] - - - expected_max = 127 - for i in xrange(min(len(start), len(end))): - if start[i] == end[i]: - midpoint.append(start[i]) - else: - ord_sum = ord(start[i]) + ord(end[i]) - midpoint.append(unichr(ord_sum / 2)) - if ord_sum % 2: - if len(start) > i + 1: - ord_start = ord(start[i+1]) - else: - ord_start = 0 - if ord_start < expected_max: - - - ord_split = (expected_max + ord_start) / 2 - else: - - ord_split = (0xFFFF + ord_start) / 2 - midpoint.append(unichr(ord_split)) - break - return "".join(midpoint) - - @staticmethod - def split_keys(key_start, key_end, batch_size): - """Return a key that is between key_start and key_end inclusive. - - This method compares components of the ancestor paths of key_start - and key_end. The first place in the path that differs is - approximately split in half. If the kind components differ, a new - non-existent kind halfway between the two is used to split the - space. If the id_or_name components differ, then a new id_or_name - that is halfway between the two is selected. If the lower - id_or_name is numeric and the upper id_or_name is a string, then - the minumum string key u'\0' is used as the split id_or_name. The - key that is returned is the shared portion of the ancestor path - followed by the generated split component. - - Args: - key_start: A db.Key instance for the lower end of a range. - key_end: A db.Key instance for the upper end of a range. - batch_size: The maximum size of a range that should not be split. - - Returns: - A db.Key instance, k, such that key_start <= k <= key_end. - """ - assert key_start.app() == key_end.app() - assert key_start.namespace() == key_end.namespace() - path1 = key_start.to_path() - path2 = key_end.to_path() - len1 = len(path1) - len2 = len(path2) - assert len1 % 2 == 0 - assert len2 % 2 == 0 - out_path = [] - min_path_len = min(len1, len2) / 2 - for i in xrange(min_path_len): - kind1 = path1[2*i] - kind2 = path2[2*i] - - if kind1 != kind2: - split_kind = KeyRange.bisect_string_range(kind1, kind2) - out_path.append(split_kind) - out_path.append(unichr(0)) - break - - - - - last = (len1 == len2 == 2*(i + 1)) - - id_or_name1 = path1[2*i + 1] - id_or_name2 = path2[2*i + 1] - id_or_name_split = KeyRange._split_id_or_name( - id_or_name1, id_or_name2, batch_size, last) - if id_or_name1 == id_or_name_split: - out_path.append(kind1) - out_path.append(id_or_name1) - else: - out_path.append(kind1) - out_path.append(id_or_name_split) - break - - return db.Key.from_path( - *out_path, - **{"_app": key_start.app(), "namespace": key_start.namespace()}) - - @staticmethod - def _split_id_or_name(id_or_name1, id_or_name2, batch_size, maintain_batches): - """Return an id_or_name that is between id_or_name1 an id_or_name2. - - Attempts to split the range [id_or_name1, id_or_name2] in half, - unless maintain_batches is true and the size of the range - [id_or_name1, id_or_name2] is less than or equal to batch_size. - - Args: - id_or_name1: A number or string or the id_or_name component of a key - id_or_name2: A number or string or the id_or_name component of a key - batch_size: The range size that will not be split if maintain_batches - is true. - maintain_batches: A boolean for whether to keep small ranges intact. - - Returns: - An id_or_name such that id_or_name1 <= id_or_name <= id_or_name2. - """ - if (isinstance(id_or_name1, (int, long)) and - isinstance(id_or_name2, (int, long))): - if not maintain_batches or id_or_name2 - id_or_name1 > batch_size: - return (id_or_name1 + id_or_name2) / 2 - else: - return id_or_name1 - elif (isinstance(id_or_name1, basestring) and - isinstance(id_or_name2, basestring)): - return KeyRange.bisect_string_range(id_or_name1, id_or_name2) - else: - if (not isinstance(id_or_name1, (int, long)) or - not isinstance(id_or_name2, basestring)): - raise KeyRangeError("Wrong key order: %r, %r" % - (id_or_name1, id_or_name2)) - - zero_ch = unichr(0) - if id_or_name2 == zero_ch: - return (id_or_name1 + 2**63 - 1) / 2 - return zero_ch - - @staticmethod - def guess_end_key(kind, - key_start, - probe_count=30, - split_rate=5): - """Guess the end of a key range with a binary search of probe queries. - - When the 'key_start' parameter has a key hierarchy, this function will - only determine the key range for keys in a similar hierarchy. That means - if the keys are in the form: - - kind=Foo, name=bar/kind=Stuff, name=meep - - only this range will be probed: - - kind=Foo, name=*/kind=Stuff, name=* - - That means other entities of kind 'Stuff' that are children of another - parent entity kind will be skipped: - - kind=Other, name=cookie/kind=Stuff, name=meep - - Args: - key_start: The starting key of the search range. In most cases this - should be id = 0 or name = '\0'. - kind: String name of the entity kind. - probe_count: Optional, how many probe queries to run. - split_rate: Exponential rate to use for splitting the range on the - way down from the full key space. For smaller ranges this should - be higher so more of the keyspace is skipped on initial descent. - - Returns: - datastore.Key that is guaranteed to be as high or higher than the - highest key existing for this Kind. Doing a query between 'key_start' and - this returned Key (inclusive) will contain all entities of this Kind. - """ - app = key_start.app() - namespace = key_start.namespace() - - full_path = key_start.to_path() - for index, piece in enumerate(full_path): - if index % 2 == 0: - - continue - elif isinstance(piece, basestring): - - full_path[index] = u"\xffff" - else: - - full_path[index] = 2**63 - 1 - - key_end = datastore.Key.from_path(*full_path, - **{"_app": app, "namespace": namespace}) - split_key = key_end - - for i in xrange(probe_count): - for j in xrange(split_rate): - split_key = KeyRange.split_keys(key_start, split_key, 1) - results = datastore.Query( - kind, - {"__key__ >": split_key}, - namespace=namespace, - _app=app, - keys_only=True).Get(1) - if results: - if results[0].name() and not key_start.name(): - - - return KeyRange.guess_end_key( - kind, results[0], probe_count - 1, split_rate) - else: - split_rate = 1 - key_start = results[0] - split_key = key_end - else: - key_end = split_key - - return key_end - - def to_json(self): - """Serialize KeyRange to json. - - Returns: - string with KeyRange json representation. - """ - if simplejson is None: - raise SimplejsonUnavailableError( - "JSON functionality requires simplejson to be available") - - def key_to_str(key): - if key: - return str(key) - else: - return None - - obj_dict = { - "direction": self.direction, - "key_start": key_to_str(self.key_start), - "key_end": key_to_str(self.key_end), - "include_start": self.include_start, - "include_end": self.include_end, - "namespace": self.namespace, - } - if self._app: - obj_dict["_app"] = self._app - - return simplejson.dumps(obj_dict, sort_keys=True) - - - @staticmethod - def from_json(json_str): - """Deserialize KeyRange from its json representation. - - Args: - json_str: string with json representation created by key_range_to_json. - - Returns: - deserialized KeyRange instance. - """ - if simplejson is None: - raise SimplejsonUnavailableError( - "JSON functionality requires simplejson to be available") - - def key_from_str(key_str): - if key_str: - return db.Key(key_str) - else: - return None - - json = simplejson.loads(json_str) - return KeyRange(key_from_str(json["key_start"]), - key_from_str(json["key_end"]), - json["direction"], - json["include_start"], - json["include_end"], - json.get("namespace"), - _app=json.get("_app")) diff --git a/mapreduce/lib/simplejson/README b/mapreduce/lib/simplejson/README deleted file mode 100755 index 6284258..0000000 --- a/mapreduce/lib/simplejson/README +++ /dev/null @@ -1,13 +0,0 @@ -Simplejson library - -The web site is http://undefined.org/python/#simple_json - -This copy was downloaded from -http://pypi.python.org/packages/source/s/simplejson/simplejson-2.0.5.tar.gz - -simplejson is licensed under the MIT open source license. - -Local changes: - -- Changed imports to make mapreduce library hermetic. - diff --git a/mapreduce/lib/simplejson/__init__.py b/mapreduce/lib/simplejson/__init__.py deleted file mode 100755 index 83eec57..0000000 --- a/mapreduce/lib/simplejson/__init__.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env python -r"""A simple, fast, extensible JSON encoder and decoder - -JSON (JavaScript Object Notation) is a subset of -JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data -interchange format. - -simplejson exposes an API familiar to uses of the standard library -marshal and pickle modules. - -Encoding basic Python object hierarchies:: - - >>> import simplejson - >>> simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) - '["foo", {"bar": ["baz", null, 1.0, 2]}]' - >>> print simplejson.dumps("\"foo\bar") - "\"foo\bar" - >>> print simplejson.dumps(u'\u1234') - "\u1234" - >>> print simplejson.dumps('\\') - "\\" - >>> print simplejson.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) - {"a": 0, "b": 0, "c": 0} - >>> from StringIO import StringIO - >>> io = StringIO() - >>> simplejson.dump(['streaming API'], io) - >>> io.getvalue() - '["streaming API"]' - -Compact encoding:: - - >>> import simplejson - >>> compact = simplejson.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) - >>> # Can't assume dict ordering - >>> compact in ('[1,2,3,{"4":5,"6":7}]', '[1,2,3,{"6":7,"4":5}]') - True - -Pretty printing (using repr() because of extraneous whitespace in the output):: - - >>> import simplejson - >>> print repr(simplejson.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4)) - '{\n "4": 5, \n "6": 7\n}' - -Decoding JSON:: - - >>> import simplejson - >>> simplejson.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == ["foo", {"bar":["baz", None, 1.0, 2]}] - True - >>> simplejson.loads('"\\"foo\\bar"') == '"foo\x08ar' - True - >>> from StringIO import StringIO - >>> io = StringIO('["streaming API"]') - >>> simplejson.load(io) == ["streaming API"] - True - -Specializing JSON object decoding:: - - >>> import simplejson - >>> def as_complex(dct): - ... if '__complex__' in dct: - ... return complex(dct['real'], dct['imag']) - ... return dct - ... - >>> simplejson.loads('{"__complex__": true, "real": 1, "imag": 2}', - ... object_hook=as_complex) - (1+2j) - >>> from decimal import Decimal - >>> simplejson.loads('1.1', parse_float=Decimal) == Decimal("1.1") - True - -Extending JSONEncoder:: - - >>> import simplejson - >>> class ComplexEncoder(simplejson.JSONEncoder): - ... def default(self, obj): - ... if isinstance(obj, complex): - ... return [obj.real, obj.imag] - ... return simplejson.JSONEncoder.default(self, obj) - ... - >>> dumps(2 + 1j, cls=ComplexEncoder) - '[2.0, 1.0]' - >>> ComplexEncoder().encode(2 + 1j) - '[2.0, 1.0]' - >>> ''.join(ComplexEncoder().iterencode(2 + 1j)) - '[2.0, 1.0]' - - -Using simplejson from the shell to validate and -pretty-print:: - - $ echo '{"json":"obj"}' | python -msimplejson.tool - { - "json": "obj" - } - $ echo '{ 1.2:3.4}' | python -msimplejson.tool - Expecting property name: line 1 column 2 (char 2) -""" -__version__ = '2.0.5' -__all__ = [ - 'dump', 'dumps', 'load', 'loads', - 'JSONDecoder', 'JSONEncoder', -] - -from decoder import JSONDecoder -from encoder import JSONEncoder - -_default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, -) - -def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, **kw): - """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a - ``.write()``-supporting file-like object). - - If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is ``False``, then the some chunks written to ``fp`` - may be ``unicode`` instances, subject to normal Python ``str`` to - ``unicode`` coercion rules. Unless ``fp.write()`` explicitly - understands ``unicode`` (as in ``codecs.getwriter()``) this is likely - to cause an error. - - If ``check_circular`` is ``False``, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a non-negative integer, then JSON array elements and object - members will be pretty-printed with that indent level. An indent level - of 0 will only insert newlines. ``None`` is the most compact representation. - - If ``separators`` is an ``(item_separator, dict_separator)`` tuple - then it will be used instead of the default ``(', ', ': ')`` separators. - ``(',', ':')`` is the most compact JSON representation. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - - """ - # cached encoder - if (skipkeys is False and ensure_ascii is True and - check_circular is True and allow_nan is True and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and not kw): - iterable = _default_encoder.iterencode(obj) - else: - if cls is None: - cls = JSONEncoder - iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, - default=default, **kw).iterencode(obj) - # could accelerate with writelines in some versions of Python, at - # a debuggability cost - for chunk in iterable: - fp.write(chunk) - - -def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, **kw): - """Serialize ``obj`` to a JSON formatted ``str``. - - If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is ``False``, then the return value will be a - ``unicode`` instance subject to normal Python ``str`` to ``unicode`` - coercion rules instead of being escaped to an ASCII ``str``. - - If ``check_circular`` is ``False``, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a non-negative integer, then JSON array elements and - object members will be pretty-printed with that indent level. An indent - level of 0 will only insert newlines. ``None`` is the most compact - representation. - - If ``separators`` is an ``(item_separator, dict_separator)`` tuple - then it will be used instead of the default ``(', ', ': ')`` separators. - ``(',', ':')`` is the most compact JSON representation. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - - """ - # cached encoder - if (skipkeys is False and ensure_ascii is True and - check_circular is True and allow_nan is True and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and not kw): - return _default_encoder.encode(obj) - if cls is None: - cls = JSONEncoder - return cls( - skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, default=default, - **kw).encode(obj) - - -_default_decoder = JSONDecoder(encoding=None, object_hook=None) - - -def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, **kw): - """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing - a JSON document) to a Python object. - - If the contents of ``fp`` is encoded with an ASCII based encoding other - than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must - be specified. Encodings that are not ASCII based (such as UCS-2) are - not allowed, and should be wrapped with - ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` - object and passed to ``loads()`` - - ``object_hook`` is an optional function that will be called with the - result of any object literal decode (a ``dict``). The return value of - ``object_hook`` will be used instead of the ``dict``. This feature - can be used to implement custom decoders (e.g. JSON-RPC class hinting). - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - - """ - return loads(fp.read(), - encoding=encoding, cls=cls, object_hook=object_hook, - parse_float=parse_float, parse_int=parse_int, - parse_constant=parse_constant, **kw) - - -def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, **kw): - """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON - document) to a Python object. - - If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding - other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name - must be specified. Encodings that are not ASCII based (such as UCS-2) - are not allowed and should be decoded to ``unicode`` first. - - ``object_hook`` is an optional function that will be called with the - result of any object literal decode (a ``dict``). The return value of - ``object_hook`` will be used instead of the ``dict``. This feature - can be used to implement custom decoders (e.g. JSON-RPC class hinting). - - ``parse_float``, if specified, will be called with the string - of every JSON float to be decoded. By default this is equivalent to - float(num_str). This can be used to use another datatype or parser - for JSON floats (e.g. decimal.Decimal). - - ``parse_int``, if specified, will be called with the string - of every JSON int to be decoded. By default this is equivalent to - int(num_str). This can be used to use another datatype or parser - for JSON integers (e.g. float). - - ``parse_constant``, if specified, will be called with one of the - following strings: -Infinity, Infinity, NaN, null, true, false. - This can be used to raise an exception if invalid JSON numbers - are encountered. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - - """ - if (cls is None and encoding is None and object_hook is None and - parse_int is None and parse_float is None and - parse_constant is None and not kw): - return _default_decoder.decode(s) - if cls is None: - cls = JSONDecoder - if object_hook is not None: - kw['object_hook'] = object_hook - if parse_float is not None: - kw['parse_float'] = parse_float - if parse_int is not None: - kw['parse_int'] = parse_int - if parse_constant is not None: - kw['parse_constant'] = parse_constant - return cls(encoding=encoding, **kw).decode(s) diff --git a/mapreduce/lib/simplejson/decoder.py b/mapreduce/lib/simplejson/decoder.py deleted file mode 100755 index 6926ec8..0000000 --- a/mapreduce/lib/simplejson/decoder.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python -"""Implementation of JSONDecoder -""" -import re -import sys -import struct - -from mapreduce.lib.simplejson.scanner import make_scanner -try: - from mapreduce.lib.simplejson._speedups import scanstring as c_scanstring -except ImportError: - c_scanstring = None - -__all__ = ['JSONDecoder'] - -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL - -def _floatconstants(): - _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') - if sys.byteorder != 'big': - _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] - nan, inf = struct.unpack('dd', _BYTES) - return nan, inf, -inf - -NaN, PosInf, NegInf = _floatconstants() - - -def linecol(doc, pos): - lineno = doc.count('\n', 0, pos) + 1 - if lineno == 1: - colno = pos - else: - colno = pos - doc.rindex('\n', 0, pos) - return lineno, colno - - -def errmsg(msg, doc, pos, end=None): - # Note that this function is called from _speedups - lineno, colno = linecol(doc, pos) - if end is None: - return '%s: line %d column %d (char %d)' % (msg, lineno, colno, pos) - endlineno, endcolno = linecol(doc, end) - return '%s: line %d column %d - line %d column %d (char %d - %d)' % ( - msg, lineno, colno, endlineno, endcolno, pos, end) - - -_CONSTANTS = { - '-Infinity': NegInf, - 'Infinity': PosInf, - 'NaN': NaN, -} - -STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) -BACKSLASH = { - '"': u'"', '\\': u'\\', '/': u'/', - 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', -} - -DEFAULT_ENCODING = "utf-8" - -def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): - if encoding is None: - encoding = DEFAULT_ENCODING - chunks = [] - _append = chunks.append - begin = end - 1 - while 1: - chunk = _m(s, end) - if chunk is None: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - end = chunk.end() - content, terminator = chunk.groups() - if content: - if not isinstance(content, unicode): - content = unicode(content, encoding) - _append(content) - if terminator == '"': - break - elif terminator != '\\': - if strict: - raise ValueError(errmsg("Invalid control character %r at", s, end)) - else: - _append(terminator) - continue - try: - esc = s[end] - except IndexError: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - if esc != 'u': - try: - m = _b[esc] - except KeyError: - raise ValueError( - errmsg("Invalid \\escape: %r" % (esc,), s, end)) - end += 1 - else: - esc = s[end + 1:end + 5] - next_end = end + 5 - msg = "Invalid \\uXXXX escape" - try: - if len(esc) != 4: - raise ValueError - uni = int(esc, 16) - if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: - msg = "Invalid \\uXXXX\\uXXXX surrogate pair" - if not s[end + 5:end + 7] == '\\u': - raise ValueError - esc2 = s[end + 7:end + 11] - if len(esc2) != 4: - raise ValueError - uni2 = int(esc2, 16) - uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) - next_end += 6 - m = unichr(uni) - except ValueError: - raise ValueError(errmsg(msg, s, end)) - end = next_end - _append(m) - return u''.join(chunks), end - - -# Use speedup if available -scanstring = c_scanstring or py_scanstring - -WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) -WHITESPACE_STR = ' \t\n\r' - -def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - pairs = {} - nextchar = s[end:end + 1] - # Normally we expect nextchar == '"' - if nextchar != '"': - if nextchar in _ws: - end = _w(s, end).end() - nextchar = s[end:end + 1] - # Trivial empty object - if nextchar == '}': - return pairs, end + 1 - elif nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end)) - end += 1 - while True: - key, end = scanstring(s, end, encoding, strict) - - # To skip some function call overhead we optimize the fast paths where - # the JSON key separator is ": " or just ":". - if s[end:end + 1] != ':': - end = _w(s, end).end() - if s[end:end + 1] != ':': - raise ValueError(errmsg("Expecting : delimiter", s, end)) - - end += 1 - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - try: - value, end = scan_once(s, end) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - pairs[key] = value - - try: - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - end += 1 - - if nextchar == '}': - break - elif nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) - - try: - nextchar = s[end] - if nextchar in _ws: - end += 1 - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - - end += 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end - 1)) - - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end - -def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - values = [] - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - # Look-ahead for trivial empty array - if nextchar == ']': - return values, end + 1 - _append = values.append - while True: - try: - value, end = scan_once(s, end) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - _append(value) - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == ']': - break - elif nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end)) - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - return values, end - -class JSONDecoder(object): - """Simple JSON decoder - - Performs the following translations in decoding by default: - - +---------------+-------------------+ - | JSON | Python | - +===============+===================+ - | object | dict | - +---------------+-------------------+ - | array | list | - +---------------+-------------------+ - | string | unicode | - +---------------+-------------------+ - | number (int) | int, long | - +---------------+-------------------+ - | number (real) | float | - +---------------+-------------------+ - | true | True | - +---------------+-------------------+ - | false | False | - +---------------+-------------------+ - | null | None | - +---------------+-------------------+ - - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as - their corresponding ``float`` values, which is outside the JSON spec. - - """ - - def __init__(self, encoding=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, strict=True): - """``encoding`` determines the encoding used to interpret any ``str`` - objects decoded by this instance (utf-8 by default). It has no - effect when decoding ``unicode`` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as ``unicode``. - - ``object_hook``, if specified, will be called with the result - of every JSON object decoded and its return value will be used in - place of the given ``dict``. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - - ``parse_float``, if specified, will be called with the string - of every JSON float to be decoded. By default this is equivalent to - float(num_str). This can be used to use another datatype or parser - for JSON floats (e.g. decimal.Decimal). - - ``parse_int``, if specified, will be called with the string - of every JSON int to be decoded. By default this is equivalent to - int(num_str). This can be used to use another datatype or parser - for JSON integers (e.g. float). - - ``parse_constant``, if specified, will be called with one of the - following strings: -Infinity, Infinity, NaN. - This can be used to raise an exception if invalid JSON numbers - are encountered. - - """ - self.encoding = encoding - self.object_hook = object_hook - self.parse_float = parse_float or float - self.parse_int = parse_int or int - self.parse_constant = parse_constant or _CONSTANTS.__getitem__ - self.strict = strict - self.parse_object = JSONObject - self.parse_array = JSONArray - self.parse_string = scanstring - self.scan_once = make_scanner(self) - - def decode(self, s, _w=WHITESPACE.match): - """Return the Python representation of ``s`` (a ``str`` or ``unicode`` - instance containing a JSON document) - - """ - obj, end = self.raw_decode(s, idx=_w(s, 0).end()) - end = _w(s, end).end() - if end != len(s): - raise ValueError(errmsg("Extra data", s, end, len(s))) - return obj - - def raw_decode(self, s, idx=0): - """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning - with a JSON document) and return a 2-tuple of the Python - representation and the index in ``s`` where the document ended. - - This can be used to decode a JSON document from a string that may - have extraneous data at the end. - - """ - try: - obj, end = self.scan_once(s, idx) - except StopIteration: - raise ValueError("No JSON object could be decoded") - return obj, end diff --git a/mapreduce/lib/simplejson/encoder.py b/mapreduce/lib/simplejson/encoder.py deleted file mode 100755 index cfec6e6..0000000 --- a/mapreduce/lib/simplejson/encoder.py +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/env python -"""Implementation of JSONEncoder -""" -import re - -try: - from mapreduce.lib.simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii -except ImportError: - c_encode_basestring_ascii = None -try: - from mapreduce.lib.simplejson._speedups import make_encoder as c_make_encoder -except ImportError: - c_make_encoder = None - -ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') -ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') -HAS_UTF8 = re.compile(r'[\x80-\xff]') -ESCAPE_DCT = { - '\\': '\\\\', - '"': '\\"', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', -} -for i in range(0x20): - ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) - -# Assume this produces an infinity on all machines (probably not guaranteed) -INFINITY = float('1e66666') -FLOAT_REPR = repr - -def encode_basestring(s): - """Return a JSON representation of a Python string - - """ - def replace(match): - return ESCAPE_DCT[match.group(0)] - return '"' + ESCAPE.sub(replace, s) + '"' - - -def py_encode_basestring_ascii(s): - if isinstance(s, str) and HAS_UTF8.search(s) is not None: - s = s.decode('utf-8') - def replace(match): - s = match.group(0) - try: - return ESCAPE_DCT[s] - except KeyError: - n = ord(s) - if n < 0x10000: - return '\\u%04x' % (n,) - else: - # surrogate pair - n -= 0x10000 - s1 = 0xd800 | ((n >> 10) & 0x3ff) - s2 = 0xdc00 | (n & 0x3ff) - return '\\u%04x\\u%04x' % (s1, s2) - return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' - - -encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii - -class JSONEncoder(object): - """Extensible JSON encoder for Python data structures. - - Supports the following objects and types by default: - - +-------------------+---------------+ - | Python | JSON | - +===================+===============+ - | dict | object | - +-------------------+---------------+ - | list, tuple | array | - +-------------------+---------------+ - | str, unicode | string | - +-------------------+---------------+ - | int, long, float | number | - +-------------------+---------------+ - | True | true | - +-------------------+---------------+ - | False | false | - +-------------------+---------------+ - | None | null | - +-------------------+---------------+ - - To extend this to recognize other objects, subclass and implement a - ``.default()`` method with another method that returns a serializable - object for ``o`` if possible, otherwise it should call the superclass - implementation (to raise ``TypeError``). - - """ - item_separator = ', ' - key_separator = ': ' - def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False, - indent=None, separators=None, encoding='utf-8', default=None): - """Constructor for JSONEncoder, with sensible defaults. - - If skipkeys is False, then it is a TypeError to attempt - encoding of keys that are not str, int, long, float or None. If - skipkeys is True, such items are simply skipped. - - If ensure_ascii is True, the output is guaranteed to be str - objects with all incoming unicode characters escaped. If - ensure_ascii is false, the output will be unicode object. - - If check_circular is True, then lists, dicts, and custom encoded - objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). - Otherwise, no such check takes place. - - If allow_nan is True, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. - - If sort_keys is True, then the output of dictionaries will be - sorted by key; this is useful for regression tests to ensure - that JSON serializations can be compared on a day-to-day basis. - - If indent is a non-negative integer, then JSON array - elements and object members will be pretty-printed with that - indent level. An indent level of 0 will only insert newlines. - None is the most compact representation. - - If specified, separators should be a (item_separator, key_separator) - tuple. The default is (', ', ': '). To get the most compact JSON - representation you should specify (',', ':') to eliminate whitespace. - - If specified, default is a function that gets called for objects - that can't otherwise be serialized. It should return a JSON encodable - version of the object or raise a ``TypeError``. - - If encoding is not None, then all input strings will be - transformed into unicode using that encoding prior to JSON-encoding. - The default is UTF-8. - - """ - - self.skipkeys = skipkeys - self.ensure_ascii = ensure_ascii - self.check_circular = check_circular - self.allow_nan = allow_nan - self.sort_keys = sort_keys - self.indent = indent - if separators is not None: - self.item_separator, self.key_separator = separators - if default is not None: - self.default = default - self.encoding = encoding - - def default(self, o): - """Implement this method in a subclass such that it returns - a serializable object for ``o``, or calls the base implementation - (to raise a ``TypeError``). - - For example, to support arbitrary iterators, you could - implement default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) - - """ - raise TypeError("%r is not JSON serializable" % (o,)) - - def encode(self, o): - """Return a JSON string representation of a Python data structure. - - >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) - '{"foo": ["bar", "baz"]}' - - """ - # This is for extremely simple cases and benchmarks. - if isinstance(o, basestring): - if isinstance(o, str): - _encoding = self.encoding - if (_encoding is not None - and not (_encoding == 'utf-8')): - o = o.decode(_encoding) - if self.ensure_ascii: - return encode_basestring_ascii(o) - else: - return encode_basestring(o) - # This doesn't pass the iterator directly to ''.join() because the - # exceptions aren't as detailed. The list call should be roughly - # equivalent to the PySequence_Fast that ''.join() would do. - chunks = self.iterencode(o, _one_shot=True) - if not isinstance(chunks, (list, tuple)): - chunks = list(chunks) - return ''.join(chunks) - - def iterencode(self, o, _one_shot=False): - """Encode the given object and yield each string - representation as available. - - For example:: - - for chunk in JSONEncoder().iterencode(bigobject): - mysocket.write(chunk) - - """ - if self.check_circular: - markers = {} - else: - markers = None - if self.ensure_ascii: - _encoder = encode_basestring_ascii - else: - _encoder = encode_basestring - if self.encoding != 'utf-8': - def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): - if isinstance(o, str): - o = o.decode(_encoding) - return _orig_encoder(o) - - def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): - # Check for specials. Note that this type of test is processor- and/or - # platform-specific, so do tests which don't depend on the internals. - - if o != o: - text = 'NaN' - elif o == _inf: - text = 'Infinity' - elif o == _neginf: - text = '-Infinity' - else: - return _repr(o) - - if not allow_nan: - raise ValueError("Out of range float values are not JSON compliant: %r" - % (o,)) - - return text - - - if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: - _iterencode = c_make_encoder( - markers, self.default, _encoder, self.indent, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, self.allow_nan) - else: - _iterencode = _make_iterencode( - markers, self.default, _encoder, self.indent, floatstr, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot) - return _iterencode(o, 0) - -def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, - ## HACK: hand-optimized bytecode; turn globals into locals - False=False, - True=True, - ValueError=ValueError, - basestring=basestring, - dict=dict, - float=float, - id=id, - int=int, - isinstance=isinstance, - list=list, - long=long, - str=str, - tuple=tuple, - ): - - def _iterencode_list(lst, _current_indent_level): - if not lst: - yield '[]' - return - if markers is not None: - markerid = id(lst) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = lst - buf = '[' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - else: - newline_indent = None - separator = _item_separator - first = True - for value in lst: - if first: - first = False - else: - buf = separator - if isinstance(value, basestring): - yield buf + _encoder(value) - elif value is None: - yield buf + 'null' - elif value is True: - yield buf + 'true' - elif value is False: - yield buf + 'false' - elif isinstance(value, (int, long)): - yield buf + str(value) - elif isinstance(value, float): - yield buf + _floatstr(value) - else: - yield buf - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - for chunk in chunks: - yield chunk - if newline_indent is not None: - _current_indent_level -= 1 - yield '\n' + (' ' * (_indent * _current_indent_level)) - yield ']' - if markers is not None: - del markers[markerid] - - def _iterencode_dict(dct, _current_indent_level): - if not dct: - yield '{}' - return - if markers is not None: - markerid = id(dct) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = dct - yield '{' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - item_separator = _item_separator + newline_indent - yield newline_indent - else: - newline_indent = None - item_separator = _item_separator - first = True - if _sort_keys: - items = dct.items() - items.sort(key=lambda kv: kv[0]) - else: - items = dct.iteritems() - for key, value in items: - if isinstance(key, basestring): - pass - # JavaScript is weakly typed for these, so it makes sense to - # also allow them. Many encoders seem to do something like this. - elif isinstance(key, float): - key = _floatstr(key) - elif isinstance(key, (int, long)): - key = str(key) - elif key is True: - key = 'true' - elif key is False: - key = 'false' - elif key is None: - key = 'null' - elif _skipkeys: - continue - else: - raise TypeError("key %r is not a string" % (key,)) - if first: - first = False - else: - yield item_separator - yield _encoder(key) - yield _key_separator - if isinstance(value, basestring): - yield _encoder(value) - elif value is None: - yield 'null' - elif value is True: - yield 'true' - elif value is False: - yield 'false' - elif isinstance(value, (int, long)): - yield str(value) - elif isinstance(value, float): - yield _floatstr(value) - else: - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - for chunk in chunks: - yield chunk - if newline_indent is not None: - _current_indent_level -= 1 - yield '\n' + (' ' * (_indent * _current_indent_level)) - yield '}' - if markers is not None: - del markers[markerid] - - def _iterencode(o, _current_indent_level): - if isinstance(o, basestring): - yield _encoder(o) - elif o is None: - yield 'null' - elif o is True: - yield 'true' - elif o is False: - yield 'false' - elif isinstance(o, (int, long)): - yield str(o) - elif isinstance(o, float): - yield _floatstr(o) - elif isinstance(o, (list, tuple)): - for chunk in _iterencode_list(o, _current_indent_level): - yield chunk - elif isinstance(o, dict): - for chunk in _iterencode_dict(o, _current_indent_level): - yield chunk - else: - if markers is not None: - markerid = id(o) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = o - o = _default(o) - for chunk in _iterencode(o, _current_indent_level): - yield chunk - if markers is not None: - del markers[markerid] - - return _iterencode diff --git a/mapreduce/lib/simplejson/scanner.py b/mapreduce/lib/simplejson/scanner.py deleted file mode 100755 index 201cbc5..0000000 --- a/mapreduce/lib/simplejson/scanner.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python -"""JSON token scanner -""" -import re -try: - from mapreduce.lib.simplejson._speedups import make_scanner as c_make_scanner -except ImportError: - c_make_scanner = None - -__all__ = ['make_scanner'] - -NUMBER_RE = re.compile( - r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', - (re.VERBOSE | re.MULTILINE | re.DOTALL)) - -def py_make_scanner(context): - parse_object = context.parse_object - parse_array = context.parse_array - parse_string = context.parse_string - match_number = NUMBER_RE.match - encoding = context.encoding - strict = context.strict - parse_float = context.parse_float - parse_int = context.parse_int - parse_constant = context.parse_constant - object_hook = context.object_hook - - def _scan_once(string, idx): - try: - nextchar = string[idx] - except IndexError: - raise StopIteration - - if nextchar == '"': - return parse_string(string, idx + 1, encoding, strict) - elif nextchar == '{': - return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) - elif nextchar == '[': - return parse_array((string, idx + 1), _scan_once) - elif nextchar == 'n' and string[idx:idx + 4] == 'null': - return None, idx + 4 - elif nextchar == 't' and string[idx:idx + 4] == 'true': - return True, idx + 4 - elif nextchar == 'f' and string[idx:idx + 5] == 'false': - return False, idx + 5 - - m = match_number(string, idx) - if m is not None: - integer, frac, exp = m.groups() - if frac or exp: - res = parse_float(integer + (frac or '') + (exp or '')) - else: - res = parse_int(integer) - return res, m.end() - elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': - return parse_constant('NaN'), idx + 3 - elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': - return parse_constant('Infinity'), idx + 8 - elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': - return parse_constant('-Infinity'), idx + 9 - else: - raise StopIteration - - return _scan_once - -make_scanner = c_make_scanner or py_make_scanner diff --git a/mapreduce/main.py b/mapreduce/main.py deleted file mode 100755 index c8b0196..0000000 --- a/mapreduce/main.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/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. - -"""Main module for map-reduce implementation. - -This module should be specified as a handler for mapreduce URLs in app.yaml: - - handlers: - - url: /mapreduce(/.*)? - login: admin - script: mapreduce/main.py -""" - -import wsgiref.handlers - -from google.appengine.ext import webapp -from mapreduce import handlers -from mapreduce import status -from google.appengine.ext.webapp import util - - -STATIC_RE = r".*/([^/]*\.(?:css|js)|status|detail)$" - - -class RedirectHandler(webapp.RequestHandler): - """Redirects the user back to the status page.""" - - def get(self): - new_path = self.request.path - if not new_path.endswith("/"): - new_path += "/" - new_path += "status" - self.redirect(new_path) - - -def create_handlers_map(): - """Create new handlers map. - - Returns: - list of (regexp, handler) pairs for WSGIApplication constructor. - """ - return [ - # Task queue handlers. - (r".*/worker_callback", handlers.MapperWorkerCallbackHandler), - (r".*/controller_callback", handlers.ControllerCallbackHandler), - (r".*/kickoffjob_callback", handlers.KickOffJobHandler), - - # RPC requests with JSON responses - # All JSON handlers should have /command/ prefix. - (r".*/command/start_job", handlers.StartJobHandler), - (r".*/command/cleanup_job", handlers.CleanUpJobHandler), - (r".*/command/abort_job", handlers.AbortJobHandler), - (r".*/command/list_configs", status.ListConfigsHandler), - (r".*/command/list_jobs", status.ListJobsHandler), - (r".*/command/get_job_detail", status.GetJobDetailHandler), - - # UI static files - (STATIC_RE, status.ResourceHandler), - - # Redirect non-file URLs that do not end in status/detail to status page. - (r".*", RedirectHandler), - ] - -def create_application(): - """Create new WSGIApplication and register all handlers. - - Returns: - an instance of webapp.WSGIApplication with all mapreduce handlers - registered. - """ - return webapp.WSGIApplication(create_handlers_map(), - debug=True) - - -APP = create_application() - - -def main(): - util.run_wsgi_app(APP) - - -if __name__ == "__main__": - main() diff --git a/mapreduce/migrate.py b/mapreduce/migrate.py deleted file mode 100755 index 24312fe..0000000 --- a/mapreduce/migrate.py +++ /dev/null @@ -1,8 +0,0 @@ -from mapreduce import operation as op -import logging -import appengine_config -from events.models import Event - -def process(entity): - yield op.db.Put(entity) - return diff --git a/mapreduce/model.py b/mapreduce/model.py deleted file mode 100755 index 3e30fa3..0000000 --- a/mapreduce/model.py +++ /dev/null @@ -1,768 +0,0 @@ -#!/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. - -"""Model classes which are used to communicate between parts of implementation. - -These model classes are describing mapreduce, its current state and -communication messages. They are either stored in the datastore or -serialized to/from json and passed around with other means. -""" - -# Disable "Invalid method name" -# pylint: disable-msg=C6409 - - - -__all__ = ["JsonMixin", "JsonProperty", "MapreduceState", "MapperSpec", - "MapreduceControl", "MapreduceSpec", "ShardState", "CountersMap"] - -import copy -import datetime -import logging -import math -import random -from mapreduce.lib import simplejson -import time -import types - -from google.appengine.api import datastore_errors -from google.appengine.api import datastore_types -from google.appengine.ext import db -from mapreduce import context -from mapreduce import hooks -from mapreduce import util -from mapreduce.lib.graphy.backends import google_chart_api - - -# Default rate of processed entities per second. -_DEFAULT_PROCESSING_RATE_PER_SEC = 100 - -# Default number of shards to have. -_DEFAULT_SHARD_COUNT = 8 - - -class JsonMixin(object): - """Simple, stateless json utilities mixin. - - Requires class to implement two methods: - to_json(self): convert data to json-compatible datastructure (dict, - list, strings, numbers) - @classmethod from_json(cls, json): load data from json-compatible structure. - """ - - def to_json_str(self): - """Convert data to json string representation. - - Returns: - json representation as string. - """ - return simplejson.dumps(self.to_json(), sort_keys=True) - - @classmethod - def from_json_str(cls, json_str): - """Convert json string representation into class instance. - - Args: - json_str: json representation as string. - - Returns: - New instance of the class with data loaded from json string. - """ - return cls.from_json(simplejson.loads(json_str)) - - -class JsonProperty(db.UnindexedProperty): - """Property type for storing json representation of data. - - Requires data types to implement two methods: - to_json(self): convert data to json-compatible datastructure (dict, - list, strings, numbers) - @classmethod from_json(cls, json): load data from json-compatible structure. - """ - - def __init__(self, data_type, default=None, **kwargs): - """Constructor. - - Args: - data_type: underlying data type as class. - default: default value for the property. The value is deep copied - fore each model instance. - kwargs: remaining arguments. - """ - kwargs["default"] = default - super(JsonProperty, self).__init__(**kwargs) - self.data_type = data_type - - def get_value_for_datastore(self, model_instance): - """Gets value for datastore. - - Args: - model_instance: instance of the model class. - - Returns: - datastore-compatible value. - """ - value = super(JsonProperty, self).get_value_for_datastore(model_instance) - if not value: - return None - json_value = value.to_json() - if not json_value: - return None - return datastore_types.Text(simplejson.dumps( - json_value, sort_keys=True)) - - def make_value_from_datastore(self, value): - """Convert value from datastore representation. - - Args: - value: datastore value. - - Returns: - value to store in the model. - """ - - if value is None: - return None - return self.data_type.from_json(simplejson.loads(value)) - - def validate(self, value): - """Validate value. - - Args: - value: model value. - - Returns: - Whether the specified value is valid data type value. - - Raises: - BadValueError: when value is not of self.data_type type. - """ - if value is not None and not isinstance(value, self.data_type): - raise datastore_errors.BadValueError( - "Property %s must be convertible to a %s instance (%s)" % - (self.name, self.data_type, value)) - return super(JsonProperty, self).validate(value) - - def empty(self, value): - """Checks if value is empty. - - Args: - value: model value. - - Returns: - True passed value is empty. - """ - return not value - - def default_value(self): - """Create default model value. - - If default option was specified, then it will be deeply copied. - None otherwise. - - Returns: - default model value. - """ - if self.default: - return copy.deepcopy(self.default) - else: - return None - - - -# Ridiculous future UNIX epoch time, 500 years from now. -_FUTURE_TIME = 2**34 - - -def _get_descending_key(gettime=time.time, getrandint=random.randint): - """Returns a key name lexically ordered by time descending. - - This lets us have a key name for use with Datastore entities which returns - rows in time descending order when it is scanned in lexically ascending order, - allowing us to bypass index building for descending indexes. - - Args: - gettime: Used for testing. - getrandint: Used for testing. - - Returns: - A string with a time descending key. - """ - now_descending = int((_FUTURE_TIME - gettime()) * 100) - tie_breaker = getrandint(0, 100) - return "%d%d" % (now_descending, tie_breaker) - - -class CountersMap(JsonMixin): - """Maintains map from counter name to counter value. - - The class is used to provide basic arithmetics of counter values (buil - add/remove), increment individual values and store/load data from json. - """ - - def __init__(self, initial_map=None): - """Constructor. - - Args: - initial_map: initial counter values map from counter name (string) to - counter value (int). - """ - if initial_map: - self.counters = initial_map - else: - self.counters = {} - - def __repr__(self): - """Compute string representation.""" - return "mapreduce.model.CountersMap(%r)" % self.counters - - def get(self, counter_name): - """Get current counter value. - - Args: - counter_name: counter name as string. - - Returns: - current counter value as int. 0 if counter was not set. - """ - return self.counters.get(counter_name, 0) - - def increment(self, counter_name, delta): - """Increment counter value. - - Args: - counter_name: counter name as String. - delta: increment delta as Integer. - - Returns: - new counter value. - """ - current_value = self.counters.get(counter_name, 0) - new_value = current_value + delta - self.counters[counter_name] = new_value - return new_value - - def add_map(self, counters_map): - """Add all counters from the map. - - For each counter in the passed map, adds its value to the counter in this - map. - - Args: - counters_map: CounterMap instance to add. - """ - for counter_name in counters_map.counters: - self.increment(counter_name, counters_map.counters[counter_name]) - - def sub_map(self, counters_map): - """Subtracts all counters from the map. - - For each counter in the passed map, subtracts its value to the counter in - this map. - - Args: - counters_map: CounterMap instance to subtract. - """ - for counter_name in counters_map.counters: - self.increment(counter_name, -counters_map.counters[counter_name]) - - def clear(self): - """Clear all values.""" - self.counters = {} - - def to_json(self): - """Serializes all the data in this map into json form. - - Returns: - json-compatible data representation. - """ - return {"counters": self.counters} - - @classmethod - def from_json(cls, json): - """Create new CountersMap from the json data structure, encoded by to_json. - - Args: - json: json representation of CountersMap . - - Returns: - an instance of CountersMap with all data deserialized from json. - """ - counters_map = cls() - counters_map.counters = json["counters"] - return counters_map - - -class MapperSpec(JsonMixin): - """Contains a specification for the mapper phase of the mapreduce. - - MapperSpec instance can be changed only during mapreduce starting process, - and it remains immutable for the rest of mapreduce execution. MapperSpec is - passed as a payload to all mapreduce tasks in JSON encoding as part of - MapreduceSpec. - - Specifying mapper handlers: - * '.' - __call__ method of class instance will be - called - * '.' - function will be called. - * '..' - class will be instantiated - and method called. - """ - - def __init__(self, handler_spec, input_reader_spec, params, shard_count): - """Creates a new MapperSpec. - - Args: - handler_spec: handler specification as string (see class doc for - details). - input_reader_spec: The class name of the input reader to use. - params: Dictionary of additional parameters for the mapper. - shard_count: number of shards to process in parallel. - - Properties: - handler_spec: name of handler class/function to use. - shard_count: number of shards to process in parallel. - handler: cached instance of mapper handler as callable. - input_reader_spec: The class name of the input reader to use. - params: Dictionary of additional parameters for the mapper. - """ - self.handler_spec = handler_spec - self.__handler = None - self.input_reader_spec = input_reader_spec - self.shard_count = shard_count - self.params = params - - def get_handler(self): - """Get mapper handler instance. - - Returns: - cached handler instance as callable. - """ - if self.__handler is None: - resolved_spec = util.for_name(self.handler_spec) - if isinstance(resolved_spec, type): - # create new instance if this is type - self.__handler = resolved_spec() - elif isinstance(resolved_spec, types.MethodType): - # bind the method - self.__handler = getattr(resolved_spec.im_class(), - resolved_spec.__name__) - else: - self.__handler = resolved_spec - return self.__handler - - handler = property(get_handler) - - def input_reader_class(self): - """Get input reader class. - - Returns: - input reader class object. - """ - return util.for_name(self.input_reader_spec) - - def to_json(self): - """Serializes this MapperSpec into a json-izable object.""" - return { - "mapper_handler_spec": self.handler_spec, - "mapper_input_reader": self.input_reader_spec, - "mapper_params": self.params, - "mapper_shard_count": self.shard_count, - } - - def __str__(self): - return "MapperSpec(%s, %s, %s, %s)" % ( - self.handler_spec, self.input_reader_spec, self.params, - self.shard_count) - - @classmethod - def from_json(cls, json): - """Creates MapperSpec from a dict-like object.""" - return cls(json["mapper_handler_spec"], - json["mapper_input_reader"], - json["mapper_params"], - json["mapper_shard_count"]) - - -class MapreduceSpec(JsonMixin): - """Contains a specification for the whole mapreduce. - - MapreduceSpec instance can be changed only during mapreduce starting process, - and it remains immutable for the rest of mapreduce execution. MapreduceSpec is - passed as a payload to all mapreduce tasks in json encoding. - """ - - # Url to call when mapreduce finishes its execution. - PARAM_DONE_CALLBACK = "done_callback" - # Queue to use to call done callback - PARAM_DONE_CALLBACK_QUEUE = "done_callback_queue" - - def __init__(self, - name, - mapreduce_id, - mapper_spec, - params={}, - hooks_class_name=None): - """Create new MapreduceSpec. - - Args: - name: The name of this mapreduce job type. - mapreduce_id: ID of the mapreduce. - mapper_spec: JSON-encoded string containing a MapperSpec. - params: dictionary of additional mapreduce parameters. - hooks_class_name: The fully qualified name of the hooks class to use. - - Properties: - name: The name of this mapreduce job type. - mapreduce_id: unique id of this mapreduce as string. - mapper: This MapreduceSpec's instance of MapperSpec. - params: dictionary of additional mapreduce parameters. - hooks_class_name: The fully qualified name of the hooks class to use. - """ - self.name = name - self.mapreduce_id = mapreduce_id - self.mapper = MapperSpec.from_json(mapper_spec) - self.params = params - self.hooks_class_name = hooks_class_name - self.__hooks = None - self.get_hooks() # Fail fast on an invalid hook class. - - def get_hooks(self): - """Returns a hooks.Hooks class or None if no hooks class has been set.""" - if self.__hooks is None and self.hooks_class_name is not None: - hooks_class = util.for_name(self.hooks_class_name) - if not isinstance(hooks_class, type): - raise ValueError("hooks_class_name must refer to a class, got %s" % - type(hooks_class).__name__) - if not issubclass(hooks_class, hooks.Hooks): - raise ValueError( - "hooks_class_name must refer to a hooks.Hooks subclass") - self.__hooks = hooks_class(self.mapper) - - return self.__hooks - - def to_json(self): - """Serializes all data in this mapreduce spec into json form. - - Returns: - data in json format. - """ - mapper_spec = self.mapper.to_json() - return { - "name": self.name, - "mapreduce_id": self.mapreduce_id, - "mapper_spec": mapper_spec, - "params": self.params, - "hooks_class_name": self.hooks_class_name, - } - - @classmethod - def from_json(cls, json): - """Create new MapreduceSpec from the json, encoded by to_json. - - Args: - json: json representation of MapreduceSpec. - - Returns: - an instance of MapreduceSpec with all data deserialized from json. - """ - mapreduce_spec = cls(json["name"], - json["mapreduce_id"], - json["mapper_spec"], - json.get("params"), - json.get("hooks_class_name")) - return mapreduce_spec - - -class MapreduceState(db.Model): - """Holds accumulated state of mapreduce execution. - - MapreduceState is stored in datastore with a key name equal to the - mapreduce ID. Only controller tasks can write to MapreduceState. - - Properties: - mapreduce_spec: cached deserialized MapreduceSpec instance. read-only - active: if we have this mapreduce running right now - last_poll_time: last time controller job has polled this mapreduce. - counters_map: shard's counters map as CountersMap. Mirrors - counters_map_json. - chart_url: last computed mapreduce status chart url. This chart displays the - progress of all the shards the best way it can. - sparkline_url: last computed mapreduce status chart url in small format. - result_status: If not None, the final status of the job. - active_shards: How many shards are still processing. - start_time: When the job started. - """ - - RESULT_SUCCESS = "success" - RESULT_FAILED = "failed" - RESULT_ABORTED = "aborted" - - _RESULTS = frozenset([RESULT_SUCCESS, RESULT_FAILED, RESULT_ABORTED]) - - # Functional properties. - mapreduce_spec = JsonProperty(MapreduceSpec, indexed=False) - active = db.BooleanProperty(default=True, indexed=False) - last_poll_time = db.DateTimeProperty(required=True) - counters_map = JsonProperty(CountersMap, default=CountersMap(), indexed=False) - app_id = db.StringProperty(required=False, indexed=True) - - # For UI purposes only. - chart_url = db.TextProperty(default="") - sparkline_url = db.TextProperty(default="") - result_status = db.StringProperty(required=False, choices=_RESULTS) - active_shards = db.IntegerProperty(default=0, indexed=False) - failed_shards = db.IntegerProperty(default=0, indexed=False) - aborted_shards = db.IntegerProperty(default=0, indexed=False) - start_time = db.DateTimeProperty(auto_now_add=True) - - @classmethod - def kind(cls): - """Returns entity kind.""" - return "_AE_MR_MapreduceState" - - @classmethod - def get_key_by_job_id(cls, mapreduce_id): - """Retrieves the Key for a Job. - - Args: - mapreduce_id: The job to retrieve. - - Returns: - Datastore Key that can be used to fetch the MapreduceState. - """ - return db.Key.from_path(cls.kind(), mapreduce_id) - - @classmethod - def get_by_job_id(cls, mapreduce_id): - """Retrieves the instance of state for a Job. - - Args: - mapreduce_id: The mapreduce job to retrieve. - - Returns: - instance of MapreduceState for passed id. - """ - return db.get(cls.get_key_by_job_id(mapreduce_id)) - - def set_processed_counts(self, shards_processed): - """Updates a chart url to display processed count for each shard. - - Args: - shards_processed: list of integers with number of processed entities in - each shard - """ - chart = google_chart_api.BarChart(shards_processed) - if self.mapreduce_spec and shards_processed: - chart.bottom.labels = [ - str(x) for x in xrange(self.mapreduce_spec.mapper.shard_count)] - chart.left.labels = ['0', str(max(shards_processed))] - chart.left.min = 0 - self.chart_url = chart.display.Url(300, 200) - - def get_processed(self): - """Number of processed entities. - - Returns: - The total number of processed entities as int. - """ - return self.counters_map.get(context.COUNTER_MAPPER_CALLS) - - processed = property(get_processed) - - @staticmethod - def create_new(mapreduce_id=None, - gettime=datetime.datetime.now): - """Create a new MapreduceState. - - Args: - mapreduce_id: Mapreduce id as string. - gettime: Used for testing. - """ - if not mapreduce_id: - mapreduce_id = MapreduceState.new_mapreduce_id() - state = MapreduceState(key_name=mapreduce_id, - last_poll_time=gettime()) - state.set_processed_counts([]) - return state - - @staticmethod - def new_mapreduce_id(): - """Generate new mapreduce id.""" - return _get_descending_key() - - -class ShardState(db.Model): - """Single shard execution state. - - The shard state is stored in the datastore and is later aggregated by - controller task. Shard key_name is equal to shard_id. - - Properties: - active: if we have this shard still running as boolean. - counters_map: shard's counters map as CountersMap. Mirrors - counters_map_json. - mapreduce_id: unique id of the mapreduce. - shard_id: unique id of this shard as string. - shard_number: ordered number for this shard. - result_status: If not None, the final status of this shard. - update_time: The last time this shard state was updated. - shard_description: A string description of the work this shard will do. - last_work_item: A string description of the last work item processed. - """ - - RESULT_SUCCESS = "success" - RESULT_FAILED = "failed" - RESULT_ABORTED = "aborted" - - _RESULTS = frozenset([RESULT_SUCCESS, RESULT_FAILED, RESULT_ABORTED]) - - # Functional properties. - active = db.BooleanProperty(default=True, indexed=False) - counters_map = JsonProperty(CountersMap, default=CountersMap(), indexed=False) - result_status = db.StringProperty(choices=_RESULTS, indexed=False) - - # For UI purposes only. - mapreduce_id = db.StringProperty(required=True) - update_time = db.DateTimeProperty(auto_now=True, indexed=False) - shard_description = db.TextProperty(default="") - last_work_item = db.TextProperty(default="") - - def get_shard_number(self): - """Gets the shard number from the key name.""" - return int(self.key().name().split("-")[-1]) - - shard_number = property(get_shard_number) - - def get_shard_id(self): - """Returns the shard ID.""" - return self.key().name() - - shard_id = property(get_shard_id) - - @classmethod - def kind(cls): - """Returns entity kind.""" - return "_AE_MR_ShardState" - - @classmethod - def shard_id_from_number(cls, mapreduce_id, shard_number): - """Get shard id by mapreduce id and shard number. - - Args: - mapreduce_id: mapreduce id as string. - shard_number: shard number to compute id for as int. - - Returns: - shard id as string. - """ - return "%s-%d" % (mapreduce_id, shard_number) - - @classmethod - def get_key_by_shard_id(cls, shard_id): - """Retrieves the Key for this ShardState. - - Args: - shard_id: The shard ID to fetch. - - Returns: - The Datatore key to use to retrieve this ShardState. - """ - return db.Key.from_path(cls.kind(), shard_id) - - @classmethod - def get_by_shard_id(cls, shard_id): - """Get shard state from datastore by shard_id. - - Args: - shard_id: shard id as string. - - Returns: - ShardState for given shard id or None if it's not found. - """ - return cls.get_by_key_name(shard_id) - - @classmethod - def find_by_mapreduce_id(cls, mapreduce_id): - """Find all shard states for given mapreduce. - - Args: - mapreduce_id: mapreduce id. - - Returns: - iterable of all ShardState for given mapreduce id. - """ - return cls.all().filter("mapreduce_id =", mapreduce_id).fetch(99999) - - @classmethod - def create_new(cls, mapreduce_id, shard_number): - """Create new shard state. - - Args: - mapreduce_id: unique mapreduce id as string. - shard_number: shard number for which to create shard state. - - Returns: - new instance of ShardState ready to put into datastore. - """ - shard_id = cls.shard_id_from_number(mapreduce_id, shard_number) - state = cls(key_name=shard_id, - mapreduce_id=mapreduce_id) - return state - - -class MapreduceControl(db.Model): - """Datastore entity used to control mapreduce job execution. - - Only one command may be sent to jobs at a time. - - Properties: - command: The command to send to the job. - """ - - ABORT = "abort" - - _COMMANDS = frozenset([ABORT]) - _KEY_NAME = "command" - - command = db.TextProperty(choices=_COMMANDS, required=True) - - @classmethod - def kind(cls): - """Returns entity kind.""" - return "_AE_MR_MapreduceControl" - - @classmethod - def get_key_by_job_id(cls, mapreduce_id): - """Retrieves the Key for a mapreduce ID. - - Args: - mapreduce_id: The job to fetch. - - Returns: - Datastore Key for the command for the given job ID. - """ - return db.Key.from_path(cls.kind(), "%s:%s" % (mapreduce_id, cls._KEY_NAME)) - - @classmethod - def abort(cls, mapreduce_id): - """Causes a job to abort. - - Args: - mapreduce_id: The job to abort. Not verified as a valid job. - """ - cls(key_name="%s:%s" % (mapreduce_id, cls._KEY_NAME), - command=cls.ABORT).put() diff --git a/mapreduce/operation/__init__.py b/mapreduce/operation/__init__.py deleted file mode 100755 index f645b87..0000000 --- a/mapreduce/operation/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/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. - -"""Operations which can be yielded from mappers. - -Operation is callable that takes context.Context as a parameter. -Operations are called during mapper execution immediately -on recieving from handler function. -""" - - - -import db -import counters - -__all__ = ['db', 'counters'] diff --git a/mapreduce/operation/counters.py b/mapreduce/operation/counters.py deleted file mode 100755 index 9cbfe1c..0000000 --- a/mapreduce/operation/counters.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/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. - -"""Counters-related operations.""" - - - -__all__ = ['Increment'] - - -class Increment(object): - """Increment counter operation.""" - - def __init__(self, counter_name, delta=1): - """Constructor. - - Args: - counter_name: name of the counter as string - delta: increment delta as int. - """ - self.counter_name = counter_name - self.delta = delta - - def __call__(self, context): - """Execute operation. - - Args: - context: mapreduce context as context.Context. - """ - context.counters.increment(self.counter_name, self.delta) diff --git a/mapreduce/operation/db.py b/mapreduce/operation/db.py deleted file mode 100755 index fbde66f..0000000 --- a/mapreduce/operation/db.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/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. - -"""DB-related operations.""" - - - -__all__ = ['Put', 'Delete'] - - -# TODO(user): handler function annotation which requests to -# use db calls directly without batching them/doing async db calls. -class Put(object): - """Put entity into datastore via mutation_pool. - - See mapreduce.context.MutationPool. - """ - - def __init__(self, entity): - """Constructor. - - Args: - entity: an entity to put. - """ - self.entity = entity - - def __call__(self, context): - """Perform operation. - - Args: - context: mapreduce context as context.Context. - """ - context.mutation_pool.put(self.entity) - - -class Delete(object): - """Delete entity from datastore via mutation_pool. - - See mapreduce.context.MutationPool. - """ - - def __init__(self, entity): - """Constructor. - - Args: - entity: a key or model instance to delete. - """ - self.entity = entity - - def __call__(self, context): - """Perform operation. - - Args: - context: mapreduce context as context.Context. - """ - context.mutation_pool.delete(self.entity) diff --git a/mapreduce/quota.py b/mapreduce/quota.py deleted file mode 100755 index bbdc39a..0000000 --- a/mapreduce/quota.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/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. - -"""Simple quota system backed by memcache storage.""" - - - - -# Memcache namespace to use. -_QUOTA_NAMESPACE = "quota" - -# Offset all quota values by this amount since memcache incr/decr -# operate only with unsigned values. -_OFFSET = 2**32 - - -class QuotaManager(object): - """Simple quota system manager, backed by memcache storage. - - Since memcache storage is not reliable, this quota system is not reliable and - best effort only. - - Quota is managed by buckets. Each bucket contains a 32-bit int value of - available quota. Buckets should be refilled manually with 'put' method. - - It is safe to use a single bucket from multiple clients simultaneously. - """ - - def __init__(self, memcache_client): - """Initialize new instance. - - Args: - memcache_client: an instance of memcache client to use. - """ - self.memcache_client = memcache_client - - def put(self, bucket, amount): - """Put amount into quota bucket. - - Args: - bucket: quota bucket as string. - amount: amount to bit put into quota as int. - """ - self.memcache_client.incr(bucket, delta=amount, - initial_value=_OFFSET, namespace=_QUOTA_NAMESPACE) - - def consume(self, bucket, amount, consume_some=False): - """Consume amount from quota bucket. - - Args: - bucket: quota bucket as string. - amount: amount to consume. - consume_some: specifies behavior in case of not enough quota. If False, - the method will leave quota intact and return 0. If True, will try to - consume as much as possible. - - Returns: - Amount of quota consumed. - """ - new_quota = self.memcache_client.decr( - bucket, delta=amount, initial_value=_OFFSET, namespace=_QUOTA_NAMESPACE) - - if new_quota >= _OFFSET: - return amount - - if consume_some and new_quota is not None and _OFFSET - new_quota < amount: - # we still can consume some - self.put(bucket, _OFFSET - new_quota) - return amount - (_OFFSET - new_quota) - else: - self.put(bucket, amount) - return 0 - - def get(self, bucket): - """Get current bucket amount. - - Args: - bucket: quota bucket as string. - - Returns: - current bucket amount as int. - """ - amount = self.memcache_client.get(bucket, namespace=_QUOTA_NAMESPACE) - if amount: - return int(amount) - _OFFSET - else: - return 0 - - def set(self, bucket, amount): - """Set bucket amount. - - Args: - bucket: quota bucket as string. - amount: new bucket amount as int. - """ - self.memcache_client.set(bucket, amount + _OFFSET, - namespace=_QUOTA_NAMESPACE) - - -class QuotaConsumer(object): - """Quota consumer wrapper for efficient quota consuming/reclaiming. - - Quota is consumed in batches and put back in dispose() method. - - WARNING: Always call the dispose() method if you need to keep quota - consistent. - """ - - def __init__(self, quota_manager, bucket, batch_size): - """Initialize new instance. - - Args: - quota_manager: quota manager to use for quota operations as QuotaManager. - bucket: quota bucket name as string. - batch_size: batch size for quota consuming as int. - """ - self.quota_manager = quota_manager - self.batch_size = batch_size - self.bucket = bucket - self.quota = 0 - - def consume(self, amount=1): - """Consume quota. - - Args: - amount: amount of quota to be consumed as int. - - Returns: - True if quota was successfully consumed, False if there's not enough - quota. - """ - while self.quota < amount: - delta = self.quota_manager.consume(self.bucket, self.batch_size, - consume_some=True) - if not delta: - return False - self.quota += delta - - self.quota -= amount - return True - - def put(self, amount=1): - """Put quota back. - - Args: - amount: amount of quota as int. - """ - self.quota += amount - - def check(self, amount=1): - """Check that we have enough quota right now. - - This doesn't lock or consume the quota. Following consume might in fact - fail/succeeded. - - Args: - amount: amount of quota to check. - - Returns: - True if we have enough quota to consume specified amount right now. False - otherwise. - """ - if self.quota >= amount: - return True - return self.quota + self.quota_manager.get(self.bucket) >= amount - - def dispose(self): - """Dispose QuotaConsumer and put all actually unconsumed quota back. - - This method has to be called for quota consistency! - """ - self.quota_manager.put(self.bucket, self.quota) diff --git a/mapreduce/static/base.css b/mapreduce/static/base.css deleted file mode 100755 index 0fca75b..0000000 --- a/mapreduce/static/base.css +++ /dev/null @@ -1,113 +0,0 @@ -html { - margin: 0; - padding: 0; - font-family: Arial, sans-serif; - font-size: 13px; -} - -body { - margin: 0; - padding: 0 3px 3px 3px; -} - -#butter { - position: absolute; - left: 40%; /* todo: actually center this */ - width: 200px; - background-color: #C5D7EF; - text-align: center; - padding: 5px; - border-left: 1px solid #3366CC; - border-right: 1px solid #3366CC; - border-bottom: 1px solid #3366CC; -} - -h1 { - margin-top: 0; - margin-bottom: 0.4em; - font-size: 2em; -} -h2 { - margin-top: 1em; - margin-bottom: 0.4em; - font-size: 1.2em; -} -h3 { - margin-top: 0; - margin-bottom: 0.7em; - font-size: 1.0em; -} - -.status-text { - text-transform: capitalize; -} - -/* Overview page */ -.editable-input, -.job-static-param { - margin: 0.3em; -} - -.editable-input > label:after { - content: ': '; -} - -#launch-control { - margin-bottom: 0.5em; -} -#launch-container { - margin-left: 0.5em; -} - -/* Detail page */ -#control { - float: right; -} - -#detail-graph, -#aggregated-counters-container, -#detail-params-container { - margin-left: 1em; - float: left; -} - -/* Shared */ -.param-key:after { - content: ': '; -} -.user-param-key:after { - content: ': '; -} -.param-aux:before { - content: ' '; -} - -.status-table { - margin: 5px; - border-collapse: collapse; - border-width: 0; - empty-cells: show; - border-top: 1px solid #C5D7EF; - border-left: 1px solid #C5D7EF; - border-right: 1px solid #C5D7EF; -} - -.status-table > thead { - height: 2em; -} - -.status-table > tfoot { - height: 1em; -} - -.status-table > thead, -.status-table > tfoot { - background-color: #E5ECF9; -} - -.status-table td { - padding: 4px; - border-left: 1px solid #C5D7EF; - border-bottom: 1px solid #C5D7EF; - border-top: 1px solid #C5D7EF; -} diff --git a/mapreduce/static/detail.html b/mapreduce/static/detail.html deleted file mode 100755 index 25b5d2b..0000000 --- a/mapreduce/static/detail.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - Loading Job Status... - - - - - - - - - -
- « Back to Overview - | - - - -
- -

Loading Job Status...

-

- -
- -
- -
-

Overview

-
    -
    - -
    -

    Counters

    -
      -
      - -
      - -
      -

      Mapper status

      - - - - - - - - - - - - -
      ShardStatusDescriptionLast work itemTime elapsed
      -
      -
      - - - diff --git a/mapreduce/static/jquery-1.4.2.min.js b/mapreduce/static/jquery-1.4.2.min.js deleted file mode 100755 index 7c24308..0000000 --- a/mapreduce/static/jquery-1.4.2.min.js +++ /dev/null @@ -1,154 +0,0 @@ -/*! - * jQuery JavaScript Library v1.4.2 - * http://jquery.com/ - * - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2010, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Sat Feb 13 22:33:48 2010 -0500 - */ -(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, -Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& -(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, -a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== -"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, -function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
      a"; -var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, -parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= -false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= -s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, -applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; -else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, -a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== -w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, -cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= -c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); -a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, -function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); -k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), -C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= -e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& -f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; -if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", -e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, -"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, -d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, -e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); -t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| -g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, -CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, -g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, -text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, -setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= -h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== -"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, -h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& -q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; -if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

      ";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); -(function(){var g=s.createElement("div");g.innerHTML="
      ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: -function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= -{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== -"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", -d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? -a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== -1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
      ","
      "],thead:[1,"","
      "],tr:[2,"","
      "],td:[3,"","
      "],col:[2,"","
      "],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
      ","
      "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= -c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, -wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, -prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, -this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); -return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, -""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); -return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", -""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= -c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? -c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= -function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= -Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, -"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= -a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= -a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== -"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
      ").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, -serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), -function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, -global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& -e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? -"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== -false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= -false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", -c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| -d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); -g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== -1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== -"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; -if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== -"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| -c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; -this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= -this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, -e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
      "; -a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); -c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, -d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- -f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": -"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in -e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/mapreduce/static/overview.html b/mapreduce/static/overview.html deleted file mode 100755 index 8063ffc..0000000 --- a/mapreduce/static/overview.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - MapReduce Overview - - - - - - - - - -

      MapReduce Overview

      - -
      -

      Running jobs

      - - - - - - - - - - - - - - - - - - - - - - - - - -
      StatusViewIDNameActivityStart timeTime elapsedControl
      - - -
      Loading...
      -
      - - -
      -

      Launch job

      -
      - Loading... -
      -
      -
      -
      - - - - - diff --git a/mapreduce/static/status.js b/mapreduce/static/status.js deleted file mode 100755 index b2ad852..0000000 --- a/mapreduce/static/status.js +++ /dev/null @@ -1,602 +0,0 @@ -/* - * 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. - */ - -/********* Common functions *********/ - -// Sets the status butter, optionally indicating if it's an error message. -function setButter(message, error) { - var butter = $("#butter"); - // Prevent flicker on butter update by hiding it first. - butter.css('display', 'none'); - if (error) { - butter.removeClass('info').addClass('error').text(message); - } else { - butter.removeClass('error').addClass('info').text(message); - } - butter.css('display', null) - $(document).scrollTop(0); -} - -// Given an AJAX error message (which is empty or null on success) and a -// data payload containing JSON, parses the data payload and returns the object. -// Server-side errors and AJAX errors will be brought to the user's attention -// if present in the response object -function getResponseDataJson(error, data) { - var response = null; - try { - response = $.parseJSON(data); - } catch (e) { - error = '' + e; - } - if (response && response.error_class) { - error = response.error_class + ': ' + response.error_message; - } else if (!response) { - error = 'Could not parse response JSON data.'; - } - if (error) { - setButter('Error -- ' + error, true); - return null; - } - return response; -} - -// Retrieve the list of configs. -function listConfigs(resultFunc) { - $.ajax({ - type: 'GET', - url: 'command/list_configs', - dataType: 'text', - error: function(request, textStatus) { - getResponseDataJson(textStatus); - }, - success: function(data, textStatus, request) { - var response = getResponseDataJson(null, data); - if (response) { - resultFunc(response.configs); - } - } - }); -} - -// Return the list of job records. -function listJobs(cursor, resultFunc) { - $.ajax({ - type: 'GET', - url: 'command/list_jobs', - dataType: 'text', - error: function(request, textStatus) { - getResponseDataJson(textStatus); - }, - success: function(data, textStatus, request) { - var response = getResponseDataJson(null, data); - if (response) { - resultFunc(response.jobs, response.cursor); - } - } - }); -} - -// Cleans up a job with the given name and ID, updates butter with status. -function cleanUpJob(name, mapreduce_id) { - if (!confirm('Clean up job "' + name + - '" with ID "' + mapreduce_id + '"?')) { - return; - } - - $.ajax({ - async: false, - type: 'POST', - url: 'command/cleanup_job', - data: {'mapreduce_id': mapreduce_id}, - dataType: 'text', - error: function(request, textStatus) { - getResponseDataJson(textStatus); - }, - success: function(data, textStatus, request) { - var response = getResponseDataJson(null, data); - if (response) { - setButter(response.status); - if (!response.status.error) { - $('#row-' + mapreduce_id).remove(); - } - } - } - }); -} - -// Aborts the job with the given ID, updates butter with status. -function abortJob(name, mapreduce_id) { - if (!confirm('Abort job "' + name + '" with ID "' + mapreduce_id + '"?')) { - return; - } - - $.ajax({ - async: false, - type: 'POST', - url: 'command/abort_job', - data: {'mapreduce_id': mapreduce_id}, - dataType: 'text', - error: function(request, textStatus) { - getResponseDataJson(textStatus); - }, - success: function(data, textStatus, request) { - var response = getResponseDataJson(null, data); - if (response) { - setButter(response.status); - } - } - }); -} - -// Retrieve the detail for a job. -function getJobDetail(jobId, resultFunc) { - $.ajax({ - type: 'GET', - url: 'command/get_job_detail', - dataType: 'text', - data: {'mapreduce_id': jobId}, - error: function(request, textStatus) { - getResponseDataJson(textStatus); - }, - success: function(data, textStatus, request) { - var response = getResponseDataJson(null, data); - if (response) { - resultFunc(jobId, response); - } - } - }); -} - -// Turns a key into a nicely scrubbed parameter name. -function getNiceParamKey(key) { - // TODO: Figure out if we want to do this at all. - return key; -} - -// Returns an array of the keys of an object in sorted order. -function getSortedKeys(obj) { - var keys = []; - $.each(obj, function(key, value) { - keys.push(key); - }); - keys.sort(); - return keys; -} - -// Gets a local datestring from a UNIX timestamp in milliseconds. -function getLocalTimestring(timestamp_ms) { - var when = new Date(); - when.setTime(timestamp_ms); - return when.toLocaleString(); -} - -function leftPadNumber(number, minSize, paddingChar) { - var stringified = '' + number; - if (stringified.length < minSize) { - for (var i = 0; i < (minSize - stringified.length); ++i) { - stringified = paddingChar + stringified; - } - } - return stringified; -} - -// Get locale time string for time portion of job runtime. Specially -// handle number of days running as a prefix. -function getElapsedTimeString(start_timestamp_ms, updated_timestamp_ms) { - var updatedDiff = updated_timestamp_ms - start_timestamp_ms; - var updatedDays = Math.floor(updatedDiff / 86400000.0); - updatedDiff -= (updatedDays * 86400000.0); - var updatedHours = Math.floor(updatedDiff / 3600000.0); - updatedDiff -= (updatedHours * 3600000.0); - var updatedMinutes = Math.floor(updatedDiff / 60000.0); - updatedDiff -= (updatedMinutes * 60000.0); - var updatedSeconds = Math.floor(updatedDiff / 1000.0); - - var updatedString = ''; - if (updatedDays == 1) { - updatedString = '1 day, '; - } else if (updatedDays > 1) { - updatedString = '' + updatedDays + ' days, '; - } - updatedString += - leftPadNumber(updatedHours, 2, '0') + ':' + - leftPadNumber(updatedMinutes, 2, '0') + ':' + - leftPadNumber(updatedSeconds, 2, '0'); - - return updatedString; -} - -// Retrieves the mapreduce_id from the query string. Assumes that it is -// the only querystring parameter. -function getJobId() { - var index = window.location.search.lastIndexOf("="); - if (index == -1) { - return ''; - } - return decodeURIComponent(window.location.search.substr(index+1)); -} - -/********* Specific to overview status page *********/ - -//////// Running jobs overview. -function initJobOverview(jobs, cursor) { - // Empty body. - var body = $('#running-list > tbody'); - body.empty(); - - if (!jobs || (jobs && jobs.length == 0)) { - $('').text("No job records found.").appendTo(body); - return; - } - - // Show header. - $('#running-list > thead').css('display', null); - - // Populate the table. - $.each(jobs, function(index, job) { - var row = $(''); - - // TODO: Style running colgroup for capitalization. - var status = (job.active ? 'running' : job.result_status) || 'unknown'; - row.append($('').text(status)); - - $('').append( - $('') - .attr('href', 'detail?mapreduce_id=' + job.mapreduce_id) - .text('Detail')).appendTo(row); - - row.append($('').text(job.mapreduce_id)) - .append($('').text(job.name)); - - var activity = '' + job.active_shards + ' / ' + job.shards + ' shards'; - row.append($('').text(activity)) - - row.append($('').text(getLocalTimestring(job.start_timestamp_ms))); - - row.append($('').text(getElapsedTimeString( - job.start_timestamp_ms, job.updated_timestamp_ms))); - - // Controller links for abort, cleanup, etc. - if (job.active) { - var control = $('').text('Abort') - .click(function(event) { - abortJob(job.name, job.mapreduce_id); - event.stopPropagation(); - return false; - }); - row.append($('').append(control)); - } else { - var control = $('').text('Cleanup') - .click(function(event) { - cleanUpJob(job.name, job.mapreduce_id); - event.stopPropagation(); - return false; - }); - row.append($('').append(control)); - } - row.appendTo(body); - }); - - // Set up the next/first page links. - $('#running-first-page') - .css('display', null) - .unbind('click') - .click(function() { - listJobs(null, initJobOverview); - return false; - }); - $('#running-next-page').unbind('click'); - if (cursor) { - $('#running-next-page') - .css('display', null) - .click(function() { - listJobs(cursor, initJobOverview); - return false; - }); - } else { - $('#running-next-page').css('display', 'none'); - } - $('#running-list > tfoot').css('display', null); -} - -//////// Launching jobs. - -var FIXED_JOB_PARAMS = [ - 'name', 'mapper_input_reader', 'mapper_handler', 'mapper_params_validator' -]; - -var EDITABLE_JOB_PARAMS = ['shard_count', 'processing_rate', 'queue_name']; - -function getJobForm(name) { - return $('form.run-job > input[name="name"][value="' + name + '"]').parent(); -} - -function showRunJobConfig(name) { - var matchedForm = null; - $.each($('form.run-job'), function(index, jobForm) { - if ($(jobForm).find('input[name="name"]').val() == name) { - matchedForm = jobForm; - } else { - $(jobForm).css('display', 'none'); - } - }); - $(matchedForm).css('display', null); -} - -function runJobDone(name, error, data) { - var jobForm = getJobForm(name); - var response = getResponseDataJson(error, data); - if (response) { - setButter('Successfully started job "' + response['mapreduce_id'] + '"'); - listJobs(null, initJobOverview); - } - jobForm.find('input[type="submit"]').attr('disabled', null); -} - -function runJob(name) { - var jobForm = getJobForm(name); - jobForm.find('input[type="submit"]').attr('disabled', 'disabled'); - $.ajax({ - type: 'POST', - url: 'command/start_job', - data: jobForm.serialize(), - dataType: 'text', - error: function(request, textStatus) { - runJobDone(name, textStatus); - }, - success: function(data, textStatus, request) { - runJobDone(name, null, data); - } - }); -} - -function initJobLaunching(configs) { - $('#launch-control').empty(); - if (!configs || (configs && configs.length == 0)) { - $('#launch-control').append('No job configurations found.'); - return; - } - - // Set up job config forms. - $.each(configs, function(index, config) { - var jobForm = $('
      ') - .submit(function() { - runJob(config.name); - return false; - }) - .css('display', 'none') - .appendTo("#launch-container"); - - // Fixed job config values. - $.each(FIXED_JOB_PARAMS, function(unused, key) { - var value = config[key]; - if (!value) return; - if (key != 'name') { - // Name is up in the page title so doesn't need to be shown again. - $('

      ') - .append($('').text(getNiceParamKey(key))) - .append($('').text(value)) - .appendTo(jobForm); - } - $('') - .attr('name', key) - .attr('value', value) - .appendTo(jobForm); - }); - - // Add parameter values to the job form. - function addParameters(params, prefix) { - if (!params) { - return; - } - - var sortedParams = getSortedKeys(params); - $.each(sortedParams, function(index, key) { - var value = params[key]; - var paramId = 'job-' + prefix + key + '-param'; - var paramP = $('

      '); - - // Deal with the case in which the value is an object rather than - // just the default value string. - var prettyKey = key; - if (value && value["human_name"]) { - prettyKey = value["human_name"]; - } - - if (value && value["default_value"]) { - value = value["default_value"]; - } - - $('Calendars
      - - -

        -{% for calendar in calendars %} -
      • {{calendar.title}}(edit) -
        - - -
        -
      • - - -{% else %} - -
      • No calendars
      • -{% endfor %} -
      - -{% endblock %} \ No newline at end of file diff --git a/django_templates/email/manage_subscription.txt b/templates/email/manage_subscription.txt similarity index 100% rename from django_templates/email/manage_subscription.txt rename to templates/email/manage_subscription.txt diff --git a/django_templates/email/verify_subscription.txt b/templates/email/verify_subscription.txt similarity index 100% rename from django_templates/email/verify_subscription.txt rename to templates/email/verify_subscription.txt diff --git a/django_templates/events/add.html b/templates/events/add.html similarity index 100% rename from django_templates/events/add.html rename to templates/events/add.html diff --git a/django_templates/events/edit_form.html b/templates/events/edit_form.html similarity index 100% rename from django_templates/events/edit_form.html rename to templates/events/edit_form.html diff --git a/django_templates/events/events.html b/templates/events/events.html similarity index 100% rename from django_templates/events/events.html rename to templates/events/events.html diff --git a/django_templates/events/one_event.html b/templates/events/one_event.html similarity index 100% rename from django_templates/events/one_event.html rename to templates/events/one_event.html diff --git a/django_templates/events/one_event_newsletter.html b/templates/events/one_event_newsletter.html similarity index 100% rename from django_templates/events/one_event_newsletter.html rename to templates/events/one_event_newsletter.html diff --git a/django_templates/events/one_event_thisweek.html b/templates/events/one_event_thisweek.html similarity index 100% rename from django_templates/events/one_event_thisweek.html rename to templates/events/one_event_thisweek.html diff --git a/django_templates/events/queue.html b/templates/events/queue.html similarity index 100% rename from django_templates/events/queue.html rename to templates/events/queue.html diff --git a/django_templates/eventsite-safe/addevent.html b/templates/eventsite-safe/addevent.html similarity index 100% rename from django_templates/eventsite-safe/addevent.html rename to templates/eventsite-safe/addevent.html diff --git a/django_templates/eventsite-safe/admin.html b/templates/eventsite-safe/admin.html similarity index 100% rename from django_templates/eventsite-safe/admin.html rename to templates/eventsite-safe/admin.html diff --git a/django_templates/eventsite/admin.html b/templates/eventsite/admin.html similarity index 100% rename from django_templates/eventsite/admin.html rename to templates/eventsite/admin.html diff --git a/django_templates/eventsite/front-page.html b/templates/eventsite/front-page.html similarity index 100% rename from django_templates/eventsite/front-page.html rename to templates/eventsite/front-page.html diff --git a/django_templates/eventsite/front_page_this_week_header.html b/templates/eventsite/front_page_this_week_header.html similarity index 100% rename from django_templates/eventsite/front_page_this_week_header.html rename to templates/eventsite/front_page_this_week_header.html diff --git a/django_templates/eventsite/jump.html b/templates/eventsite/jump.html similarity index 100% rename from django_templates/eventsite/jump.html rename to templates/eventsite/jump.html diff --git a/django_templates/eventsite/newsletter.html b/templates/eventsite/newsletter.html similarity index 100% rename from django_templates/eventsite/newsletter.html rename to templates/eventsite/newsletter.html diff --git a/django_templates/eventsite/sidebar.html b/templates/eventsite/sidebar.html similarity index 100% rename from django_templates/eventsite/sidebar.html rename to templates/eventsite/sidebar.html diff --git a/django_templates/eventsite/tagpage.html b/templates/eventsite/tagpage.html similarity index 100% rename from django_templates/eventsite/tagpage.html rename to templates/eventsite/tagpage.html diff --git a/django_templates/eventsite/week.html b/templates/eventsite/week.html similarity index 100% rename from django_templates/eventsite/week.html rename to templates/eventsite/week.html diff --git a/django_templates/eventsite/week.xml b/templates/eventsite/week.xml similarity index 100% rename from django_templates/eventsite/week.xml rename to templates/eventsite/week.xml diff --git a/django_templates/feeds/latest_description.html b/templates/feeds/latest_description.html similarity index 100% rename from django_templates/feeds/latest_description.html rename to templates/feeds/latest_description.html diff --git a/templates/form_macros.html b/templates/form_macros.html deleted file mode 100755 index 17d33db..0000000 --- a/templates/form_macros.html +++ /dev/null @@ -1,79 +0,0 @@ -{%- macro form_field_label(field) -%} - -{% endmacro %} - -{%- macro form_field_description(field) -%} - {% if field.description %} - {{ field.description }} - {% endif %} -{%- endmacro -%} - -{%- macro form_field_errors(field) -%} - {% if field.errors %} -
        - {%- for error in field.errors -%} -
      • {{ error }}
      • - {%- endfor -%} -
      - {% endif %} -{%- endmacro -%} - -{%- macro form_field_boolean(field) -%} - {{ field(**kwargs) }} - {{ form_field_label(field) }} - {{ form_field_description(field) }} - {{ form_field_errors(field) }} -{%- endmacro -%} - -{%- macro form_field(field) -%} - {% if field.type == 'BooleanField' %} - {{ form_field_boolean(field, **kwargs) }} - {% else%} - {{ form_field_label(field) }} - {% if field.type == 'RadioField' %} - {{ field(class='radio-group', **kwargs) }} - {% else %} - {{ field(**kwargs) }} - {% endif %} - {{ form_field_description(field) }} - {{ form_field_errors(field) }} - {% endif %} -{%- endmacro -%} - -{%- macro form_field_td(field) -%} - {% if field.type == 'BooleanField' %} - - - {{ form_field_boolean(field, **kwargs) }} - - {% else %} - - {{ form_field_label(field) }} - - - {% if field.type == 'RadioField' %} - {{ field(class='radio-group', **kwargs) }} - {% else %} - {{ field(**kwargs) }} - {% endif %} - {{ form_field_description(field) }} - {{ form_field_errors(field) }} - - {% endif %} -{%- endmacro -%} - -{%- macro form_fields(fields) -%} - {% for field in fields %} - {% if field.type == 'HiddenField' %} - {{ field() }} - {% endif %} - {% endfor %} -
        - {% for field in fields %} - {% if field.type != 'HiddenField' %} -
      1. {{ form_field(field) }}
      2. - {% endif %} - {% endfor %} -
      -{%- endmacro -%} \ No newline at end of file diff --git a/django_templates/forms.html b/templates/forms.html similarity index 100% rename from django_templates/forms.html rename to templates/forms.html diff --git a/templates/layout.html b/templates/layout.html deleted file mode 100755 index c0d56f3..0000000 --- a/templates/layout.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "base.html" %} - -{% block layoutextra %} - {% block title %} - {% endblock %} - - - - - - - - - - - - - - - - - - - - -{% endblock %} - -{% block headextra %} - -{% endblock %} - - -{% block body %} -
      -

      Submit a community link

      -
      - -
      - - -{% block content %} - - -{% endblock content %} - - - -
      - - -{% endblock body %} - diff --git a/templates/link/add.html b/templates/link/add.html deleted file mode 100755 index 132c9ba..0000000 --- a/templates/link/add.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "layout.html" %} - -{% from 'form_macros.html' import form_field_td %} - - -{% block content %} - -
      - - {{ form_field_td(form.name) }} - {{ form_field_td(form.href) }} -
      - -
      - - -{% endblock %} \ No newline at end of file diff --git a/templates/link/review.html b/templates/link/review.html deleted file mode 100755 index c136bcb..0000000 --- a/templates/link/review.html +++ /dev/null @@ -1,11 +0,0 @@ -{% for link in pending_links %} - \ No newline at end of file diff --git a/django_templates/newsletter-admin/upcoming.html b/templates/newsletter-admin/upcoming.html similarity index 100% rename from django_templates/newsletter-admin/upcoming.html rename to templates/newsletter-admin/upcoming.html diff --git a/django_templates/sources/add.html b/templates/sources/add.html similarity index 100% rename from django_templates/sources/add.html rename to templates/sources/add.html diff --git a/django_templates/sources/icalendar_manage_listing.html b/templates/sources/icalendar_manage_listing.html similarity index 100% rename from django_templates/sources/icalendar_manage_listing.html rename to templates/sources/icalendar_manage_listing.html diff --git a/django_templates/sources/index.html b/templates/sources/index.html similarity index 100% rename from django_templates/sources/index.html rename to templates/sources/index.html diff --git a/django_templates/sources/index.opml b/templates/sources/index.opml similarity index 100% rename from django_templates/sources/index.opml rename to templates/sources/index.opml diff --git a/django_templates/sources/manage.html b/templates/sources/manage.html similarity index 100% rename from django_templates/sources/manage.html rename to templates/sources/manage.html diff --git a/django_templates/subscriptions/manage.html b/templates/subscriptions/manage.html similarity index 100% rename from django_templates/subscriptions/manage.html rename to templates/subscriptions/manage.html diff --git a/django_templates/subscriptions/new.html b/templates/subscriptions/new.html similarity index 100% rename from django_templates/subscriptions/new.html rename to templates/subscriptions/new.html diff --git a/django_templates/subscriptions/recover.html b/templates/subscriptions/recover.html similarity index 100% rename from django_templates/subscriptions/recover.html rename to templates/subscriptions/recover.html diff --git a/django_templates/subscriptions/thankyou.html b/templates/subscriptions/thankyou.html similarity index 100% rename from django_templates/subscriptions/thankyou.html rename to templates/subscriptions/thankyou.html diff --git a/django_templates/subscriptions/verified_thankyou.html b/templates/subscriptions/verified_thankyou.html similarity index 100% rename from django_templates/subscriptions/verified_thankyou.html rename to templates/subscriptions/verified_thankyou.html diff --git a/test.py b/test.py deleted file mode 100755 index 1628a07..0000000 --- a/test.py +++ /dev/null @@ -1,3 +0,0 @@ -from mapreduce import util -util.for_name('migrate.process') - diff --git a/todo.txt b/todo.txt deleted file mode 100755 index e0fd893..0000000 --- a/todo.txt +++ /dev/null @@ -1,10 +0,0 @@ -Priority: -- add event by URL -- Design/typography overhaul - -Annoyances: -- imported descriptions should be available to editors, even if not included -- Events should have a last_edited date -- recurrence-expansion shouldn't create a fresh event every time -- no navigation between weeks - diff --git a/urls.py b/urls.py index cc4a371..7115c44 100755 --- a/urls.py +++ b/urls.py @@ -1,35 +1,28 @@ -# -*- coding: utf-8 -*- -""" - urls - ~~~~ +# Copyright 2008 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. - URL definitions. +from django.conf.urls.defaults import * - :copyright: 2009 by tipfy.org. - :license: BSD, see LICENSE.txt for more details. -""" -from tipfy import Rule, import_string - - -def get_rules(app): - """Returns a list of URL rules for the application. The list can be - defined entirely here or in separate ``urls.py`` files. - - :param app: - The WSGI application instance. - :return: - A list of class:`tipfy.Rule` instances. - """ - # Here we show an example of joining all rules from the - # ``apps_installed`` definition set in config.py. - rules = [] - - for app_module in app.get_config('tipfy', 'apps_installed'): - try: - # Load the urls module from the app and extend our rules. - app_rules = import_string('%s.urls' % app_module) - rules.extend(app_rules.get_rules(app)) - except ImportError: - pass - - return rules +urlpatterns = patterns('', + # Example: + (r'^account/', include('account.urls')), + url(r'^_ah/login_required', 'account.views.signin', name="account-signin"), + (r'^events/', include('events.urls')), + (r'^sources/', include('sources.urls')), + (r'^subscriptions/', include('subscriptions.urls')), + (r'^admin/', include('eventsite.admin.urls')), + (r'^assets/', include('assets.urls')), + (r'^links/', include('links.urls')), + (r'', include('eventsite.urls')), +) From 15a2b62b71a34adb22cac1e24f3295372bbe3721 Mon Sep 17 00:00:00 2001 From: Ross Karchner Date: Wed, 14 Sep 2011 00:58:37 -0400 Subject: [PATCH 2/7] continuing cleanup --- README | 0 app.yaml | 6 +----- appengine_config.py | 2 -- events/views.py | 2 +- eventsite/context.py | 2 +- links/urls.py | 4 ++-- links/views.py | 13 +++++++++++++ main.py | 18 ------------------ migrate.py | 8 -------- notes.txt | 19 ------------------- out.txt | Bin 1568768 -> 0 bytes settings.py | 2 +- 12 files changed, 19 insertions(+), 57 deletions(-) delete mode 100644 README delete mode 100755 migrate.py delete mode 100755 notes.txt delete mode 100755 out.txt diff --git a/README b/README deleted file mode 100644 index e69de29..0000000 diff --git a/app.yaml b/app.yaml index 3608b89..6c86c0b 100755 --- a/app.yaml +++ b/app.yaml @@ -1,5 +1,5 @@ application: techevents -version: newhomepage +version: cleanup runtime: python api_version: 1 @@ -15,10 +15,6 @@ handlers: script: main.py login: admin -- url: /links/review - script: main.py - login: admin - - url: .* script: main.py diff --git a/appengine_config.py b/appengine_config.py index 2cbc48f..b737722 100755 --- a/appengine_config.py +++ b/appengine_config.py @@ -7,8 +7,6 @@ use_library('django', '1.2') -from django.conf import settings -settings.ROOT_URLCONF="django_urls" diff --git a/events/views.py b/events/views.py index 2f26eae..d6366b8 100755 --- a/events/views.py +++ b/events/views.py @@ -20,7 +20,7 @@ from datetime import datetime from sources.models import ICalendarSource -from apps.links.models import Link +from links.models import Link @profile_required diff --git a/eventsite/context.py b/eventsite/context.py index 77194f1..184dbbd 100755 --- a/eventsite/context.py +++ b/eventsite/context.py @@ -6,7 +6,7 @@ from google.appengine.api import users from google.appengine.api.images import get_serving_url -from apps.links.models import Link +from links.models import Link diff --git a/links/urls.py b/links/urls.py index 1f73a16..2bbb0fe 100755 --- a/links/urls.py +++ b/links/urls.py @@ -3,8 +3,8 @@ urlpatterns = patterns('links.views', url(r'^add/$','add', name="add_link"), - url(r'^review/$','review', name="review_links"), - url(r'^change/$','add', name="change_link"), + # url(r'^review/$','review', name="review_links"), + # url(r'^change/$','add', name="change_link"), ) diff --git a/links/views.py b/links/views.py index e69de29..1ce32f7 100644 --- a/links/views.py +++ b/links/views.py @@ -0,0 +1,13 @@ +from django.shortcuts import render_to_response, redirect +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext +from django.contrib import messages + +from account.utility import get_current_user, profile_required, get_current_profile + +from eventsite import site_required + + +@site_required +def add(request): + return(render_to_response('links/add.html', locals(), context_instance=RequestContext(request))) \ No newline at end of file diff --git a/main.py b/main.py index b297297..e447d9c 100755 --- a/main.py +++ b/main.py @@ -19,24 +19,6 @@ -import logging -import django.core.signals -import django.dispatch.dispatcher -import django.db - -def log_exception(*args, **kwds): - logging.exception('Exception in request:') - -# Log errors. -django.dispatch.Signal.connect( - django.core.signals.got_request_exception, log_exception) - -# Unregister the rollback event handler. -django.dispatch.Signal.disconnect( - django.core.signals.got_request_exception, - django.db._rollback_on_exception) - - def main(): sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path diff --git a/migrate.py b/migrate.py deleted file mode 100755 index 24312fe..0000000 --- a/migrate.py +++ /dev/null @@ -1,8 +0,0 @@ -from mapreduce import operation as op -import logging -import appengine_config -from events.models import Event - -def process(entity): - yield op.db.Put(entity) - return diff --git a/notes.txt b/notes.txt deleted file mode 100755 index 3daa493..0000000 --- a/notes.txt +++ /dev/null @@ -1,19 +0,0 @@ -/tasks/start_fetch_source_calendar - - Call driver-appropriate prep method - create SourceProcessTicket - -/tasks/parse - call driver-appropriate method (perhaps spawning other tasks) - to convert source to iCal-formatted snippets - iCalSnippet - iCal - SourceProcessTicket - source - - - - -iCal -Google Calendar -Meetup diff --git a/out.txt b/out.txt deleted file mode 100755 index f85b248221d67289cad5bb4e756687557a3e194b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1568768 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 z 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

      kLl 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 X|H5Vb6w&#=Vg7e!g#S}Vxby!9Padn; diff --git a/settings.py b/settings.py index c71b8fb..92fc76b 100755 --- a/settings.py +++ b/settings.py @@ -103,7 +103,7 @@ TEMPLATE_DIRS = ( - os.path.join(ROOT_PATH, 'django_templates') + os.path.join(ROOT_PATH, 'templates') ) From a9987efbe264941af009ad3b927e99c3a16d81ef Mon Sep 17 00:00:00 2001 From: Ross Karchner Date: Mon, 7 Nov 2011 23:59:52 -0500 Subject: [PATCH 3/7] ported links functionality to Django, prep'ed app for new billing rules and python2.7 --- account/forms.py | 1 - account/views.py | 1 - app.yaml | 19 ++-- appengine_config.py | 5 +- events/models.py | 1 - eventsite/__init__.py | 2 +- eventsite/admin/forms.py | 10 +- eventsite/admin/views.py | 10 +- eventsite/models.py | 1 - eventsite/views.py | 1 - links/forms.py | 39 +------ links/urls.py | 2 +- links/views.py | 29 +++++- main.py | 11 +- readme.md | 4 +- settings.py | 2 +- sources/forms.py | 1 + sources/models.py | 49 +++++---- sources/tasks.py | 148 ++++++++------------------- sources/urls.py | 4 +- sources/views.py | 2 +- templates/account/profile-setup.html | 1 + templates/base.html | 1 - templates/eventsite/front-page.html | 2 +- templates/eventsite/newsletter.html | 1 - templates/eventsite/sidebar.html | 18 +--- templates/eventsite/tagpage.html | 1 - templates/eventsite/week.html | 1 - templates/eventsite/week.xml | 1 - templates/links/add.html | 40 ++++++++ templates/links/review.html | 50 +++++++++ 31 files changed, 221 insertions(+), 237 deletions(-) create mode 100644 templates/links/add.html create mode 100644 templates/links/review.html diff --git a/account/forms.py b/account/forms.py index 0fdd568..d5796bc 100755 --- a/account/forms.py +++ b/account/forms.py @@ -15,7 +15,6 @@ class ProfileForm(forms.Form): nickname = forms.CharField(max_length=255, help_text="What you want to be known as on the site") email=forms.EmailField(required=True, help_text="Where should we send email?", widget=EmailInput) - subscribe = forms.BooleanField(required=False, help_text="Do you want to get the weekly events email?") link=forms.URLField(required=False, help_text="This can be the URL of your blog, twitter page, LinkedIn profile, homepage, or anything else.", widget=TextInput(attrs={'placeholder':'http://whatever'})) diff --git a/account/views.py b/account/views.py index 2946a28..870e0ef 100755 --- a/account/views.py +++ b/account/views.py @@ -50,7 +50,6 @@ def save_profile(profile, form): profile.slug=unicode(slugify(nickname)) profile.confirmed_at=datetime.now() profile.link=form.cleaned_data['link'] or None - profile.subscribes=form.cleaned_data['subscribe'] profile.put() if profile.subscribes: site=get_site() diff --git a/app.yaml b/app.yaml index 6c86c0b..5d4b497 100755 --- a/app.yaml +++ b/app.yaml @@ -1,22 +1,29 @@ -application: techevents -version: cleanup -runtime: python +application: eventgrinder2 +version: cleanup2 +runtime: python27 api_version: 1 +threadsafe: true + +libraries: +- name: PIL + version: "1.1.7" +- name: django + version: "1.2" -handlers: +handlers: - url: /static/([^/]*)/(.*) static_files: static/\2 upload: static/(.*) expiration: "24d" - url: /tasks.* - script: main.py + script: main.application login: admin - url: .* - script: main.py + script: main.application builtins: - datastore_admin: on \ No newline at end of file diff --git a/appengine_config.py b/appengine_config.py index b737722..25d832b 100755 --- a/appengine_config.py +++ b/appengine_config.py @@ -2,11 +2,8 @@ import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' -from google.appengine.dist import use_library -use_library('django', '1.2') - +sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path diff --git a/events/models.py b/events/models.py index c8cf434..f28c426 100755 --- a/events/models.py +++ b/events/models.py @@ -1,7 +1,6 @@ import datetime from google.appengine.ext import db from google.appengine.api import users -from sources.models import ICalendarSource from eventsite.models import Eventsite from account.models import Profile from utility import slugify diff --git a/eventsite/__init__.py b/eventsite/__init__.py index da5fe67..5deb128 100755 --- a/eventsite/__init__.py +++ b/eventsite/__init__.py @@ -23,7 +23,7 @@ def get_site(key_name=None): def site_required(func): def no_site(request, *args, **kwargs): if not request.site: - return HttpResponse("No calendar with that name exists (yet!)") + return HttpResponseRedirect("/admin/create/") else: return HttpResponse("%s will return soon!" % request.site.name) diff --git a/eventsite/admin/forms.py b/eventsite/admin/forms.py index 59a85de..5f7e286 100755 --- a/eventsite/admin/forms.py +++ b/eventsite/admin/forms.py @@ -11,19 +11,15 @@ class SiteCreateForm(forms.Form): name = forms.CharField(max_length=255, required=True) timezone=forms.ChoiceField(choices=timezones) - slug=forms.CharField(max_length=255, required=True) - audience=forms.CharField(max_length=255, required=True) + + class SiteDetailsForm(forms.Form): name = forms.CharField(max_length=255, required=True) timezone=forms.ChoiceField(choices=timezones) - audience=forms.CharField(max_length=255, required=True) - google_analytics_code=forms.CharField(max_length=255, required=False) - google_site_verification=forms.CharField(max_length=255, required=False) twitter=forms.CharField(max_length=255, required=False) - bsa_code=forms.CharField(max_length=255, required=False, widget=forms.Textarea) offline=forms.BooleanField(required=False) - hostnames=forms.CharField(max_length=255, required=False) + def clean_hostnames(self): diff --git a/eventsite/admin/views.py b/eventsite/admin/views.py index e2c0373..a449d82 100755 --- a/eventsite/admin/views.py +++ b/eventsite/admin/views.py @@ -37,10 +37,9 @@ def create_site(request): if form.is_valid(): new_site=models.Eventsite(name=form.cleaned_data['name'].strip(), timezone= form.cleaned_data['timezone'].strip(), - audience=form.cleaned_data['audience'].strip(), hostnames=[hostname,], key_name=hostname, - slug=form.cleaned_data['slug']) + slug=str(slugify(hostname))) new_site.put() return HttpResponseRedirect(reverse('admin-home')) return render_to_response('eventsite/admin.html', locals(), context_instance=RequestContext(request)) @@ -66,12 +65,7 @@ def edit_site(request): if form.is_valid(): # All validation rules pass site.name=form.cleaned_data['name'].strip() site.timezone= form.cleaned_data['timezone'].strip() - site.audience=form.cleaned_data['audience'].strip() - site.hostnames=form.cleaned_data['hostnames'] - site.google_site_verification=form.cleaned_data['google_site_verification'].strip() - site.google_analytics_code=form.cleaned_data['google_analytics_code'].strip() site.twitter=form.cleaned_data['twitter'] or None - site.bsa_code=form.cleaned_data['bsa_code'] or None site.offline=form.cleaned_data['offline'] or None site.put() site.expire_assets() @@ -81,7 +75,7 @@ def edit_site(request): site=get_site() if site: site_details={'name':site.name, 'timezone':site.timezone, 'slug':site.slug, - 'audience': site.audience, 'hostnames':",".join(site.hostnames), + 'hostnames':",".join(site.hostnames), 'google_analytics_code':site.google_analytics_code, 'google_site_verification':site.google_site_verification, 'twitter':site.twitter, diff --git a/eventsite/models.py b/eventsite/models.py index a972d9e..2c34497 100755 --- a/eventsite/models.py +++ b/eventsite/models.py @@ -30,7 +30,6 @@ class Eventsite(db.Model): original_logo_version=db.IntegerProperty() logo=db.BlobProperty() logo_asset_href=db.StringProperty() - audience=db.TextProperty(required=False) google_analytics_code=db.TextProperty(required=False) google_site_verification=db.TextProperty(required=False) disqus_shortname=db.TextProperty(required=False) diff --git a/eventsite/views.py b/eventsite/views.py index 5dd32d1..d62b540 100755 --- a/eventsite/views.py +++ b/eventsite/views.py @@ -148,7 +148,6 @@ def this_week_rss(request): -@cache_page(60 * 10) @site_required def front_page(request, tag=None): start=request.site.today diff --git a/links/forms.py b/links/forms.py index 92c1743..c10b2a1 100755 --- a/links/forms.py +++ b/links/forms.py @@ -1,42 +1,13 @@ -from datetime import date -import logging - -from tipfy import RequestHandler, Response, redirect, cached_property -from tipfy.ext.jinja2 import Jinja2Mixin -from tipfy.ext.wtforms import Form, fields, validators, widgets -from tipfy.ext.wtforms.validators import ValidationError - -from tipfy.ext.db import populate_entity - -from wtforms.ext.dateutil.fields import DateField - - -from pytz.gae import pytz - - -# a set of HTML5 widgets for wtforms - -from wtforms.widgets import Input - -REQUIRED = validators.required() +from django import forms from models import Link - -class LinkWidget(Input): - def __init__(self, input_type='url'): - if input_type is not None: - self.input_type = input_type - - - - -class AddLinkForm(Form): - name = fields.TextField('Site Name', validators=[REQUIRED]) - href=fields.TextField('URL', validators=[validators.URL(), REQUIRED], widget=LinkWidget()) +class AddLinkForm(forms.Form): + name = forms.CharField(max_length=500) + href=forms.URLField() def save(self): - new_link=Link(**self.data) + new_link=Link(**self.cleaned_data) new_link.status='submitted' new_link.put() diff --git a/links/urls.py b/links/urls.py index 2bbb0fe..7835284 100755 --- a/links/urls.py +++ b/links/urls.py @@ -3,7 +3,7 @@ urlpatterns = patterns('links.views', url(r'^add/$','add', name="add_link"), - # url(r'^review/$','review', name="review_links"), + url(r'^review/$','review', name="review_links"), # url(r'^change/$','add', name="change_link"), diff --git a/links/views.py b/links/views.py index 1ce32f7..93a0b5e 100644 --- a/links/views.py +++ b/links/views.py @@ -3,11 +3,34 @@ from django.template import RequestContext from django.contrib import messages -from account.utility import get_current_user, profile_required, get_current_profile +from account.utility import get_current_user, profile_required, get_current_profile, admin_required from eventsite import site_required - +from forms import AddLinkForm +from models import Link @site_required def add(request): - return(render_to_response('links/add.html', locals(), context_instance=RequestContext(request))) \ No newline at end of file + if request.method == 'POST': + form=AddLinkForm(request.POST) + if form.is_valid(): + form.save() + messages.add_message(request, messages.INFO, "Thank you for submitting a link!") + return redirect('/') + + + else: + form=AddLinkForm() + return(render_to_response('links/add.html', locals(), context_instance=RequestContext(request))) + + +@admin_required +def review(request): + if request.method=='POST': + link=Link.get_by_id(int(request.POST['id'])) + link.status=request.POST['action'] + link.put() + + approved_links=Link.all().filter('status =', 'approved') + submitted_links=Link.all().filter('status =', 'submitted') + return(render_to_response('links/review.html', locals(), context_instance=RequestContext(request))) \ No newline at end of file diff --git a/main.py b/main.py index e447d9c..673245c 100755 --- a/main.py +++ b/main.py @@ -8,9 +8,6 @@ # Django imports and other code go here... import os os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' -from google.appengine.dist import use_library -use_library('django', '1.2') - import django.core.handlers, django.core.handlers.wsgi @@ -20,10 +17,4 @@ -def main(): - sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path - application = django.core.handlers.wsgi.WSGIHandler() - util.run_wsgi_app(application) - -if __name__ == '__main__': - main() \ No newline at end of file +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/readme.md b/readme.md index a408998..14254ee 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ #What is this crazy thing? -This is the current, production version of "Eventgrinder"-- which runs some calendar aggregator sites, like dctechevents.com +This is a "cleanup" branch of "Eventgrinder"-- which runs some calendar aggregator sites, like dctechevents.com #Whatgrinder? @@ -13,8 +13,6 @@ Lot's of reasons! Here's a few: * I didn't concieve of it as open source, and it never occured to me that someone else might see the code. * Testing? LOL * There are a lot of half-implemented ideas in the code. -* It only runs on AppEngine -* It's written in at least two frameworks (Django and Tipfy), with a little bit of hacky WSGI middleware thrown in for reasons I forget. # What is most broken? diff --git a/settings.py b/settings.py index 92fc76b..14acd40 100755 --- a/settings.py +++ b/settings.py @@ -97,7 +97,7 @@ #'messaging.messaging_context' ) -ROOT_URLCONF = 'django_urls' +ROOT_URLCONF = 'urls' ROOT_PATH = os.path.dirname(__file__) diff --git a/sources/forms.py b/sources/forms.py index efdf195..265eb42 100755 --- a/sources/forms.py +++ b/sources/forms.py @@ -21,6 +21,7 @@ class ICalendarEditForm(forms.Form): source_key=forms.CharField(required=False) def save(self): + from models import ICalendarSource cleaned_data=self.cleaned_data profile=get_current_profile() from models import ICalendarSource diff --git a/sources/models.py b/sources/models.py index 98524c5..f3f0473 100755 --- a/sources/models.py +++ b/sources/models.py @@ -14,6 +14,8 @@ from google.appengine.api.labs import taskqueue from google.appengine.api import memcache +from sources.tasks import process_ical, process_gdata + from urllib import unquote import gdata.calendar.service @@ -49,7 +51,9 @@ def fetch(self,started=None, timestamp=None): format_start="%Y%m%d%H%M" if not started: started=str(datetime.now()) if not timestamp:timestamp= datetime.now().strftime("%Y%m%d%H%M") - if self.ical_href.startswith('http://www.google.com/calendar/ical/'): + + if self.ical_href.startswith('http://www.google.com/calendar/ical/') or self.ical_href.startswith('https://www.google.com/calendar/ical/'): + gcal_id=unquote(self.ical_href[36:].split('/')[0]) query = gdata.calendar.service.CalendarEventQuery(gcal_id, 'public', 'full-noattendees') query.start_min= self.site.today.strftime("%Y-%m-%d") @@ -57,33 +61,38 @@ def fetch(self,started=None, timestamp=None): query.start_max=(date.today()+relativedelta(months=3)).strftime("%Y-%m-%d") query.singleevents='true' result=urlfetch.fetch(query.ToUri(), allow_truncated=False, deadline=10) + logging.warning("fetching %s" % result.content) if result.status_code == 200: detection=chardet.detect(result.content) self.last_fetch=datetime.now() + self.content=result.content.decode(detection['encoding']) self.put() - cache_key="%s-%s-%s" %(self.site.slug, self.slug,timestamp) - memcache.add(cache_key, result.content.decode(detection['encoding']),600) - logging.warning("cached gdata with key %s"% cache_key) - taskqueue.add(url='/sources/split_gdata/', params={'ical_key': self.key(), - 'cache_key':cache_key, - 'timestamp':timestamp}, - name=cache_key - ) - logging.warning("enqueued splitting of %s" % self.ical_href) + process_gdata(self) + #cache_key="%s-%s-%s" %(self.site.slug, self.slug,timestamp) + #memcache.add(cache_key, ,600) + #logging.warning("cached gdata with key %s"% cache_key) + #taskqueue.add(url='/sources/split_gdata/', params={'ical_key': self.key(), + # 'cache_key':cache_key, + # 'timestamp':timestamp}, + # name=cache_key + # ) + #logging.warning("enqueued splitting of %s" % self.ical_href) return - result=urlfetch.fetch(self.ical_href, allow_truncated=True, deadline=5) + result=urlfetch.fetch(self.ical_href, allow_truncated=False, deadline=5) if result.status_code == 200: detection=chardet.detect(result.content) self.last_fetch=datetime.now() + self.content=result.content.decode(detection['encoding']) self.put() - cache_key="%s-%s-%s" %(self.site.slug, self.slug,timestamp) - memcache.add(cache_key, result.content.decode(detection['encoding']),600) - logging.warning("cached ical with key %s"% cache_key) - taskqueue.add(url='/sources/split_ical/', params={'ical_key': self.key(), - 'cache_key':cache_key, - 'timestamp':timestamp}, - name=cache_key - ) - logging.warning("enqueued splitting of %s" % self.ical_href) + process_ical(self) + #cache_key="%s-%s-%s" %(self.site.slug, self.slug,timestamp) + #memcache.add(cache_key, result.content.decode(detection['encoding']),600) + #logging.warning("cached ical with key %s"% cache_key) + #taskqueue.add(url='/sources/split_ical/', params={'ical_key': self.key(), + # 'cache_key':cache_key, + # 'timestamp':timestamp}, + # name=cache_key + # ) + #logging.warning("enqueued splitting of %s" % self.ical_href) diff --git a/sources/tasks.py b/sources/tasks.py index a3cdabf..2e7c06d 100755 --- a/sources/tasks.py +++ b/sources/tasks.py @@ -1,4 +1,3 @@ -from models import ICalendarSource from django.http import HttpResponse from dateutil.parser import parse from google.appengine.api.labs import taskqueue @@ -11,6 +10,7 @@ from google.appengine.api import memcache from google.appengine.api import urlfetch + import gdata.calendar from vobject.icalendar import stringToDate, stringToDateTime @@ -28,35 +28,22 @@ -def split_gdata(request): - try: - if request.method == 'POST': - key=db.Key(request.POST.get('ical_key')) - source=ICalendarSource.get(key) - gdata_source=memcache.get(request.POST.get('cache_key')) - memcache.delete(request.POST.get('cache_key')) - feed=gdata.calendar.CalendarEventFeedFromString(gdata_source) - cal_count=0 - for gevent in feed.entry: - cal_count=cal_count +1 - source_cache_key=request.POST.get('cache_key') - cache_key=source_cache_key +"-"+ str(cal_count) - memcache.set(cache_key, gevent.ToString(),1200) - - params=params={'cache_key': cache_key, - 'ical_key': request.POST['ical_key']} - taskqueue.add(url='/events/parse_one_gdata/', - params=params, - name=cache_key,countdown=30) +def process_gdata(source): + from events.models import Event + try: + gdata_source=source.content + feed=gdata.calendar.CalendarEventFeedFromString(gdata_source) + cal_count=0 + for gevent in feed.entry: + Event.from_gdata(gevent, source) - except urlfetch.DownloadError: - raise + except urlfetch.DownloadError: + raise - except Exception,e: - logging.error("%s in \n%s"% (traceback.format_exc(),str(request.POST))) + except Exception,e: + logging.error(traceback.format_exc()) - return HttpResponse("OK") @@ -69,7 +56,7 @@ def fetch_icals(request): q=ICalendarSource.all().filter('status = ', 'approved').filter('last_fetch < ', parsed_started- relativedelta(hours=+3)) if cursor: q=q.with_cursor(cursor) - cals= q.fetch(1) + cals= q.fetch(100) if cals: params={'cursor': q.cursor(), 'started': started} @@ -86,84 +73,31 @@ def fetch_icals(request): return HttpResponse("OK") -def split_ical(request): - - - - def is_future(ical): - return True - v_start=ical.find("BEGIN:VEVENT") - start_line=ical.find('DTSTART:',v_start) - end_line=ical.find("\n", start_line) - logging.warning("startline: \n %s"% ical[start_line:end_line]) - if end_line < 0: - end_line=ical.find("\r\n") - try: - dateobject= parse(ical[start_line+8:end_line]) - except ValueError: - try: - dateobject= stringToDateTime(ical[start_line+8:end_line]) - except: - logging.warning("Could not parse DTSTART %s" % ical[start_line+8:end_line]) - return True - if hasattr(dateobject, 'tzinfo') and dateobject.tzinfo: - - diff= utc.localize(datetime.utcnow()) - dateobject - else: - diff= datetime.now() - dateobject - - return diff < timedelta(1) - - - - if request.method == 'POST': - key=db.Key(request.POST.get('ical_key')) - source=ICalendarSource.get(key) - - def split_ical_file(ical): - first_vevent_start=ical.find('BEGIN:VEVENT') - cal_meta=ical[0:first_vevent_start] - #print cal_meta - - def next_vevent(start): - end=ical.find('END:VEVENT',start) - if end > -1: - end=end+10 - cal=cal_meta+ical[start:end]+"\nEND:VCALENDAR" - return (cal, end) - else: - return(None, None) - vevent, index=next_vevent(first_vevent_start) - while vevent: - yield vevent - vevent, index=next_vevent(index) - try: - ical_source=memcache.get(request.POST.get('cache_key')) - memcache.delete(request.POST.get('cache_key')) - - if not ical_source: - logging.error("nothing in cache") - return HttpResponse("nothing in cache") - if ical_source: - cal_count=0 - for event_ical in split_ical_file(ical_source): - cal_count=cal_count +1 - if is_future(event_ical): - source_cache_key=request.POST.get('cache_key') - cache_key=source_cache_key +"-"+ str(cal_count) - memcache.set(cache_key, event_ical,1200) - - params=params={'cache_key': cache_key, - 'ical_key': request.POST['ical_key']} - logging.warning(params) - taskqueue.add(url='/events/parse_one_event/', - params=params, - name=cache_key,countdown=30) - - - except DeadlineExceededError: - return HttpResponse("Deadline Exceeded!") - except Exception,e: - logging.error("%s in \n%s"% (traceback.format_exc(),str(request.POST))) - return HttpResponse("OK") \ No newline at end of file +def process_ical(source): + from events.models import Event + try: + ical=source.content + first_vevent_start=ical.find('BEGIN:VEVENT') + cal_meta=ical[0:first_vevent_start] + + def next_vevent(start): + end=ical.find('END:VEVENT',start) + if end > -1: + end=end+10 + cal=cal_meta+ical[start:end]+"\nEND:VCALENDAR" + return (cal, end) + else: + return(None, None) + vevent, index=next_vevent(first_vevent_start) + while vevent: + event=Event.from_vcal(vevent, source) + vevent, index=next_vevent(index) + + + + except DeadlineExceededError: + return HttpResponse("Deadline Exceeded!") + except Exception,e: + logging.error(traceback.format_exc()) + return HttpResponse("OK") \ No newline at end of file diff --git a/sources/urls.py b/sources/urls.py index 9988229..c850e87 100755 --- a/sources/urls.py +++ b/sources/urls.py @@ -1,6 +1,6 @@ from django.conf.urls.defaults import * from views import add_source, sources, manage_sources, save_ical, start_fetch_icals, opml, json -from tasks import fetch_icals, split_ical, split_gdata +from tasks import fetch_icals urlpatterns = patterns('events', @@ -11,8 +11,6 @@ url(r'^add/$', add_source, name="add-source"), url(r'^save/$', save_ical, name="save-ical"), url(r'^fetch/$', fetch_icals, name="fetch-icals-task"), - url(r'^split_gdata/$', split_gdata ), - url(r'^split_ical/$', split_ical, name="split_ical"), url(r'^start_fetch_icals/$',start_fetch_icals ) ) diff --git a/sources/views.py b/sources/views.py index 8b239ec..7c77075 100755 --- a/sources/views.py +++ b/sources/views.py @@ -2,7 +2,7 @@ from django.http import HttpResponse, HttpResponseRedirect from django.template import RequestContext from django.contrib import messages - +from models import ICalendarSource from forms import ICalendarEditForm, ICalApprovalForm from account.utility import profile_required, userlevel_required from eventsite import site_required diff --git a/templates/account/profile-setup.html b/templates/account/profile-setup.html index a6f2c5f..3531402 100755 --- a/templates/account/profile-setup.html +++ b/templates/account/profile-setup.html @@ -6,6 +6,7 @@

      +

      Set up your profile

      {% include "account/edit_profile_form.html" %}
      diff --git a/templates/base.html b/templates/base.html index f1e3544..7f9d00d 100755 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,6 @@ {% block opengraph %} - {% endblock %} diff --git a/templates/eventsite/front-page.html b/templates/eventsite/front-page.html index 2bdd580..4f98ea0 100755 --- a/templates/eventsite/front-page.html +++ b/templates/eventsite/front-page.html @@ -1,4 +1,4 @@ -{% extends "base-cacheable.html" %} +{% extends "base.html" %} {% load cdn_helper %} {% load event_tags %} {% load utility %} diff --git a/templates/eventsite/newsletter.html b/templates/eventsite/newsletter.html index 7e6a982..8a260b4 100755 --- a/templates/eventsite/newsletter.html +++ b/templates/eventsite/newsletter.html @@ -41,7 +41,6 @@

      {{ site.name }} for the week of {{ start|date:"F j, Y"}}

      -
      {{site.name}} is a calendar for {{site.audience}}
      Please consider forwarding this along to your friends and colleagues in the community. If you find it useful, they might too.
      diff --git a/templates/eventsite/sidebar.html b/templates/eventsite/sidebar.html index 08b64b8..d981f4b 100755 --- a/templates/eventsite/sidebar.html +++ b/templates/eventsite/sidebar.html @@ -1,22 +1,6 @@
      -{% if site.bsa_code %} - -{{ site.bsa_code|safe }} - -{% else %} - - -{% endif %} +
      diff --git a/templates/eventsite/tagpage.html b/templates/eventsite/tagpage.html index 73015ea..e1a0e96 100755 --- a/templates/eventsite/tagpage.html +++ b/templates/eventsite/tagpage.html @@ -11,7 +11,6 @@ {% block opengraph %} - {% endblock opengraph %} diff --git a/templates/eventsite/week.html b/templates/eventsite/week.html index e5bfd3d..1bcc2d5 100755 --- a/templates/eventsite/week.html +++ b/templates/eventsite/week.html @@ -13,7 +13,6 @@ {% block opengraph %} - {% endblock opengraph %} diff --git a/templates/eventsite/week.xml b/templates/eventsite/week.xml index c89d012..7477766 100644 --- a/templates/eventsite/week.xml +++ b/templates/eventsite/week.xml @@ -6,7 +6,6 @@ en-us rosskarchner@gmail.com (Ross Karchner) - a calendar for {{site.audience}} This Week on {{ site.name }} http://{{site.host}}/week-of/{{start|date:"c"}} diff --git a/templates/links/add.html b/templates/links/add.html new file mode 100644 index 0000000..fe6132a --- /dev/null +++ b/templates/links/add.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% load cdn_helper %} + + +{% block content %} +
      +
      +

      submit a community link

      + + +
      +
      + + + +
      +
      +
      +
      {{form.name.label_tag}} + {{form.name}} {{form.name.help_text}} + {{form.name.errors}} +
      +
      {{form.href.label_tag}} + {{form.href}} {{form.href.help_text}} + {{form.href.errors}} +
      + + +
      +
      + +
      +
      + +
      + +
      + + +{% endblock %} \ No newline at end of file diff --git a/templates/links/review.html b/templates/links/review.html new file mode 100644 index 0000000..58dada8 --- /dev/null +++ b/templates/links/review.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} +{% load cdn_helper %} + + +{% block content %} +
      +
      +

      Review community links

      + + +
      +
      + + + +
      +
      +

      Submitted Links

      + {% for link in submitted_links %} + {{link.name}}
      + {{ link.href }} + + + set to: +
      + {% empty %} + no submitted links + {% endfor %} +

      Approved links

      + {% for link in approved_links %} + {{link.name}}
      + {{ link.href }} +
      + + set to: +

      + {% empty %} + No approved links + + {% endfor %} + +
      +
      + +
      + +
      + + +{% endblock %} \ No newline at end of file From 3f43829e593525e12f2ca5ec9f5079d2a30d0efd Mon Sep 17 00:00:00 2001 From: Ross Karchner Date: Wed, 16 Nov 2011 06:40:29 -0500 Subject: [PATCH 4/7] removed logo feature, fixed a bad import in tasks.py --- app.yaml | 3 ++- appengine_config.py | 10 +++++++--- appengine_console.py | 25 ------------------------- sources/tasks.py | 1 + templates/admin-base.html | 8 +++----- templates/base.html | 8 +++----- templates/eventsite/newsletter.html | 10 +--------- 7 files changed, 17 insertions(+), 48 deletions(-) delete mode 100755 appengine_console.py diff --git a/app.yaml b/app.yaml index 5d4b497..0f69ad3 100755 --- a/app.yaml +++ b/app.yaml @@ -26,4 +26,5 @@ handlers: script: main.application builtins: -- datastore_admin: on \ No newline at end of file +- datastore_admin: on +- remote_api: on \ No newline at end of file diff --git a/appengine_config.py b/appengine_config.py index 25d832b..5c35975 100755 --- a/appengine_config.py +++ b/appengine_config.py @@ -1,9 +1,9 @@ import sys -import os +import os, os.path +from logging import error -sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path @@ -11,7 +11,11 @@ def namespace_manager_default_namespace_for_request(): import os + if os.environ.get('PATH_INFO', '').startswith('/_ah') and not os.environ.get('PATH_INFO', '').startswith('/_ah/login_required') or os.environ.get('PATH_INFO', '').startswith('/_ah/upload'): return '' if os.environ.get('HTTP_HOST'): return os.environ.get('HTTP_HOST').split(':')[0] else: - return '' \ No newline at end of file + return '' + +remoteapi_CUSTOM_ENVIRONMENT_AUTHENTICATION = ( + 'HTTP_X_APPENGINE_INBOUND_APPID', ['techevents']) \ No newline at end of file diff --git a/appengine_console.py b/appengine_console.py deleted file mode 100755 index 51085fe..0000000 --- a/appengine_console.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/python -import code -import getpass -import sys -import os - -sys.path.append("/usr/local/google_appengine") -sys.path.append("/usr/local/google_appengine/lib/yaml/lib") -sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), 'Apps')]+sys.path - -from google.appengine.ext.remote_api import remote_api_stub -from google.appengine.ext import db -from google.appengine.dist import use_library -use_library('django', '1.1') -import os, cgi -os.environ['DJANGO_SETTINGS_MODULE']='techevents_settings' - -def auth_func(): - return 'rosskarchner', '1GingerBeer' - -host = 'localhost:8083' - -remote_api_stub.ConfigureRemoteDatastore('techevents', '/remote_api', auth_func, host) - -code.interact('App Engine interactive console for %s' % ('techevents'), None, locals()) \ No newline at end of file diff --git a/sources/tasks.py b/sources/tasks.py index 2e7c06d..989bbb2 100755 --- a/sources/tasks.py +++ b/sources/tasks.py @@ -48,6 +48,7 @@ def process_gdata(source): def fetch_icals(request): + from models import ICalendarSource try: if request.method == 'POST': cursor=request.POST.get('cursor') diff --git a/templates/admin-base.html b/templates/admin-base.html index a6f43cb..ebfb3ee 100755 --- a/templates/admin-base.html +++ b/templates/admin-base.html @@ -54,11 +54,9 @@
      @@ -76,7 +74,7 @@ {% endblock %} diff --git a/templates/base.html b/templates/base.html index 7f9d00d..df9cdbf 100755 --- a/templates/base.html +++ b/templates/base.html @@ -120,11 +120,9 @@

      - {% if site.logo_asset_href %} - {{ site.name }} - {% else %} + {{ site.name }} - {% endif %} +

      {% if site.twitter %}
      Get events as soon as we do, follow @{{site.twitter}}!
      {% endif %} @@ -153,7 +151,7 @@

      {% block endcode %} diff --git a/templates/eventsite/newsletter.html b/templates/eventsite/newsletter.html index 8a260b4..43742c6 100755 --- a/templates/eventsite/newsletter.html +++ b/templates/eventsite/newsletter.html @@ -30,15 +30,7 @@ - - - {% if site.logo_asset_href %} - {{ site.name }} - {% else %} - {{ site.name }} - {% endif %} - - +

      {{ site.name }} for the week of {{ start|date:"F j, Y"}}

      From fde09043c4c881df2a7cfa71118cb5c09ae0dc2f Mon Sep 17 00:00:00 2001 From: Ross Karchner Date: Wed, 16 Nov 2011 06:43:30 -0500 Subject: [PATCH 5/7] move app.yaml to app.yaml-dist --- app.yaml => app.yaml-dist | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app.yaml => app.yaml-dist (100%) diff --git a/app.yaml b/app.yaml-dist similarity index 100% rename from app.yaml rename to app.yaml-dist From a5bde9f256bff56dc3b2c1ed7d78e7d928226996 Mon Sep 17 00:00:00 2001 From: Ross Karchner Date: Sun, 11 Mar 2012 13:41:54 -0700 Subject: [PATCH 6/7] misc fixes --- .gitignore | 3 ++- appengine_config.py | 1 + main.py | 1 - static/event-add.js | 15 --------------- templates/account/edit_profile_form.html | 4 ---- templates/base-cacheable.html | 4 ++-- templates/base.html | 6 +++--- templates/events/add.html | 6 ------ templates/events/one_event.html | 4 +--- templates/events/one_event_thisweek.html | 5 ----- templates/eventsite/sidebar.html | 13 ++----------- 11 files changed, 11 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 7d8faaf..4edfa96 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ var/ .DS_Store? ehthumbs.db Icon? -Thumbs.db \ No newline at end of file +Thumbs.db +app.yaml diff --git a/appengine_config.py b/appengine_config.py index 5c35975..721d5cc 100755 --- a/appengine_config.py +++ b/appengine_config.py @@ -3,6 +3,7 @@ from logging import error +sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path diff --git a/main.py b/main.py index 673245c..60dfbcd 100755 --- a/main.py +++ b/main.py @@ -2,7 +2,6 @@ from google.appengine.ext.webapp import util -sys.path= [os.path.join(os.path.dirname(__file__), 'shared'), os.path.join(os.path.dirname(__file__), '.')]+sys.path # Django imports and other code go here... diff --git a/static/event-add.js b/static/event-add.js index 06841a8..2c73ed4 100755 --- a/static/event-add.js +++ b/static/event-add.js @@ -19,21 +19,6 @@ $(function() { max_chars: 250 }); - - - - if ($("#id_description").val() == ""){ - $("#event_description").hide(); - var add_descriptions; - add_days=$('').click(function(){ - $("#event_description").show() - $(this).hide(); - return false; - }) - - $("#link").append(add_days) - } - if ($("#end_date input").val() == ""){ $("#end_date").hide(); diff --git a/templates/account/edit_profile_form.html b/templates/account/edit_profile_form.html index 4713489..c24abcd 100755 --- a/templates/account/edit_profile_form.html +++ b/templates/account/edit_profile_form.html @@ -12,8 +12,4 @@ {{form.link}} {{form.link.help_text}} {{form.link.errors}}
      -
      {{form.subscribe.label_tag}} - {{form.subscribe}} {{form.subscribe.help_text}} - {{form.subscribe.errors}} -
      \ No newline at end of file diff --git a/templates/base-cacheable.html b/templates/base-cacheable.html index 34725b2..8213ead 100755 --- a/templates/base-cacheable.html +++ b/templates/base-cacheable.html @@ -31,11 +31,11 @@ - @@ -36,23 +35,17 @@ }, 7000); - $(".datepicker").datepicker(); + if (!Modernizr.inputtypes.date) { + $('input[type=date]').datepicker(); + } - }) - {% block headextra %}