summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2015-07-28 21:15:06 -0700
committerMike Crute <mcrute@gmail.com>2015-07-28 21:15:06 -0700
commit6da717bf333dc82c67d7aece3fd36f97090040f8 (patch)
tree056bb5e2ef5642d836425c2ac85212d3328364bb
downloadfoundry-master.tar.bz2
foundry-master.tar.xz
foundry-master.zip
Initial importHEADmaster
-rw-r--r--foundry/__init__.py11
-rw-r--r--foundry/application.py63
-rw-r--r--foundry/controllers/__init__.py33
-rw-r--r--foundry/di.py16
-rw-r--r--foundry/interfaces.py23
-rw-r--r--foundry/router.py22
-rw-r--r--foundry/template_filters.py53
-rw-r--r--foundry/test_router.py159
-rw-r--r--foundry/utils.py106
-rw-r--r--foundry/vcs/__init__.py0
-rw-r--r--foundry/vcs/hg/__init__.py0
-rw-r--r--foundry/vcs/hg/providers.py24
-rw-r--r--foundry/views/__init__.py23
-rw-r--r--foundry/views/changelog.tpl42
-rwxr-xr-xsetup.py18
-rw-r--r--tests/test_frozendict.py19
-rw-r--r--tests/test_implements.py51
17 files changed, 663 insertions, 0 deletions
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 @@
1# vim: set filencoding=utf8
2"""
3Foundary
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 02, 2010
8"""
9
10
11__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 @@
1#!/usr/bin/env python
2# vim: set filencoding=utf8
3"""
4Foundry Main Application
5
6@author: Mike Crute (mcrute@gmail.com)
7@organization: SoftGroup Interactive, Inc.
8@date: May 02, 2010
9
10Entry point for the application. All of the main app
11wireup happens here. This is also where you can get
12the WSGI application object. This isn't strictly
13required. You could wire this all up by hand.
14"""
15
16import jinja2
17from snakeguice import Injector
18from snakeguice.extras.snakeweb import Application, AutoRoutesModule
19
20from foundry.utils import frozendict
21from foundry import controllers, interfaces
22from foundry.views import JinjaRenderer
23from foundry.template_filters import TEMPLATE_FILTERS
24
25from foundry.vcs.hg.providers import RepoProvider
26
27
28class MainModule(object):
29
30 def configure(self, binder):
31 #-------------------------------------------------------------
32 # Mercurial Bindings
33 #-------------------------------------------------------------
34 binder.bind(interfaces.RepositoryProvider, to=RepoProvider)
35
36 #-------------------------------------------------------------
37 # Template Engine Bindings
38 #-------------------------------------------------------------
39 loader = jinja2.PackageLoader('foundry', 'views')
40 tpl_env = jinja2.Environment(loader=loader)
41 tpl_env.filters.update(TEMPLATE_FILTERS)
42 renderer = JinjaRenderer(tpl_env)
43
44 binder.bind(interfaces.TemplateRenderer, to_instance=renderer)
45
46
47class MapperModule(AutoRoutesModule):
48
49 configured_routes = frozendict({
50 '/': controllers.ChangelogController,
51 })
52
53
54def get_application():
55 injector = Injector([MainModule(), MapperModule()])
56 return Application(injector)
57
58
59if __name__ == '__main__':
60 from wsgiref.simple_server import make_server
61
62 httpd = make_server('', 8080, get_application())
63 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 @@
1from webob import Response
2from datetime import datetime
3from foundry import interfaces
4from foundry.di import inject, Injected
5
6
7class Changeset(object):
8
9 def __init__(self, changeset):
10 self.changeset = changeset
11
12 def __getattr__(self, key):
13 return getattr(self.changeset, key)
14
15 def date(self):
16 return datetime.fromtimestamp(float(self.changeset.date()[0]))
17
18
19class ChangelogController(object):
20
21 @inject(repo=interfaces.RepositoryProvider,
22 renderer=interfaces.TemplateRenderer)
23 def __init__(self, repo=Injected, renderer=Injected):
24 self.repo = repo.get('/Users/mcrute')
25 self.renderer = renderer
26
27 def __call__(self, request):
28 def _repo_iter():
29 for rev in self.repo:
30 yield Changeset(self.repo[rev])
31
32 return Response(self.renderer.render('changelog.tpl',
33 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 @@
1# vim: set filencoding=utf8
2"""
3Dependency Injection Utils
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 02, 2010
8
9This module provides a convenient place to replace the
10core DI functions in the case that somebody would want
11to manually assemble the application without the help
12of a DI system.
13"""
14
15
16from 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 @@
1# vim: set filencoding=utf8
2"""
3Foundry Interfaces
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 02, 2010
8"""
9
10
11class RepositoryProvider:
12 """
13 Repository providers return an instance of a repository.
14 """
15
16 def get(self, repo_path=''):
17 pass
18
19
20class TemplateRenderer:
21
22 def render(self, template, *args, **kwargs):
23 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 @@
1# vim: set filencoding=utf8
2"""
3RESTful URL Router
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 20, 2010
8"""
9
10
11class Renderer(object):
12 pass
13
14
15class JSONRenderer(Renderer):
16
17 can_handle = ('application/json', 'text/json')
18
19
20
21class Resource(object):
22 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 @@
1# vim: set filencoding=utf8
2"""
3Foundry Template Filters
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 03, 2010
8"""
9
10
11from datetime import datetime
12from foundry.utils import frozendict
13
14
15def pluralize(word, how_many=1):
16 "Naive pluralization function."
17 return word if how_many == 1 else word + "s"
18
19
20def nice_date_delta(from_date, to_date=datetime.now()):
21 """
22 Provides a friendly text representation (ie. 7 months)
23 for the delta between two dates.
24 """
25 delta = to_date - from_date
26
27 months = delta.days / 30
28 if months > 0:
29 return "{0} {1}".format(months, pluralize("month", months))
30
31 weeks = delta.days / 7
32 if weeks > 0:
33 return "{0} {1}".format(weeks, pluralize("week", weeks))
34
35 if delta.days > 0:
36 return "{0} {1}".format(delta.days, pluralize("day", delta.days))
37
38 hours = delta.seconds / (60 * 60)
39 if hours > 0:
40 return "{0} {1}".format(hours, pluralize("hour", hours))
41
42 minutes = delta.seconds / 60
43 if minutes > 0:
44 return "{0} {1}".format(minutes, pluralize("minute", minutes))
45
46 return "seconds ago"
47
48
49#: Template filter registry
50TEMPLATE_FILTERS = frozendict({
51 'nice_date_delta': nice_date_delta,
52 'pluralize': pluralize,
53})
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 @@
1from router import Router, Resource
2from router import JSONRenderer, HTMLRenderer, XMLRenderer, AtomRenderer
3
4
5router = Router()
6
7# Add Renderers
8router.add_renderer(JSONRenderer, default=True)
9router.add_renderer(HTMLRenderer)
10router.add_renderer(AtomRenderer)
11
12router.add_auth_source(DbAuthenticator('users.db'))
13router.add_authenz_source(DbAuthorizor('users.db'))
14
15revision_spec = FragmentSpec(required=False, default='tip',
16 regex='(tip|[0-9a-zA-Z]+)')
17
18# Add Resource Mappings
19Route('/{project_name}', ProjectSummaryResource, [
20 Route('/summary', ProjectSummaryResource),
21 Route('/shortlog/{revision}', ShortLogResource,
22 uri_spec={ 'revision': revision_spec }),
23 Route('/graph/{revision}', GraphResource,
24 uri_spec={ 'revision': revision_spec }),
25 Route('/raw-rev/{revision}', RawRevisionResource,
26 uri_spec={ 'revision': revision_spec }),
27 Route('/tags', TagsResource),
28 Route('/annotate/{revision}/{filename}', AnnotateResource,
29 uri_spec={ 'revision': revision_spec,
30 'filename': FragmentSpec(required=False) }),
31 Route(('/diff/{revision}/{filename}', '/filediff/{revision}/{filename}'),
32 DiffResource,
33 uri_spec={ 'revision': revision_spec,
34 'filename': FragmentSpec(required=False) }),
35 Route('/raw-file/{revision}/{filename}', RawFileResource,
36 uri_spec={ 'revision': revision_spec,
37 'filename': FragmentSpec(required=False) }),
38 Route('/branches', BranchesResource),
39 Route('/archive/{revision}.tar.{format}', ArchiveResource,
40 uri_spec={ 'revision': FragmentSpec(regex='(tip|[0-9a-zA-Z]+)'),
41 'format': FragmentSpec(value_list=('gz', 'bz2') }),
42 Route(('/rev/{revision}','/changeset/{revision}'), RevisionResource,
43 uri_spec={ 'revision': revision_spec }),
44 Route(('/log/{revision}', '/changelog/{revision}', '/filelog/{revision}'),
45 LogResource,
46 uri_spec={ 'revision': revision_spec }),
47 Route('/file/{revision}/{filename}', FileResource,
48 uri_spec={ 'revision': revision_spec,
49 'filename': FragmentSpec(required=False) }),
50 ])
51
52
53class constant(object):
54 """
55 Constant descriptor to provide a level of protection against
56 changes to constants in class instances. Does not prevent
57 changes directly to constants in non-instances.
58 """
59
60 def __init__(self, value):
61 self.value = value
62
63 def __get__(self, instance, owner):
64 return self.value
65
66 def __set__(self, instance, value):
67 raise ValueError('Can not assign to constant.')
68
69 def __delete__(self, instance):
70 raise ValueError('Can not delete constant.')
71
72
73class Route(object):
74
75 # ----------------------------------------------------------------
76 # Constants
77 # ----------------------------------------------------------------
78 REDIRECT = constant('redirect')
79 FAILURE = constant('failure')
80
81 # ----------------------------------------------------------------
82 # Required Parameters
83 # ----------------------------------------------------------------
84
85 #: Part of the URI that will map to this route
86 uri_part = None
87
88 #: Resource existing at this route
89 resource = None
90
91 #: Resource requires that the user is using an HTTPS connection
92 #: can be True, False, REDIRECT or FAILRE. If True and no HTTPS
93 #: the route will fail to match. If REDIRECT and no HTTPS the
94 #: client will be redirect to the secure version of the resouce.
95 #: If FAILURE a 403 (Forbidden) error will be returned to the
96 #: client.
97 https = False
98
99 #: Route requires authentication
100 requires_authentication = True
101
102 # ----------------------------------------------------------------
103 # Optional, if unspecified these will not be used
104 # ----------------------------------------------------------------
105
106 #: Dictionary of uri fragment names to FragmentSpec objects used
107 #: to validate the individual uri fragments
108 uri_spec = None
109
110 #: Callable or list of callables to which the matching resouce
111 #: and context will be passed, the first failure will cause the
112 #: route to not match.
113 conditions = None
114
115 #: Type of permission required. The router doesn't care what
116 #: this object is as long as the security system understands it
117 permissions_required = None
118
119 # ----------------------------------------------------------------
120 # Optional, if unspecified these will use the router defaults
121 # ----------------------------------------------------------------
122 #: List of content-types supported by this route
123 content_types = None
124
125 #: Source for authentication
126 auth_source = None
127
128 #: Source for authorization
129 authz_source = None
130
131
132class FragmentSpec(object):
133
134 #: Indicates if the framgent is required
135 required = True
136
137 #: Regex used to check if the fragment is valid if specified
138 #: value_list may not be specified.
139 regex = r'.*'
140
141 #: Optional list of acceptable values. May not be specified
142 #: with regex
143 value_list = None
144
145 #: Default value if the user provides no value
146 default = None
147
148
149class NotSupported(object):
150 pass
151
152
153class Resource(object):
154
155 GET = NotSupported()
156 POST = NotSupported()
157 HEAD = NotSupported()
158 PUT = NotSupported()
159 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 @@
1# vim: set filencoding=utf8
2"""
3Random Stuff
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 02, 2010
8
9Random stuff that doesn't deserve it's own module
10but is still needed for the rest of the program.
11"""
12
13class frozendict(dict):
14 """
15 A frozen dictionary implementation can not be modified once
16 it has been constructed, much like a tuple. Frozen dictionaries
17 are hashable.
18 """
19
20 def __new__(cls, indict):
21 inst = dict.__new__(cls)
22 inst.__hash = hash(tuple(sorted(indict.items())))
23 inst.__slots__ = indict.keys()
24 dict.__init__(inst, indict)
25 return inst
26
27 @property
28 def __blocked(self):
29 raise AttributeError("Can't modify frozendict instance.")
30
31 __delitem__ = __setitem__ = clear = pop = __blocked
32 popitem = setdefault = update = __blocked
33
34 __hash__ = lambda self: self.__hash
35 __repr__ = lambda self: "frozendict({1})".format(dict.__repr__(self))
36
37
38def implements(interface, debug=False):
39 """
40 Verify that a class conforms to a specified interface.
41 This decorator is not perfect, for example it can not
42 check exceptions or return values. But it does ensure
43 that all public methods exist and their arguments
44 conform to the interface.
45
46 The debug flag allows overriding checking of the runtime
47 flag for testing purposes, it should never be set in
48 production code.
49
50 NOTE: This decorator does nothing if -d is not passed
51 to the Python runtime.
52 """
53 import sys
54 if not sys.flags.debug and not debug:
55 return lambda func: func
56
57 # Defer this import until we know we're supposed to run
58 import inspect
59
60 def get_filtered_members(item):
61 "Gets non-private or non-protected symbols."
62 return dict([(key, value) for key, value in inspect.getmembers(item)
63 if not key.startswith('_')])
64
65 def build_contract(item):
66 """
67 Builds a function contract string. The contract
68 string will ignore the name of positional params
69 but will consider the name of keyword arguments.
70 """
71 argspec = inspect.getargspec(item)
72
73 if argspec.defaults:
74 num_keywords = len(argspec.defaults)
75 args = ['_'] * (len(argspec.args) - num_keywords)
76 args.extend(argspec.args[num_keywords-1:])
77 else:
78 args = ['_'] * len(argspec.args)
79
80 if argspec.varargs:
81 args.append('*args')
82
83 if argspec.keywords:
84 args.append('**kwargs')
85
86 return ', '.join(args)
87
88 def tester(klass):
89 "Verifies conformance to the interface."
90 interface_elements = get_filtered_members(interface)
91 class_elements = get_filtered_members(klass)
92
93 for key, value in interface_elements.items():
94 assert key in class_elements, \
95 "{0!r} is required but missing.".format(key)
96
97 if inspect.isfunction(value) or inspect.ismethod(value):
98 contract = build_contract(value)
99 implementation = build_contract(class_elements[key])
100
101 assert implementation == contract, \
102 "{0!r} doesn't conform to interface.".format(key)
103
104 return klass
105
106 return tester
diff --git a/foundry/vcs/__init__.py b/foundry/vcs/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/foundry/vcs/__init__.py
diff --git a/foundry/vcs/hg/__init__.py b/foundry/vcs/hg/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/foundry/vcs/hg/__init__.py
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 @@
1# vim: set filencoding=utf8
2"""
3SnakeGuice Providers
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 02, 2010
8"""
9
10from mercurial import hg
11from mercurial.ui import ui
12from foundry.utils import implements
13from foundry.interfaces import RepositoryProvider
14
15
16@implements(RepositoryProvider)
17class RepoProvider(object):
18
19 def get(self, repo_path=''):
20 u = ui()
21 u.setconfig('ui', 'report_untrusted', 'off')
22 u.setconfig('ui', 'interactive', 'false')
23
24 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 @@
1# vim: set filencoding=utf8
2"""
3Template Renderer
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 03, 2010
8"""
9
10
11from foundry import interfaces
12from foundry.utils import implements
13
14
15@implements(interfaces.TemplateRenderer)
16class JinjaRenderer(object):
17
18 def __init__(self, tpl_env):
19 self.tpl_env = tpl_env
20
21 def render(self, template, *args, **kwargs):
22 template = self.tpl_env.get_template(template)
23 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 @@
1<style type="text/css">
2.branchtag, .tagtag
3{
4 border: 1px solid;
5 padding: 2px 6px;
6}
7
8.branchtag
9{
10 background-color: #AFA;
11 border-color: #CFC #0C3 #0C3 #CFC;
12}
13
14.tagtag
15{
16 background-color: #FFA;
17 border-color: #FFC #FE0 #FE0 #FFC;
18}
19</style>
20
21<table>
22<tr>
23 <th>Changed</th>
24 <th>User</th>
25 <th>Summary</th>
26</tr>
27{% for changeset in repo %}
28<tr>
29 <td>{{ changeset.date()|nice_date_delta }}</td>
30 <td>{{ changeset.user() }}</td>
31 <td>
32 {{ changeset.description() }}
33 {% for tag in changeset.tags() %}
34 <span class="tagtag">{{ tag }}</span>
35 {% endfor %}
36 {% if changeset.branch() != 'default' %}
37 <span class="branchtag">{{ changeset.branch() }}</span>
38 {% endif %}
39 </td>
40</tr>
41{% endfor %}
42</table>
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..eacf87f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,18 @@
1#!/usr/bin/env python
2from setuptools import setup
3
4setup(
5 name="Foundry",
6 description="",
7 author="Mike Crute",
8 author_email="mcrute@gmail.com",
9 license="BSD",
10 version="0.1",
11 zip_safe=False,
12 include_package_data=True,
13 install_requires=[
14 "Routes",
15 "Webob",
16 "Jinja2",
17 "SQLAlchemy",
18 ])
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 @@
1from foundry.utils import frozendict
2from nose.tools import raises
3
4
5class TestFrozenDict(object):
6
7 def setup(self):
8 self.inputs = { 'foo': 'bar' }
9 self.dict_ = frozendict(self.inputs)
10
11 @raises(AttributeError)
12 def test_should_not_allow_assignment(self):
13 self.dict_['bar'] = 'baz'
14
15 def test_should_use_precomputed_hash(self):
16 assert hash(self.dict_) == self.dict_._frozendict__hash
17
18 def test_should_set_slots(self):
19 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 @@
1# vim: set filencoding=utf8
2"""
3Implements Decorator Tests
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: May 02, 2010
8"""
9
10
11from foundry.utils import implements
12from nose.tools import raises
13
14
15class MyInterface(object):
16
17 def get(self, foo):
18 pass
19
20 def set(self, bar, baz=None):
21 pass
22
23 def remove(self, *args, **kwargs):
24 pass
25
26
27def test_conforming_should_not_fail():
28 @implements(MyInterface, debug=True)
29 class Conforming(object):
30
31 def get(self, foo):
32 pass
33
34 def set(self, bar, baz=None):
35 pass
36
37 def remove(self, *args, **kwargs):
38 pass
39
40
41@raises(AssertionError)
42def test_non_conforming_should_fail():
43 @implements(MyInterface, debug=True)
44 class NonConforming(object):
45 pass
46
47
48def test_non_debug_should_do_nothing():
49 @implements(MyInterface)
50 class NonConforming(object):
51 pass