From 6da717bf333dc82c67d7aece3fd36f97090040f8 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Tue, 28 Jul 2015 21:15:06 -0700 Subject: Initial import --- foundry/__init__.py | 11 +++ foundry/application.py | 63 ++++++++++++++++ foundry/controllers/__init__.py | 33 +++++++++ foundry/di.py | 16 ++++ foundry/interfaces.py | 23 ++++++ foundry/router.py | 22 ++++++ foundry/template_filters.py | 53 ++++++++++++++ foundry/test_router.py | 159 ++++++++++++++++++++++++++++++++++++++++ foundry/utils.py | 106 +++++++++++++++++++++++++++ foundry/vcs/__init__.py | 0 foundry/vcs/hg/__init__.py | 0 foundry/vcs/hg/providers.py | 24 ++++++ foundry/views/__init__.py | 23 ++++++ foundry/views/changelog.tpl | 42 +++++++++++ setup.py | 18 +++++ tests/test_frozendict.py | 19 +++++ tests/test_implements.py | 51 +++++++++++++ 17 files changed, 663 insertions(+) create mode 100644 foundry/__init__.py create mode 100644 foundry/application.py create mode 100644 foundry/controllers/__init__.py create mode 100644 foundry/di.py create mode 100644 foundry/interfaces.py create mode 100644 foundry/router.py create mode 100644 foundry/template_filters.py create mode 100644 foundry/test_router.py create mode 100644 foundry/utils.py create mode 100644 foundry/vcs/__init__.py create mode 100644 foundry/vcs/hg/__init__.py create mode 100644 foundry/vcs/hg/providers.py create mode 100644 foundry/views/__init__.py create mode 100644 foundry/views/changelog.tpl create mode 100755 setup.py create mode 100644 tests/test_frozendict.py create mode 100644 tests/test_implements.py diff --git a/foundry/__init__.py b/foundry/__init__.py new file mode 100644 index 0000000..c71d252 --- /dev/null +++ b/foundry/__init__.py @@ -0,0 +1,11 @@ +# vim: set filencoding=utf8 +""" +Foundary + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 +""" + + +__version__ = "0.1" diff --git a/foundry/application.py b/foundry/application.py new file mode 100644 index 0000000..bc4a569 --- /dev/null +++ b/foundry/application.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# vim: set filencoding=utf8 +""" +Foundry Main Application + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 + +Entry point for the application. All of the main app +wireup happens here. This is also where you can get +the WSGI application object. This isn't strictly +required. You could wire this all up by hand. +""" + +import jinja2 +from snakeguice import Injector +from snakeguice.extras.snakeweb import Application, AutoRoutesModule + +from foundry.utils import frozendict +from foundry import controllers, interfaces +from foundry.views import JinjaRenderer +from foundry.template_filters import TEMPLATE_FILTERS + +from foundry.vcs.hg.providers import RepoProvider + + +class MainModule(object): + + def configure(self, binder): + #------------------------------------------------------------- + # Mercurial Bindings + #------------------------------------------------------------- + binder.bind(interfaces.RepositoryProvider, to=RepoProvider) + + #------------------------------------------------------------- + # Template Engine Bindings + #------------------------------------------------------------- + loader = jinja2.PackageLoader('foundry', 'views') + tpl_env = jinja2.Environment(loader=loader) + tpl_env.filters.update(TEMPLATE_FILTERS) + renderer = JinjaRenderer(tpl_env) + + binder.bind(interfaces.TemplateRenderer, to_instance=renderer) + + +class MapperModule(AutoRoutesModule): + + configured_routes = frozendict({ + '/': controllers.ChangelogController, + }) + + +def get_application(): + injector = Injector([MainModule(), MapperModule()]) + return Application(injector) + + +if __name__ == '__main__': + from wsgiref.simple_server import make_server + + httpd = make_server('', 8080, get_application()) + httpd.serve_forever() diff --git a/foundry/controllers/__init__.py b/foundry/controllers/__init__.py new file mode 100644 index 0000000..b17b3e9 --- /dev/null +++ b/foundry/controllers/__init__.py @@ -0,0 +1,33 @@ +from webob import Response +from datetime import datetime +from foundry import interfaces +from foundry.di import inject, Injected + + +class Changeset(object): + + def __init__(self, changeset): + self.changeset = changeset + + def __getattr__(self, key): + return getattr(self.changeset, key) + + def date(self): + return datetime.fromtimestamp(float(self.changeset.date()[0])) + + +class ChangelogController(object): + + @inject(repo=interfaces.RepositoryProvider, + renderer=interfaces.TemplateRenderer) + def __init__(self, repo=Injected, renderer=Injected): + self.repo = repo.get('/Users/mcrute') + self.renderer = renderer + + def __call__(self, request): + def _repo_iter(): + for rev in self.repo: + yield Changeset(self.repo[rev]) + + return Response(self.renderer.render('changelog.tpl', + repo=_repo_iter())) diff --git a/foundry/di.py b/foundry/di.py new file mode 100644 index 0000000..d283871 --- /dev/null +++ b/foundry/di.py @@ -0,0 +1,16 @@ +# vim: set filencoding=utf8 +""" +Dependency Injection Utils + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 + +This module provides a convenient place to replace the +core DI functions in the case that somebody would want +to manually assemble the application without the help +of a DI system. +""" + + +from snakeguice import inject, Injected diff --git a/foundry/interfaces.py b/foundry/interfaces.py new file mode 100644 index 0000000..4717778 --- /dev/null +++ b/foundry/interfaces.py @@ -0,0 +1,23 @@ +# vim: set filencoding=utf8 +""" +Foundry Interfaces + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 +""" + + +class RepositoryProvider: + """ + Repository providers return an instance of a repository. + """ + + def get(self, repo_path=''): + pass + + +class TemplateRenderer: + + def render(self, template, *args, **kwargs): + pass diff --git a/foundry/router.py b/foundry/router.py new file mode 100644 index 0000000..233fa30 --- /dev/null +++ b/foundry/router.py @@ -0,0 +1,22 @@ +# vim: set filencoding=utf8 +""" +RESTful URL Router + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 20, 2010 +""" + + +class Renderer(object): + pass + + +class JSONRenderer(Renderer): + + can_handle = ('application/json', 'text/json') + + + +class Resource(object): + pass diff --git a/foundry/template_filters.py b/foundry/template_filters.py new file mode 100644 index 0000000..6dec2e7 --- /dev/null +++ b/foundry/template_filters.py @@ -0,0 +1,53 @@ +# vim: set filencoding=utf8 +""" +Foundry Template Filters + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 03, 2010 +""" + + +from datetime import datetime +from foundry.utils import frozendict + + +def pluralize(word, how_many=1): + "Naive pluralization function." + return word if how_many == 1 else word + "s" + + +def nice_date_delta(from_date, to_date=datetime.now()): + """ + Provides a friendly text representation (ie. 7 months) + for the delta between two dates. + """ + delta = to_date - from_date + + months = delta.days / 30 + if months > 0: + return "{0} {1}".format(months, pluralize("month", months)) + + weeks = delta.days / 7 + if weeks > 0: + return "{0} {1}".format(weeks, pluralize("week", weeks)) + + if delta.days > 0: + return "{0} {1}".format(delta.days, pluralize("day", delta.days)) + + hours = delta.seconds / (60 * 60) + if hours > 0: + return "{0} {1}".format(hours, pluralize("hour", hours)) + + minutes = delta.seconds / 60 + if minutes > 0: + return "{0} {1}".format(minutes, pluralize("minute", minutes)) + + return "seconds ago" + + +#: Template filter registry +TEMPLATE_FILTERS = frozendict({ + 'nice_date_delta': nice_date_delta, + 'pluralize': pluralize, +}) diff --git a/foundry/test_router.py b/foundry/test_router.py new file mode 100644 index 0000000..15e97fd --- /dev/null +++ b/foundry/test_router.py @@ -0,0 +1,159 @@ +from router import Router, Resource +from router import JSONRenderer, HTMLRenderer, XMLRenderer, AtomRenderer + + +router = Router() + +# Add Renderers +router.add_renderer(JSONRenderer, default=True) +router.add_renderer(HTMLRenderer) +router.add_renderer(AtomRenderer) + +router.add_auth_source(DbAuthenticator('users.db')) +router.add_authenz_source(DbAuthorizor('users.db')) + +revision_spec = FragmentSpec(required=False, default='tip', + regex='(tip|[0-9a-zA-Z]+)') + +# Add Resource Mappings +Route('/{project_name}', ProjectSummaryResource, [ + Route('/summary', ProjectSummaryResource), + Route('/shortlog/{revision}', ShortLogResource, + uri_spec={ 'revision': revision_spec }), + Route('/graph/{revision}', GraphResource, + uri_spec={ 'revision': revision_spec }), + Route('/raw-rev/{revision}', RawRevisionResource, + uri_spec={ 'revision': revision_spec }), + Route('/tags', TagsResource), + Route('/annotate/{revision}/{filename}', AnnotateResource, + uri_spec={ 'revision': revision_spec, + 'filename': FragmentSpec(required=False) }), + Route(('/diff/{revision}/{filename}', '/filediff/{revision}/{filename}'), + DiffResource, + uri_spec={ 'revision': revision_spec, + 'filename': FragmentSpec(required=False) }), + Route('/raw-file/{revision}/{filename}', RawFileResource, + uri_spec={ 'revision': revision_spec, + 'filename': FragmentSpec(required=False) }), + Route('/branches', BranchesResource), + Route('/archive/{revision}.tar.{format}', ArchiveResource, + uri_spec={ 'revision': FragmentSpec(regex='(tip|[0-9a-zA-Z]+)'), + 'format': FragmentSpec(value_list=('gz', 'bz2') }), + Route(('/rev/{revision}','/changeset/{revision}'), RevisionResource, + uri_spec={ 'revision': revision_spec }), + Route(('/log/{revision}', '/changelog/{revision}', '/filelog/{revision}'), + LogResource, + uri_spec={ 'revision': revision_spec }), + Route('/file/{revision}/{filename}', FileResource, + uri_spec={ 'revision': revision_spec, + 'filename': FragmentSpec(required=False) }), + ]) + + +class constant(object): + """ + Constant descriptor to provide a level of protection against + changes to constants in class instances. Does not prevent + changes directly to constants in non-instances. + """ + + def __init__(self, value): + self.value = value + + def __get__(self, instance, owner): + return self.value + + def __set__(self, instance, value): + raise ValueError('Can not assign to constant.') + + def __delete__(self, instance): + raise ValueError('Can not delete constant.') + + +class Route(object): + + # ---------------------------------------------------------------- + # Constants + # ---------------------------------------------------------------- + REDIRECT = constant('redirect') + FAILURE = constant('failure') + + # ---------------------------------------------------------------- + # Required Parameters + # ---------------------------------------------------------------- + + #: Part of the URI that will map to this route + uri_part = None + + #: Resource existing at this route + resource = None + + #: Resource requires that the user is using an HTTPS connection + #: can be True, False, REDIRECT or FAILRE. If True and no HTTPS + #: the route will fail to match. If REDIRECT and no HTTPS the + #: client will be redirect to the secure version of the resouce. + #: If FAILURE a 403 (Forbidden) error will be returned to the + #: client. + https = False + + #: Route requires authentication + requires_authentication = True + + # ---------------------------------------------------------------- + # Optional, if unspecified these will not be used + # ---------------------------------------------------------------- + + #: Dictionary of uri fragment names to FragmentSpec objects used + #: to validate the individual uri fragments + uri_spec = None + + #: Callable or list of callables to which the matching resouce + #: and context will be passed, the first failure will cause the + #: route to not match. + conditions = None + + #: Type of permission required. The router doesn't care what + #: this object is as long as the security system understands it + permissions_required = None + + # ---------------------------------------------------------------- + # Optional, if unspecified these will use the router defaults + # ---------------------------------------------------------------- + #: List of content-types supported by this route + content_types = None + + #: Source for authentication + auth_source = None + + #: Source for authorization + authz_source = None + + +class FragmentSpec(object): + + #: Indicates if the framgent is required + required = True + + #: Regex used to check if the fragment is valid if specified + #: value_list may not be specified. + regex = r'.*' + + #: Optional list of acceptable values. May not be specified + #: with regex + value_list = None + + #: Default value if the user provides no value + default = None + + +class NotSupported(object): + pass + + +class Resource(object): + + GET = NotSupported() + POST = NotSupported() + HEAD = NotSupported() + PUT = NotSupported() + DELETE = NotSupported() diff --git a/foundry/utils.py b/foundry/utils.py new file mode 100644 index 0000000..b5d58b6 --- /dev/null +++ b/foundry/utils.py @@ -0,0 +1,106 @@ +# vim: set filencoding=utf8 +""" +Random Stuff + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 + +Random stuff that doesn't deserve it's own module +but is still needed for the rest of the program. +""" + +class frozendict(dict): + """ + A frozen dictionary implementation can not be modified once + it has been constructed, much like a tuple. Frozen dictionaries + are hashable. + """ + + def __new__(cls, indict): + inst = dict.__new__(cls) + inst.__hash = hash(tuple(sorted(indict.items()))) + inst.__slots__ = indict.keys() + dict.__init__(inst, indict) + return inst + + @property + def __blocked(self): + raise AttributeError("Can't modify frozendict instance.") + + __delitem__ = __setitem__ = clear = pop = __blocked + popitem = setdefault = update = __blocked + + __hash__ = lambda self: self.__hash + __repr__ = lambda self: "frozendict({1})".format(dict.__repr__(self)) + + +def implements(interface, debug=False): + """ + Verify that a class conforms to a specified interface. + This decorator is not perfect, for example it can not + check exceptions or return values. But it does ensure + that all public methods exist and their arguments + conform to the interface. + + The debug flag allows overriding checking of the runtime + flag for testing purposes, it should never be set in + production code. + + NOTE: This decorator does nothing if -d is not passed + to the Python runtime. + """ + import sys + if not sys.flags.debug and not debug: + return lambda func: func + + # Defer this import until we know we're supposed to run + import inspect + + def get_filtered_members(item): + "Gets non-private or non-protected symbols." + return dict([(key, value) for key, value in inspect.getmembers(item) + if not key.startswith('_')]) + + def build_contract(item): + """ + Builds a function contract string. The contract + string will ignore the name of positional params + but will consider the name of keyword arguments. + """ + argspec = inspect.getargspec(item) + + if argspec.defaults: + num_keywords = len(argspec.defaults) + args = ['_'] * (len(argspec.args) - num_keywords) + args.extend(argspec.args[num_keywords-1:]) + else: + args = ['_'] * len(argspec.args) + + if argspec.varargs: + args.append('*args') + + if argspec.keywords: + args.append('**kwargs') + + return ', '.join(args) + + def tester(klass): + "Verifies conformance to the interface." + interface_elements = get_filtered_members(interface) + class_elements = get_filtered_members(klass) + + for key, value in interface_elements.items(): + assert key in class_elements, \ + "{0!r} is required but missing.".format(key) + + if inspect.isfunction(value) or inspect.ismethod(value): + contract = build_contract(value) + implementation = build_contract(class_elements[key]) + + assert implementation == contract, \ + "{0!r} doesn't conform to interface.".format(key) + + return klass + + return tester diff --git a/foundry/vcs/__init__.py b/foundry/vcs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/foundry/vcs/hg/__init__.py b/foundry/vcs/hg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/foundry/vcs/hg/providers.py b/foundry/vcs/hg/providers.py new file mode 100644 index 0000000..232464e --- /dev/null +++ b/foundry/vcs/hg/providers.py @@ -0,0 +1,24 @@ +# vim: set filencoding=utf8 +""" +SnakeGuice Providers + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 +""" + +from mercurial import hg +from mercurial.ui import ui +from foundry.utils import implements +from foundry.interfaces import RepositoryProvider + + +@implements(RepositoryProvider) +class RepoProvider(object): + + def get(self, repo_path=''): + u = ui() + u.setconfig('ui', 'report_untrusted', 'off') + u.setconfig('ui', 'interactive', 'false') + + return hg.repository(u, repo_path) diff --git a/foundry/views/__init__.py b/foundry/views/__init__.py new file mode 100644 index 0000000..dd26dfe --- /dev/null +++ b/foundry/views/__init__.py @@ -0,0 +1,23 @@ +# vim: set filencoding=utf8 +""" +Template Renderer + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 03, 2010 +""" + + +from foundry import interfaces +from foundry.utils import implements + + +@implements(interfaces.TemplateRenderer) +class JinjaRenderer(object): + + def __init__(self, tpl_env): + self.tpl_env = tpl_env + + def render(self, template, *args, **kwargs): + template = self.tpl_env.get_template(template) + return template.render(*args, **kwargs) diff --git a/foundry/views/changelog.tpl b/foundry/views/changelog.tpl new file mode 100644 index 0000000..f6d3a75 --- /dev/null +++ b/foundry/views/changelog.tpl @@ -0,0 +1,42 @@ + + + + + + + + +{% for changeset in repo %} + + + + + +{% endfor %} +
ChangedUserSummary
{{ changeset.date()|nice_date_delta }}{{ changeset.user() }} + {{ changeset.description() }} + {% for tag in changeset.tags() %} + {{ tag }} + {% endfor %} + {% if changeset.branch() != 'default' %} + {{ changeset.branch() }} + {% endif %} +
diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..eacf87f --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +from setuptools import setup + +setup( + name="Foundry", + description="", + author="Mike Crute", + author_email="mcrute@gmail.com", + license="BSD", + version="0.1", + zip_safe=False, + include_package_data=True, + install_requires=[ + "Routes", + "Webob", + "Jinja2", + "SQLAlchemy", + ]) diff --git a/tests/test_frozendict.py b/tests/test_frozendict.py new file mode 100644 index 0000000..3f8045d --- /dev/null +++ b/tests/test_frozendict.py @@ -0,0 +1,19 @@ +from foundry.utils import frozendict +from nose.tools import raises + + +class TestFrozenDict(object): + + def setup(self): + self.inputs = { 'foo': 'bar' } + self.dict_ = frozendict(self.inputs) + + @raises(AttributeError) + def test_should_not_allow_assignment(self): + self.dict_['bar'] = 'baz' + + def test_should_use_precomputed_hash(self): + assert hash(self.dict_) == self.dict_._frozendict__hash + + def test_should_set_slots(self): + assert self.dict_.__slots__ == self.inputs.keys() diff --git a/tests/test_implements.py b/tests/test_implements.py new file mode 100644 index 0000000..48ffe4b --- /dev/null +++ b/tests/test_implements.py @@ -0,0 +1,51 @@ +# vim: set filencoding=utf8 +""" +Implements Decorator Tests + +@author: Mike Crute (mcrute@gmail.com) +@organization: SoftGroup Interactive, Inc. +@date: May 02, 2010 +""" + + +from foundry.utils import implements +from nose.tools import raises + + +class MyInterface(object): + + def get(self, foo): + pass + + def set(self, bar, baz=None): + pass + + def remove(self, *args, **kwargs): + pass + + +def test_conforming_should_not_fail(): + @implements(MyInterface, debug=True) + class Conforming(object): + + def get(self, foo): + pass + + def set(self, bar, baz=None): + pass + + def remove(self, *args, **kwargs): + pass + + +@raises(AssertionError) +def test_non_conforming_should_fail(): + @implements(MyInterface, debug=True) + class NonConforming(object): + pass + + +def test_non_debug_should_do_nothing(): + @implements(MyInterface) + class NonConforming(object): + pass -- cgit v1.2.3