Skip to content
Browse files

Initial import

  • Loading branch information...
1 parent 9e3b8b9 commit 76c52f40a7ed722b5f5c0fb753ecbf7ef08580ef Guillaume Gauvrit committed Dec 13, 2012
Showing with 2,416 additions and 2 deletions.
  1. +3 −1 .gitignore
  2. +8 −0 CHANGES.txt
  3. +24 −0 LICENSE
  4. +2 −0 MANIFEST.in
  5. +75 −1 README.md
  6. +82 −0 development.ini
  7. +6 −0 message-extraction.ini
  8. +82 −0 production.ini
  9. +42 −0 pyshop/__init__.py
  10. 0 pyshop/assets/sass/00_vars.scss
  11. 0 pyshop/assets/sass/01_macro.scss
  12. 0 pyshop/assets/sass/02_reset.scss
  13. 0 pyshop/assets/sass/10_layout.scss
  14. +5 −0 pyshop/assets/sass/index.scss
  15. +62 −0 pyshop/bin/install.py
  16. +60 −0 pyshop/bin/shell.py
  17. +137 −0 pyshop/config.py
  18. 0 pyshop/helpers/__init__.py
  19. +60 −0 pyshop/helpers/authentication.py
  20. +55 −0 pyshop/helpers/download.py
  21. +23 −0 pyshop/helpers/i18n.py
  22. +22 −0 pyshop/helpers/pypi.py
  23. +10 −0 pyshop/helpers/restxt.py
  24. +142 −0 pyshop/helpers/sqla.py
  25. +25 −0 pyshop/helpers/task.py
  26. +19 −0 pyshop/locale/fr/LC_MESSAGES/pyshop.po
  27. +19 −0 pyshop/locale/pyshop.pot
  28. +296 −0 pyshop/models.py
  29. +31 −0 pyshop/resources.py
  30. +27 −0 pyshop/security.py
  31. BIN pyshop/static/favicon.ico
  32. BIN pyshop/static/pyramid-small.png
  33. BIN pyshop/static/pyramid.png
  34. +7 −0 pyshop/templates/macro/nav.html
  35. +8 −0 pyshop/templates/pyshop/layout.html
  36. +5 −0 pyshop/templates/pyshop/nav.html
  37. +40 −0 pyshop/templates/pyshop/package/list.html
  38. +44 −0 pyshop/templates/pyshop/package/show.html
  39. +4 −0 pyshop/templates/pyshop/simple/list.html
  40. +15 −0 pyshop/templates/pyshop/simple/show.html
  41. +20 −0 pyshop/templates/shared/base_layout.html
  42. +8 −0 pyshop/templates/shared/footer_buttons.html
  43. +15 −0 pyshop/templates/shared/form_layout.html
  44. +75 −0 pyshop/templates/shared/layout.html
  45. +23 −0 pyshop/templates/shared/login.html
  46. +20 −0 pyshop/views/__init__.py
  47. +71 −0 pyshop/views/base.py
  48. +68 −0 pyshop/views/credentials.py
  49. +6 −0 pyshop/views/json/__init__.py
  50. +70 −0 pyshop/views/json/pypi.py
  51. +30 −0 pyshop/views/package.py
  52. +24 −0 pyshop/views/repository.py
  53. +227 −0 pyshop/views/simple.py
  54. +223 −0 pyshop/views/xmlrpc.py
  55. +21 −0 setup.cfg
  56. +75 −0 setup.py
View
4 .gitignore
@@ -1,4 +1,5 @@
*.py[cod]
+__pycache__
# C extensions
*.so
@@ -10,7 +11,6 @@ dist
build
eggs
parts
-bin
var
sdist
develop-eggs
@@ -33,3 +33,5 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject
+
+/repository/*
View
8 CHANGES.txt
@@ -0,0 +1,8 @@
+0.1
+---
+
+- Initial version.
+vital functionnality:
+ - work with pip, setuptools
+ - mirror package
+ - secure access with login password
View
24 LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2012, Guillaume Gauvrit
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the <organization> nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
2 MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include waterfall *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
View
76 README.md
@@ -1,4 +1,78 @@
pyshop
======
-A cheeseshop clone (PyPI server) written in pyramid
+A cheeseshop clone (PyPI server) written in pyramid
+
+
+Getting Started
+---------------
+
+.. code-block: bash
+
+ $ virtualenv pyshop
+ $ cd pyshop
+ (pyshop)$ source bin/activate
+ (pyshop)$ pip install git+https://github.com/mardiros/pyshop.git
+ (pyshop)$ initialize_pyshop_db development.ini
+ (pyshop)$ pserve development.ini
+
+
+For production usage, you should create a user pyshop
+with restriction right.
+
+For editing permission, the web user interface is not ready.
+You can use the pyshop shell.
+
+ (pyshop)$ pyshop_shell
+
+
+The upload on PyPI will be done when the project is more advanced.
+
+
+Configuring your environment to use that new PyShop
+---------------------------------------------------
+
+Here is a all configuration files by usual python tools.
+
+
+~/.pip/pip.conf
+~~~~~~~~~~~~~~~
+
+Configuration used by pip
+
+.. code-block: ini
+
+ [install]
+ index-url = http://admin:changeme@localhost:6543/simple/
+
+
+~/.pypirc
+~~~~~~~~~
+
+Configuration used by setuptools to upload package
+
+.. code-block: ini
+
+ [distutils]
+ index-servers =
+ pyshop
+
+ [pyshop]
+ username: admin
+ password: changeme
+ repository: http://localhost:6543/simple/
+
+
+setup.cfg
+~~~~~~~~~
+
+.. code-block: ini
+
+ [easy_install]
+ index-url = http://admin:changeme@localhost:6543/simple/
+
+
+Uploading a file to PyShop
+--------------------------
+
+python setup.py sdist upload -v -r pyshop
View
82 development.ini
@@ -0,0 +1,82 @@
+[app:main]
+use = egg:pyshop
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_tm
+ pyramid_jinja2
+ pyramid_scss
+ pyramid_xmlrpc
+ pyramid_debugtoolbar
+ pyshop
+
+jinja2.directories = pyshop:templates
+jinja2.i18n.domain = pyshop
+scss.asset_path = pyshop:assets/sass
+scss.compress = false
+scss.cache = false
+
+sqlalchemy.url = sqlite:///%(here)s/pyshop.db
+sqlalchemy.echo = false
+#sqlalchemy.pool_size = 1
+
+tm.commit_veto = pyramid_tm.default_commit_veto
+
+
+xmlrpc.encoding = 'utf-8'
+xmlrpc.allow_none = True
+xmlrpc.datetime = True
+
+pypi.url = http://pypi.python.org/pypi
+
+repository.root = %(here)s/repository/
+
+cookie_key = secret
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+# Begin logging configuration
+
+[loggers]
+keys = root, pyshop, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_pyshop]
+level = DEBUG
+handlers =
+qualname = pyshop
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+
+# End logging configuration
View
6 message-extraction.ini
@@ -0,0 +1,6 @@
+[python: **.py]
+encoding = utf-8
+
+[jinja2: **/templates/**.html]
+encoding = utf-8
+extensions=jinja2.ext.autoescape,jinja2.ext.with_
View
82 production.ini
@@ -0,0 +1,82 @@
+[app:main]
+use = egg:pyshop
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_tm
+ pyramid_jinja2
+ pyramid_scss
+ pyramid_xmlrpc
+ pyramid_debugtoolbar
+ pyshop
+
+
+jinja2.directories = pyshop:templates
+jinja2.i18n.domain = pyshop
+scss.asset_path = pyshop:assets/sass
+scss.compress = false
+scss.cache = false
+
+sqlalchemy.url = sqlite:///%(here)s/pyshop.db
+sqlalchemy.echo = false
+#sqlalchemy.pool_size = 1
+
+tm.commit_veto = pyramid_tm.default_commit_veto
+
+
+xmlrpc.encoding = 'utf-8'
+xmlrpc.allow_none = True
+xmlrpc.datetime = True
+
+pypi.url = http://pypi.python.org/pypi
+repository.root = %(here)s/repository/
+
+cookie_key = secret
+
+[server:main]
+use = egg:waitress#main
+host = 0.0.0.0
+port = 6543
+
+# Begin logging configuration
+
+[loggers]
+keys = root, pyshop, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_pyshop]
+level = WARN
+handlers =
+qualname = pyshop
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+
+# End logging configuration
View
42 pyshop/__init__.py
@@ -0,0 +1,42 @@
+import sys
+from pyramid.config import Configurator
+from pyramid.authorization import ACLAuthorizationPolicy as ACLPolicy
+from pyramid.authentication import AuthTktAuthenticationPolicy as AuthPolicy
+
+from .security import groupfinder
+
+# used by pyramid
+from .config import includeme
+from .models import create_engine
+from .helpers.i18n import locale_negotiator
+from .helpers.authentication import RouteSwithchAuthPolicy
+
+__version__ = '0.1'
+
+
+def main(global_config, **settings):
+ """Get a PyShop WSGI application configured with settings.
+ """
+
+ settings = dict(settings)
+
+ if 'celery' in sys.argv[0]:
+ # XXX celery must config sqlalchemy engine AFTER forkin consumer
+ config = Configurator(settings=settings)
+ else:
+ # Scoping sessions for Pyramid ensure session are commit/rollback
+ # after the template has been rendered
+ create_engine(settings, scoped=True)
+
+ authn_policy = RouteSwithchAuthPolicy(secret=settings['cookie_key'],
+ callback=groupfinder)
+ authz_policy = ACLPolicy()
+
+ config = Configurator(settings=settings,
+ root_factory='pyshop.resources.RootFactory',
+ locale_negotiator=locale_negotiator,
+ authentication_policy=authn_policy,
+ authorization_policy=authz_policy)
+ config.end()
+
+ return config.make_wsgi_app()
View
0 pyshop/assets/sass/00_vars.scss
No changes.
View
0 pyshop/assets/sass/01_macro.scss
No changes.
View
0 pyshop/assets/sass/02_reset.scss
No changes.
View
0 pyshop/assets/sass/10_layout.scss
No changes.
View
5 pyshop/assets/sass/index.scss
@@ -0,0 +1,5 @@
+@import 00_vars.scss;
+@import 01_macro.scss;
+@import 02_reset.scss;
+
+@import 10_layout.scss;
View
62 pyshop/bin/install.py
@@ -0,0 +1,62 @@
+import os
+import sys
+
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from pyshop.helpers.sqla import create_engine, dispose_engine
+from pyshop.models import (Base, DBSession,
+ Permission, Group, User,
+ AuthorizedIP)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def populate(engine):
+
+ Base.metadata.create_all(engine)
+ session = DBSession()
+ user_perm = Permission(name=u'user_view')
+ admin_perm = Permission(name=u'admin_view')
+ session.add(user_perm)
+ session.add(admin_perm)
+
+ user_group = Group(name=u'user')
+ user_group.permissions.append(user_perm)
+ session.add(user_group)
+ admin_group = Group(name=u'admin')
+ admin_group.permissions.append(user_perm)
+ admin_group.permissions.append(admin_perm)
+ session.add(admin_group)
+
+ admin = User(login=u'admin', password=u'changeme', email=u'root@localhost')
+ admin.groups.append(user_group)
+ admin.groups.append(admin_group)
+ session.add(admin)
+
+ ip = User(login=u'ip', password=u'changeme', email=u'root@localhost')
+ ip.groups.append(user_group)
+ session.add(ip)
+
+ session.add(AuthorizedIP(address=u'127.0.0.1'))
+ session.commit()
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri)
+ engine = create_engine('pyshop', settings, scoped=False)
+ populate(engine)
+ dispose_engine('pyshop')
+
+
+if __name__ == '__main__':
+ main()
View
60 pyshop/bin/shell.py
@@ -0,0 +1,60 @@
+#-*- coding: utf-8 -*-
+"""
+Initialize a python shell with a given environment (a config file).
+"""
+
+import os
+import sys
+
+from sqlalchemy import engine_from_config
+
+from pyramid.paster import get_appsettings, setup_logging
+from pyramid.config import Configurator
+
+from pyshop.models import DBSession, initialize_sql
+from pyshop.helpers import pypi
+
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri, 'main')
+ engine = engine_from_config(settings, 'sqlalchemy.')
+ initialize_sql(engine)
+ pypi.set_proxy(settings['pypi.url'])
+
+ config = Configurator(settings=settings)
+ config.end()
+
+ # XXX Configure pyramid_celery, something looks wrong, the bad way ???
+ # from pyramid_celery.commands.celeryctl import CeleryCtl
+ # CeleryCtl().setup_app_from_commandline(argv)
+
+ # add models and session to locals
+ from pyshop.models import (User, Group,
+ Package, Release, ReleaseFile)
+
+ session = DBSession()
+ try:
+ from IPython import embed
+ from IPython.config.loader import Config
+ cfg = Config()
+ cfg.InteractiveShellEmbed.confirm_exit = False
+ embed(config=cfg, banner1="pyshop shell.")
+ except ImportError:
+ import code
+ code.interact("pyshop shell", local=locals())
+
+
+if __name__ == '__main__':
+ main()
View
137 pyshop/config.py
@@ -0,0 +1,137 @@
+#-*- coding: utf-8 -*-
+from pyramid.interfaces import IBeforeRender
+from pyramid.security import has_permission
+from pyramid.url import static_path, route_path
+# from pyramid.renderers import JSONP
+
+from pyramid_jinja2 import renderer_factory
+
+from pyshop.helpers import pypi
+from pyshop.helpers.restxt import parse_rest
+from pyshop.helpers.download import renderer_factory as dl_renderer_factory
+
+
+def add_urlhelpers(event):
+ """
+ Add helpers to the template engine.
+ """
+ event['static_url'] = lambda x: static_path(x, event['request'])
+ event['route_url'] = lambda name, *args, **kwargs: \
+ route_path(name, event['request'], *args, **kwargs)
+ event['parse_rest'] = lambda x: parse_rest(x)
+ event['has_permission'] = lambda perm: has_permission(perm,
+ event['request'].context,
+ event['request'])
+
+
+def includeme(config):
+ # config.add_renderer('json', JSONP())
+ # release file download
+ config.add_renderer('repository', dl_renderer_factory)
+
+ # Jinja configuration
+ # We don't use jinja2 filename, .html instead
+ config.add_renderer('.html', renderer_factory)
+ # helpers
+ config.add_subscriber(add_urlhelpers, IBeforeRender)
+ # i18n
+ config.add_translation_dirs('locale/')
+
+ # PyPI url for XML RPC service consume
+ pypi.set_proxy(config.registry.settings['pypi.url'])
+
+ # Javascript + Media
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ #config.add_static_view('repository', 'repository', cache_max_age=3600)
+
+ # Css
+ config.add_route('css', '/css/{css_path:.*}.css')
+ config.add_view(route_name=u'css', renderer=u'scss', request_method=u'GET',
+ view=u'pyramid_scss.controller.get_scss')
+
+ # Credentials
+ config.add_view('pyshop.views.login',
+ renderer=u'shared/login.html',
+ context=u'pyramid.exceptions.Forbidden')
+
+ config.add_view('pyshop.views.credentials.authbasic',
+ route_name='list_simple',
+ context='pyramid.exceptions.Forbidden'
+ )
+
+ config.add_view('pyshop.views.credentials.authbasic',
+ route_name='show_simple',
+ context='pyramid.exceptions.Forbidden'
+ )
+
+ config.add_route(u'login', u'/login',
+ view=u'pyshop.views.login',
+ view_renderer=u'shared/login.html')
+
+ config.add_route(u'logout', u'/logout',
+ view=u'pyshop.views.logout',
+ view_permission=u'user_view')
+
+ # Home page
+ config.add_route(u'index', u'/',
+ view=u'pyshop.views.index',
+ view_permission=u'user_view')
+
+ # Archive downloads
+ config.add_route(u'repository',
+ u'/repository/{file_id}/{filename:.*}',
+ renderer=u'repository',
+ request_method=u'GET',
+ view=u'pyshop.views.repository.get_release_file',
+ view_permission=u'download_releasefile')
+
+ # Simple views used by pip
+
+ config.add_route(u'list_simple',
+ u'/simple/',
+ request_method=u'GET',
+ view=u'pyshop.views.list_simple',
+ view_renderer=u'pyshop/simple/list.html')
+
+ config.add_route(u'show_simple',
+ u'/simple/{package_name}/',
+ view=u'pyshop.views.show_simple',
+ view_renderer=u'pyshop/simple/show.html',
+ view_permission=u'user_view')
+
+ # Used by setup.py sdist upload
+
+ config.add_route(u'upload_releasefile',
+ u'/simple/',
+ request_method=u'POST',
+ view=u'pyshop.views.list_simple',
+ view_permission=u'upload_releasefile')
+
+
+ # Web Services
+
+ config.add_route(u'json_list_package',
+ u'/pypi/{package_name}/json',
+ view=u'pyshop.views.json.list_package',
+ view_renderer='json')
+
+ config.add_route(u'json_list_package_version',
+ u'/pypi/{package_name}/{version}/json',
+ view=u'pyshop.views.json.list_package_version',
+ view_renderer='json')
+
+ config.add_view('pyshop.views.xmlrpc.PyPI', name='pypi')
+
+ # Backoffice Views
+
+ config.add_route(u'list_package',
+ u'/pyshop/package',
+ view=u'pyshop.views.list_package',
+ view_renderer=u'pyshop/package/list.html',
+ view_permission=u'user_view')
+
+ config.add_route(u'show_package',
+ u'/pyshop/package/{package_name}',
+ view=u'pyshop.views.show_package',
+ view_renderer=u'pyshop/package/show.html',
+ view_permission=u'user_view')
View
0 pyshop/helpers/__init__.py
No changes.
View
60 pyshop/helpers/authentication.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+from zope.interface import implements
+
+from pyramid.interfaces import IAuthenticationPolicy
+from pyramid.authentication import CallbackAuthenticationPolicy, \
+ AuthTktAuthenticationPolicy
+
+from pyshop.models import DBSession, User
+
+
+class AuthBasicAuthenticationPolicy(CallbackAuthenticationPolicy):
+ implements(IAuthenticationPolicy)
+
+ def __init__(self, callback=None):
+ self.callback = callback
+
+ def unauthenticated_userid(self, request):
+ auth = request.environ.get('HTTP_AUTHORIZATION')
+ if auth is None:
+ return None
+ scheme, data = auth.split(None, 1)
+ assert scheme.lower() == 'basic'
+ username, password = data.decode('base64').split(':', 1)
+ if User.by_credentials(DBSession(), username, password):
+ return username
+ return None
+
+ def remember(self, request, principal, **kw):
+ return []
+
+ def forget(self, request):
+ return []
+
+
+class RouteSwithchAuthPolicy(CallbackAuthenticationPolicy):
+ implements(IAuthenticationPolicy)
+
+ def __init__(self, secret='key',callback=None):
+ self.impl = {'basic': AuthBasicAuthenticationPolicy(callback=callback),
+ 'tk': AuthTktAuthenticationPolicy(secret,
+ callback=callback)
+ }
+
+ def get_impl(self,request):
+ if request.matched_route.name in ('list_simple', 'show_simple'):
+ return self.impl['basic']
+ return self.impl['tk']
+
+ def unauthenticated_userid(self, request):
+ impl = self.get_impl(request)
+ return impl.unauthenticated_userid(request)
+
+ def remember(self, request, principal, **kw):
+ impl = self.get_impl(request)
+ return impl.remember(request, principal, **kw)
+
+ def forget(self, request, *args, **kw):
+ impl = self.get_impl(request)
+ return impl.forget(request, *args, **kw)
View
55 pyshop/helpers/download.py
@@ -0,0 +1,55 @@
+import os
+import os.path
+import mimetypes
+from zope.interface import implements
+
+import requests
+from pyramid.interfaces import ITemplateRenderer
+
+
+class ReleaseFileRenderer(object):
+ implements(ITemplateRenderer)
+
+ def __init__(self, repository_root):
+ self.repository_root = repository_root
+
+ def __call__(self, value, system):
+
+ if 'request' in system:
+ request = system['request']
+
+ mime, encoding = mimetypes.guess_type(value['filename'])
+ request.response_content_type = mime
+ if encoding:
+ request.response_encoding = encoding
+
+ f = os.path.join(self.repository_root,
+ value['filename'][0].lower(),
+ value['filename'])
+
+ if not os.path.exists(f):
+ dir_ = os.path.join(self.repository_root,
+ value['filename'][0].lower())
+ if not os.path.exists(dir_):
+ os.mkdir(dir_, 0750)
+
+ resp = requests.get(value['url'])
+ with open(f, 'wb') as rf:
+ rf.write(resp.content)
+ return resp.content
+ else:
+ data = ''
+ with open(f, 'rb') as rf:
+ data = ''
+ while True:
+ content = rf.read(2<<16)
+ if not content:
+ break
+ data += content
+ return data
+
+
+def renderer_factory(info):
+ repository_root = info.settings['repository.root']
+ return ReleaseFileRenderer(repository_root)
+
View
23 pyshop/helpers/i18n.py
@@ -0,0 +1,23 @@
+#: Mapping of language codes send by browsers to supported dialects.
+from pyramid.i18n import TranslationStringFactory
+
+LANGUAGES = {
+ 'en-CA': 'en',
+ 'en-GB': 'en',
+ 'en-US': 'en',
+ 'en': 'en',
+ 'fr-FR': 'fr',
+ 'fr-BE': 'fr',
+ 'fr': 'fr',
+ }
+
+
+def locale_negotiator(request):
+ """Locale negotiator base on the `Accept-Language` header"""
+ locale = 'en'
+ if request.accept_language:
+ locale = request.accept_language.best_match(LANGUAGES)
+ locale = LANGUAGES.get(locale, 'en')
+ return locale
+
+trans = TranslationStringFactory('pyshop')
View
22 pyshop/helpers/pypi.py
@@ -0,0 +1,22 @@
+from xmlrpclib import ServerProxy
+
+
+proxy = None
+
+
+def set_proxy(proxy_url):
+ global proxy
+ proxy = ServerProxy(proxy_url, allow_none=True)
+
+
+'''
+import requests
+
+def list_package(root, package_name):
+ return requests.get(u'%s/%s/json' % (root, package_name)).json
+
+
+def list_package_version(root, package_name, version):
+ return requests.get(u'%s/%s/%s/json' %
+ (root, package_name, version)).json
+'''
View
10 pyshop/helpers/restxt.py
@@ -0,0 +1,10 @@
+from docutils import core
+from jinja2 import Markup
+
+
+def parse_rest(rest):
+ html = core.publish_string(
+ source=rest,
+ writer_name="html",
+ settings_overrides={'output_encoding': 'unicode'})
+ return Markup(html[html.find("<body>")+6:html.find("</body>")].strip())
View
142 pyshop/helpers/sqla.py
@@ -0,0 +1,142 @@
+import re
+
+from zope.sqlalchemy import ZopeTransactionExtension
+
+from sqlalchemy import Column, Integer, DateTime, engine_from_config
+from sqlalchemy.interfaces import PoolListener
+from sqlalchemy.exc import DisconnectionError
+from sqlalchemy.sql.expression import func, asc, desc
+
+from sqlalchemy.orm import scoped_session, sessionmaker, joinedload
+from sqlalchemy.ext.declarative import declarative_base, declared_attr
+
+
+class _Base(object):
+
+ @declared_attr
+ def __tablename__(cls):
+ # CamelCase to underscore cast
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', cls.__name__)
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
+
+ __table_args__ = {'mysql_engine': 'InnoDB',
+ 'mysql_charset': 'utf8'
+ }
+
+ id = Column(Integer(unsigned=True), primary_key=True)
+ created_at = Column(DateTime, default=func.now())
+
+ @classmethod
+ def by_id(cls, session, id):
+ return cls.first(session, where=(cls.id == id,))
+
+ @classmethod
+ def find(cls, session, join=None, where=None, order_by=None, limit=None,
+ offset=None, count=False):
+ qry = cls.build_query(session, join, where, order_by, limit,
+ offset)
+ return qry.count() if count else qry.all()
+
+ @classmethod
+ def first(cls, session, join=None, where=None, order_by=None):
+ return cls.build_query(session, join, where, order_by).first()
+
+ @classmethod
+ def all(cls, session, page_size=1000, order_by=None):
+ offset = 0
+ order_by = order_by or cls.id
+ while True:
+ page = cls.find(session, order_by=order_by,
+ limit=page_size, offset=offset)
+ for m in page:
+ yield m
+ session.flush()
+ if len(page) != page_size:
+ raise StopIteration()
+ offset += page_size
+
+ @classmethod
+ def build_query(cls, session, join=None, where=None, order_by=None,
+ limit=None, offset=None):
+ query = session.query(cls)
+
+ to_model = lambda m: getattr(cls, m) if isinstance(m, basestring)\
+ else m
+
+ if join:
+ if isinstance(join, (list, tuple)):
+ for j in join:
+ query = query.join(to_model(j))
+ else:
+ query = query.join(to_model(join))
+
+ if where:
+ for filter in where:
+ query = query.filter(filter)
+
+ if order_by is not None:
+ if isinstance(order_by, (list, tuple)):
+ fields = []
+ for ob in order_by:
+ fields.append(ob)
+ query = query.order_by(*fields)
+ else:
+ if isinstance(order_by, basestring):
+ order_by = order_by.split(' ', 1)
+ field = getattr(cls, order_by[0])
+ if len(order_by) > 1 and order_by[1] == 'desc':
+ field = desc(field)
+ else:
+ field = order_by
+ query = query.order_by(field)
+ if limit:
+ query = query.limit(limit)
+ if offset:
+ query = query.offset(offset)
+ return query
+
+
+class Database(object):
+ databases = {}
+
+ @classmethod
+ def register(cls, name):
+ if name not in cls.databases:
+ cls.databases[name] = declarative_base(cls=_Base)
+ return cls.databases[name]
+
+ @classmethod
+ def get(cls, name):
+ return cls.databases[name]
+
+
+class SessionFactory(object):
+
+ sessions = {}
+
+ @classmethod
+ def register(cls, name, scoped):
+ if scoped:
+ cls.sessions[name] = scoped_session(sessionmaker(
+ extension=ZopeTransactionExtension()))
+ else:
+ cls.sessions[name] = sessionmaker()
+ return cls.sessions[name]
+
+ @classmethod
+ def get(cls, name):
+ return cls.sessions[name]
+
+
+def create_engine(db_name, settings, prefix='sqlalchemy.', scoped=False):
+ engine = engine_from_config(settings, prefix)
+
+ DBSession = SessionFactory.register(db_name, scoped)
+ DBSession.configure(bind=engine)
+ Database.get(db_name).metadata.bind = engine
+
+ return engine
+
+
+def dispose_engine(db_name):
+ Database.get(db_name).metadata.bind.dispose()
View
25 pyshop/helpers/task.py
@@ -0,0 +1,25 @@
+import transaction
+from celery.task import Task
+
+
+class SqlAlchemyTask(Task):
+
+ ignore_result = True
+ logname = None
+ session_class = None
+
+ def process(session, *args, **kwargs):
+ raise NotImplementedError()
+
+ def run(self, *args, **kwargs):
+ rv = None
+ session = self.session_class()
+ try:
+ rv = self.process(session, *args, **kwargs)
+ except Exception:
+ raise
+ # getLogger(self.logname).error('error in task %s(*%r,**%r)' %
+ # (self.name, args, kwargs), exc_info=True)
+ # Error Queue here for retry jobs here ?
+ transaction.commit()
+ return rv
View
19 pyshop/locale/fr/LC_MESSAGES/pyshop.po
@@ -0,0 +1,19 @@
+# Translations template for PROJECT.
+# Copyright (C) 2011 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
+#
+
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2011-05-12 09:14-0330\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
View
19 pyshop/locale/pyshop.pot
@@ -0,0 +1,19 @@
+# Translations template for pyshop.
+# Copyright (C) 2012 ORGANIZATION
+# This file is distributed under the same license as the pyshop project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
+#
+
+msgid ""
+msgstr ""
+"Project-Id-Version: pyshop 0.1\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2012-12-02 21:27+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
View
296 pyshop/models.py
@@ -0,0 +1,296 @@
+import cryptacular.bcrypt
+
+from sqlalchemy import (Table, Column, ForeignKey, Index,
+ Integer, Boolean, Unicode, UnicodeText,
+ DateTime, Enum)
+from sqlalchemy.orm import relationship, backref, synonym
+from sqlalchemy.sql.expression import func, asc, desc, or_, and_
+from sqlalchemy.ext.declarative import declared_attr
+
+from .helpers.sqla import (Database, SessionFactory,
+ create_engine as create_engine_base,
+ dispose_engine as dispose_engine_base
+ )
+
+crypt = cryptacular.bcrypt.BCRYPTPasswordManager()
+
+Base = Database.register('pyshop')
+DBSession = lambda: SessionFactory.get('pyshop')()
+
+
+def create_engine(settings, prefix='sqlalchemy.', scoped=False):
+ return create_engine_base('pyshop', settings, prefix, scoped)
+
+
+def dispose_engine():
+ dispose_engine_base('pyshop')
+
+
+class Permission(Base):
+
+ name = Column(Unicode(255), nullable=False, unique=True)
+
+
+group__permission = Table('group__permission', Base.metadata,
+ Column('group_id', Integer, ForeignKey('group.id')),
+ Column('permission_id',
+ Integer, ForeignKey('permission.id'))
+ )
+
+
+class Group(Base):
+
+ name = Column(Unicode(255), nullable=False, unique=True)
+ permissions = relationship(Permission, secondary=group__permission,
+ lazy='select')
+
+ @classmethod
+ def by_name(cls, session, name):
+ return cls.first(where=(cls.name == name,))
+
+
+user__group = Table('user__group', Base.metadata,
+ Column('group_id', Integer, ForeignKey('group.id')),
+ Column('user_id', Integer, ForeignKey('user.id'))
+ )
+
+
+class User(Base):
+
+ @declared_attr
+ def __table_args__(cls):
+ return (Index('idx_%s_login_local' % cls.__tablename__,
+ 'login', 'local', unique=True),
+ )
+
+ login = Column(Unicode(255), nullable=False)
+ _password = Column('password', Unicode(60), nullable=True)
+
+ firstname = Column(Unicode(255), nullable=True)
+ lastname = Column(Unicode(255), nullable=True)
+ email = Column(Unicode(255), nullable=True)
+ groups = relationship(Group, secondary=user__group, lazy='joined',
+ backref='users')
+
+ local = Column(Boolean, nullable=False, default=True)
+
+ @property
+ def name(self):
+ return u'%s %s' % (self.firstname, self.lastname)\
+ if self.lastname else self.login
+
+ def _get_password(self):
+ return self._password
+
+ def _set_password(self, password):
+ self._password = unicode(crypt.encode(password))
+
+ password = property(_get_password, _set_password)
+ password = synonym('_password', descriptor=password)
+
+ @classmethod
+ def by_login(cls, session, login, local=True):
+ user = cls.first(session,
+ where=((cls.login == login),
+ (cls.local == local),)
+ )
+ # XXX it's appear that this is not case sensitive !
+ return user if user and user.login == login else None
+
+ @classmethod
+ def by_credentials(cls, session, login, password):
+ user = cls.by_login(session, login)
+ if not user:
+ return None
+ if not user.local:
+ return None
+ if crypt.check(user.password, password):
+ return user
+
+
+class AuthorizedIP(Base):
+ address = Column(Unicode(40), unique=True)
+
+ @classmethod
+ def by_address(cls, session, address):
+ return cls.find(session, where=(cls.address == address,))
+
+
+class Classifier(Base):
+
+ name = Column(Unicode(255), nullable=False, unique=True)
+
+ @classmethod
+ def by_name(cls, session, name):
+ return cls.first(session, where=(cls.name == name))
+
+
+package__owner = Table('package__owner', Base.metadata,
+ Column('package_id', Integer, ForeignKey('package.id')),
+ Column('owner_id', Integer, ForeignKey('user.id'))
+ )
+
+package__maintainer = Table('package__maintainer', Base.metadata,
+ Column('package_id',
+ Integer, ForeignKey('package.id')),
+ Column('maintainer_id',
+ Integer, ForeignKey('user.id'))
+ )
+
+
+class Package(Base):
+
+ update_at = Column(DateTime, default=func.now())
+ name = Column(Unicode(200), unique=True)
+ local = Column(Boolean, default=False)
+ owners = relationship(User, secondary=package__owner,
+ backref='owned_packages')
+ downloads = Column(Integer(unsigned=True), default=0)
+ maintainers = relationship(User, secondary=package__maintainer,
+ backref='maintained_packages')
+
+ @property
+ def versions(self):
+ return [r.version for r in self.releases]
+
+ @classmethod
+ def by_name(cls, session, name):
+ return cls.first(session, where=(cls.name == name,))
+
+ @classmethod
+ def by_owner(cls, session, owner_name):
+ return cls.find(session,
+ join=(cls.owners),
+ where=(User.login == owner_name,))
+
+ @classmethod
+ def by_maintainer(cls, session, maintainer_name):
+ return cls.find(session,
+ join=(cls.maintainers),
+ where=(User.login == maintainer_name,))
+
+ @classmethod
+ def get_locals(cls, session):
+ return cls.find(session,
+ where=(cls.local == True,))
+
+ @classmethod
+ def get_mirrored(cls, session):
+ return cls.find(session,
+ where=(cls.local == False,))
+
+
+
+classifier__release = Table('classifier__release', Base.metadata,
+ Column('classifier_id',
+ Integer, ForeignKey('classifier.id')),
+ Column('release_id',
+ Integer, ForeignKey('release.id'))
+ )
+
+
+class Release(Base):
+
+ @declared_attr
+ def __table_args__(cls):
+ return (Index('idx_%s_package_id_version' % cls.__tablename__,
+ 'package_id', 'version', unique=True),
+ )
+
+
+ version = Column(Unicode(16), nullable=False)
+ summary = Column(Unicode(255))
+ downloads = Column(Integer(unsigned=True), default=0)
+
+ package_id = Column(Integer(unsigned=True), ForeignKey(Package.id),
+ nullable=False)
+ author_id = Column(Integer(unsigned=True), ForeignKey(User.id),
+ nullable=True)
+ maintainer_id = Column(Integer(unsigned=True), ForeignKey(User.id),
+ nullable=True)
+ stable_version = Column(Unicode(16))
+ home_page = Column(Unicode(255))
+ license = Column(UnicodeText())
+ description = Column(UnicodeText())
+ keywords = Column(Unicode(255))
+ platform = Column(Unicode(24))
+ download_url = Column(Unicode(800), nullable=True)
+ bugtrack_url = Column(Unicode(800), nullable=True)
+ docs_url = Column(Unicode(800), nullable=True)
+ classifiers = relationship(Classifier, secondary=classifier__release,
+ lazy='dynamic', backref='release')
+ package = relationship(Package, lazy='join', backref='releases')
+ author = relationship(User, primaryjoin=author_id == User.id)
+ maintainer = relationship(User, primaryjoin=maintainer_id == User.id)
+
+ @classmethod
+ def by_version(cls, session, package_name, version):
+ return cls.first(session,
+ join=(Package,),
+ where=((Package.name == package_name),
+ (cls.version == version)))
+
+ @classmethod
+ def by_classifier(cls, session, classifier):
+ return cls.find(session,
+ join=(cls.classifiers,),
+ where=(cls.classifiers.name.in_(classifier),),
+ )
+
+ @classmethod
+ def search(cls, session, opts, operator):
+ available = ['name', 'version', 'author', 'author_email', 'maintainer',
+ 'maintainer_email', 'home_page', 'license', 'summary',
+ 'description', 'keywords', 'platform', 'download_url']
+ where = []
+ join = []
+ oper = {'or': or_, 'and': and_}
+ for opt, val in opts.items():
+ assert opt in available, u'%s is not valid' % opt
+ if opt == 'name':
+ field = Package.name
+ else:
+ field = getattr(cls, opt)
+
+ if hasattr(val, '__iter__'):
+ stmt = or_([field.like(u'%%%s%%' % v) for v in val])
+ else:
+ stmt = field.like(u'%%%s%%' % val)
+ where.append(stmt)
+ join.append(Package)
+ return cls.find(session, join=join,
+ where=(oper[operator](*where),))
+
+
+class ReleaseFile(Base):
+
+ release_id = Column(Integer(unsigned=True), ForeignKey(Release.id),
+ nullable=False)
+ filename = Column(Unicode(200), nullable=True)
+ md5_digest = Column(Unicode(50))
+ size = Column(Integer(nullable=True))
+ package_type = Column(Enum(u'sdist', u'bdist_egg', u'bdist_msi',
+ u'bdist_dmg', u'bdist_rpm', u'bdist_dumb',
+ u'bdist_wininst'), nullable=False)
+
+ python_version = Column(Unicode(25))
+ url = Column(Unicode(1024), nullable=True)
+ downloads = Column(Integer(unsigned=True), default=0)
+ has_sig = Column(Boolean(), default=False)
+ comment_text = Column(UnicodeText())
+
+ release = relationship(Release, backref='files', lazy='join')
+
+ @classmethod
+ def by_release(cls, session, package_name, version):
+ return cls.find(session,
+ join=(Release, Package),
+ where=(Package.name == package_name,
+ Release.version == version,
+ ))
+
+ @classmethod
+ def by_filename(cls, session, release, filename):
+ return cls.first(session,
+ where=(ReleaseFile.release_id == release.id,
+ ReleaseFile.filename == filename))
View
31 pyshop/resources.py
@@ -0,0 +1,31 @@
+import logging
+from pyramid.security import Allow
+
+from .models import DBSession, Group
+from .security import groupfinder
+
+log = logging.getLogger(__name__)
+
+class RootFactory(object):
+ __name__ = None
+ __parent__ = None
+
+ _acl = None
+
+ def __init__(self, request):
+ log.info("[%s] %s %s" % (request.client_addr,
+ request.method,
+ request.current_route_url()
+ ))
+ self.__acl__ = self.get_acl(request)
+
+ def get_acl(self, request):
+ if RootFactory._acl is None:
+ acl = []
+ session = DBSession()
+ groups = Group.all(session)
+ for g in groups:
+ acl.extend([(Allow, g.name, p.name) for p in g.permissions])
+ RootFactory._acl = acl
+
+ return RootFactory._acl
View
27 pyshop/security.py
@@ -0,0 +1,27 @@
+from .models import DBSession, User, Group
+
+class GroupFinder(object):
+
+ _users = {}
+
+ def reset(self):
+ self._users = {}
+
+ def __call__(self, login, request):
+
+ print "*"*80
+ print login
+ print "*"*80
+
+ if login in self._users:
+ return self._users[login]
+
+ user = User.by_login(DBSession(), login)
+ if user:
+ rv = [g.name for g in user.groups]
+ else:
+ rv = []
+ self._users[login] = rv
+ return rv
+
+groupfinder = GroupFinder()
View
BIN pyshop/static/favicon.ico
Binary file not shown.
View
BIN pyshop/static/pyramid-small.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN pyshop/static/pyramid.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
7 pyshop/templates/macro/nav.html
@@ -0,0 +1,7 @@
+{% macro nav(request, route, text, selected=None) -%}
+ {% set selected = selected or [route] %}
+ <a href="{{ request.route_url(route) }}"
+ {%if request.matched_route.name in selected %}class="selected"{%endif%}>
+ {{text}}
+ </a>
+{%- endmacro %}
View
8 pyshop/templates/pyshop/layout.html
@@ -0,0 +1,8 @@
+{% extends "/shared/layout.html" %}
+
+{% block css_section %}pyshop{% endblock %}
+
+{% block subnav %}
+ {% include "/pyshop/nav.html" %}
+{% endblock %}
+
View
5 pyshop/templates/pyshop/nav.html
@@ -0,0 +1,5 @@
+{% import "macro/nav.html" as nav %}
+
+{{nav.nav(request, 'list_package', 'packages',
+ ['list_package', 'create_package',
+ 'edit_package', 'delete_package'])}}
View
40 pyshop/templates/pyshop/package/list.html
@@ -0,0 +1,40 @@
+{% extends "/pyshop/layout.html" %}
+
+{% block title %}
+ {% trans %}Available Packages{% endtrans %}
+{% endblock %}
+
+
+{% block main %}
+<h4>{% trans %}Local package{% endtrans %}</h4>
+
+<table>
+ {% for pkg in local_packages %}
+ <tr>
+ <td>
+ <a href="{{ route_url('show_package', package_name=pkg.name) }}">
+ {{ pkg.name }}
+ </a>
+ </td>
+ <td>{{ pkg.releases[-1].version }}</td>
+ <td></td>
+ </tr>
+ {% endfor %}
+</table>
+
+<h4>{% trans %}Mirrored package{% endtrans %}</h4>
+<table>
+ {% for pkg in mirrored_packages %}
+ <tr>
+ <td>
+ <a href="{{ route_url('show_package', package_name=pkg.name) }}">
+ {{ pkg.name }}
+ </a>
+ </td>
+ <td>{{ pkg.releases[-1].version }}</td>
+ <td>{{ pkg.downloads }}</td>
+ </tr>
+ {% endfor %}
+</table>
+
+{% endblock %}
View
44 pyshop/templates/pyshop/package/show.html
@@ -0,0 +1,44 @@
+{% extends "/pyshop/layout.html" %}
+
+{% block title %}
+ Package {{package.name}}
+{% endblock %}
+
+{% set release = package.releases[-1] %}
+
+{% block main %}
+
+<section class="release-download">
+ <h4>download</h4>
+ {% for f in release.files %}
+ <a href="{{ route_url('repository', file_id=f.id, filename=f.filename)
+ }}#{{ f.md5_digest }}">{{f.filename}}
+ </a><br/>
+ {% endfor %}
+</section>
+
+<section class="release-summary">
+ <h4> {{ release.summary }} </h4>
+ <div> {{ parse_rest(release.description) }} </div>
+</section>
+
+<table>
+ {% for release in package.releases|reverse %}
+ <tr>
+ <td>
+ {{ package.name }}
+ </td>
+ <td>
+ {{ release.version }}
+ </td>
+ <td>
+ {{ release.author.login }}
+ </td>
+ <td>{% if release.downloads > 0 %}
+ [mirrored]
+ {% endif%}
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+{% endblock %}
View
4 pyshop/templates/pyshop/simple/list.html
@@ -0,0 +1,4 @@
+<html><head><title>Simple Index</title></head><body>
+{% for package in packages %}<a href="{{ route_url('show_simple', package_name=package.name) }}">{{ package.name }}</a><br />
+{% endfor %}
+</body></html>
View
15 pyshop/templates/pyshop/simple/show.html
@@ -0,0 +1,15 @@
+{%if package%}<html><head><title>Links for {{ package.name }}</title></head><body><h1>Links for {{ package.name }}</h1>
+{% for r in package.releases|reverse %}
+ {% for f in r.files %}<a href="{{ route_url('repository', file_id=f.id, filename=f.filename)
+ }}#{{ f.md5_digest }}">{{f.filename}}</a><br/>
+ {% endfor %}
+{% endfor %}
+{% for r in package.releases|reverse %}
+ {% if r.home_page and r.home_page != 'UNKNOWN' %}
+<a href="{{ r.home_page }}" rel="homepage">{{ r.version }} home_page</a><br/>
+ {% endif %}
+ {% if r.download_url and r.download_url != 'UNKNOWN' %}
+<a href="{{ r.download_url }}" rel="download">{{ r.version }} download_url</a><br/>
+ {% endif %}
+{% endfor %}
+</body></html>{% else %}Not found ({{package_name}} does have any release){% endif %}
View
20 pyshop/templates/shared/base_layout.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>PyShop</title>
+ <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+ <meta name="robots" content="noindex, nofollow">
+ <link rel="shortcut icon" href="{{ static_url('pyshop:static/favicon.ico') }}" />
+ <link rel="stylesheet" href="/css/index.css" type="text/css" />
+
+ <link rel='stylesheet' type='text/css'
+ href='http://fonts.googleapis.com/css?family=Cantarell:regular,bold&subset=Latin'>
+ <link rel='stylesheet' type='text/css'
+ href='http://fonts.googleapis.com/css?family=Open+Sans:regular,bold'>
+ {% block header %}{% endblock %}
+</head>
+<body class="{% block css_section%}{% endblock%}">
+ {% block body %}
+ {% endblock %}
+</body>
+</html>
View
8 pyshop/templates/shared/footer_buttons.html
@@ -0,0 +1,8 @@
+<footer>
+ <button name="form.cancelled">
+ <span>Cancel</span>
+ </button>
+ <button name="form.submitted" class="primary">
+ <span>Submit</span>
+ </button>
+</footer>
View
15 pyshop/templates/shared/form_layout.html
@@ -0,0 +1,15 @@
+{% extends "/shared/base_layout.html" %}
+
+{% block body %}
+
+<form action="" method="post">
+ <fieldset>
+ {% block form %}
+ {% endblock %}
+ </fieldset>
+ {% block form_buttons %}
+ {% include "/shared/footer_buttons.html" %}
+ {% endblock %}
+</form>
+
+{% endblock %}
View
75 pyshop/templates/shared/layout.html
@@ -0,0 +1,75 @@
+{% extends "/shared/base_layout.html" %}
+
+{% block body %}
+
+ <header>
+ <h1>{% block maintitle %}
+ <a href="{{ route_url('index') }}">pyshop</a>
+ <figure>
+ {{pyshop.login}}
+ </figure>
+ {% endblock %}</h1>
+ <nav>
+ {% block mainnav %}
+ {% endblock %}
+ </nav>
+ </header>
+ <section class="main">
+ <nav class="subnav">
+ {% block subnav %}
+ {% endblock %}
+ </nav>
+ <article class="page">
+ <header>
+ <h4>{% block title %}{% endblock %}</h4>
+ </header>
+ {% block messages %}
+ {% block error %}
+ {% if errormessage %}
+ <div class="error">
+ <span>{{ errormessage }}</span>
+ </div>
+ {% endif %}
+ {% if errors %}
+ {%for errormessage in errors %}
+ <div class="error">
+ <span>{{ errormessage }}</span>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% endblock %}
+ {% block warn %}
+ {% if warnmessage %}
+ <div class="warn">
+ <span>{{ warnmessage }}</span>
+ </div>
+ {% endif %}
+ {% endblock %}
+ {% block info %}
+ {% if infomessage %}
+ <div class="info">
+ <span>{{ infomessage }}</span>
+ </div>
+ {% endif %}
+ {% endblock %}
+ {% endblock %}
+ <div>
+ <nav class="toolbar">
+ {% block toolbar %}
+ {% endblock %}
+ </nav>
+ <section>
+ {% block main %}
+ {% endblock %}
+ </section>
+ <footer class="paging">
+ {% block paging %}
+ {% endblock %}
+ </footer>
+ </div>
+ </article>
+ </section>
+ <footer>
+ <div>pyshop v{{pyshop.version}}</div>
+ </footer>
+{% endblock %}
View
23 pyshop/templates/shared/login.html
@@ -0,0 +1,23 @@
+{% extends "/shared/form_layout.html" %}
+
+{% block form %}
+
+ {% if errors %}
+ {%for errormessage in errors %}
+ <div class="error">
+ <span>{{ errormessage }}</span>
+ </div>
+ {% endfor %}
+ {% endif %}
+
+ <ul>
+ <li>
+ <label>Login:</label>
+ <input name="user.login" type="text" value="{{user.login}}">
+ </li>
+ <li>
+ <label>Password:</label>
+ <input name="user.password" type="password">
+ </li>
+ </ul>
+{% endblock %}
View
20 pyshop/views/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+
+from .base import RedirectView
+
+from . import credentials
+from . import simple
+from . import package
+
+class _Index(RedirectView):
+ redirect_route = u'list_package'
+
+index = _Index()
+login = credentials.Login()
+logout = credentials.Logout()
+
+list_simple = simple.List()
+show_simple = simple.Show()
+
+list_package = package.List()
+show_package = package.Show()
View
71 pyshop/views/base.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from pyramid.security import authenticated_userid
+from pyramid.httpexceptions import HTTPFound
+from pyramid.url import route_url
+
+from pyshop.helpers.i18n import trans as _
+
+from .. import __version__
+from ..models import DBSession, User
+
+
+log = logging.getLogger(__name__)
+
+class ViewBase(object):
+
+ def update_response(self, request, session, response):
+ pass
+
+ def on_error(self, request, exception):
+ return True
+
+ def __call__(self, request):
+ try:
+ log.info("dispatch view %s", self.__class__.__name__)
+
+ session = DBSession()
+ response = self.render(request, session)
+ self.update_response(request, session, response)
+ # if isinstance(response, dict):
+ # log.info("rendering template with context %r", dict)
+ except Exception, exc:
+ if self.on_error(request, exc):
+ log.error("Error on view %s" % self.__class__.__name__,
+ exc_info=True)
+ raise
+
+ return response
+
+ def render(self, request, session):
+ return {}
+
+
+class View(ViewBase):
+
+ def update_response(self, request, session, response):
+ # this is a view to render
+ if isinstance(response, dict):
+ login = authenticated_userid(request) or u'anonymous'
+ global_ = {
+ 'pyshop': {
+ 'version': __version__,
+ 'login': login,
+ },
+ }
+ if login != u'anonymous':
+ global_['pyshop']['user'] = User.by_login(session, login)
+ response.update(global_)
+
+
+class RedirectView(View):
+ redirect_route = None
+ redirect_kwargs = {}
+
+ def render(self, request, session):
+ return self.redirect(request)
+
+ def redirect(self, request):
+ return HTTPFound(location=route_url(self.redirect_route, request,
+ **self.redirect_kwargs))
View
68 pyshop/views/credentials.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from pyramid.httpexceptions import HTTPFound, HTTPForbidden
+from pyramid.url import resource_url, route_url
+from pyramid.security import authenticated_userid, remember, forget
+from pyramid.response import Response
+
+from pyshop.helpers.i18n import trans as _
+from pyshop.models import DBSession, User, AuthorizedIP
+
+from .base import View
+
+
+log = logging.getLogger(__name__)
+
+
+class Login(View):
+
+ def render(self, request, session):
+
+ login_url = resource_url(request.context, request, 'login')
+ referrer = request.url
+ # never use the login form itself as came_from
+ if referrer == login_url:
+ referrer = '/'
+ came_from = request.params.get('came_from', referrer)
+
+ login = request.params.get('user.login', '')
+ if 'form.submitted' in request.params:
+ password = request.params.get('user.password', u'')
+ if password and \
+ User.by_credentials(session, login, password) is not None:
+ log.info('login %r succeed' % login)
+ headers = remember(request, login)
+ return HTTPFound(location=came_from,
+ headers=headers)
+
+ return {'came_from': came_from,
+ 'user': User(login=login),
+ }
+
+class Logout(View):
+
+ def render(self, request, session):
+
+ return HTTPFound(location=route_url('index', request),
+ headers=forget(request))
+
+
+def authbasic(request):
+ """
+ Authentification basic, Upload pyshop repository access
+ """
+ if len(request.environ.get('HTTP_AUTHORIZATION','')) > 0:
+ auth = request.environ.get('HTTP_AUTHORIZATION')
+ scheme, data = auth.split(None, 1)
+ assert scheme.lower() == 'basic'
+ username, password = data.decode('base64').split(':', 1)
+ if User.by_credentials(DBSession(), username, password):
+ headers = remember(request, username)
+ return HTTPFound(location=route_url('repository', request,
+ file_id=request.matchdict['file_id'],
+ filename=request.matchdict['filename']))
+ return Response(status=401,
+ headerlist=[('WWW-Authenticate',
+ 'Basic realm="pyshop repository access"',)],
+ )
View
6 pyshop/views/json/__init__.py
@@ -0,0 +1,6 @@
+import requests
+
+from . import pypi
+
+list_package = pypi.list_package
+list_package_version = pypi.PackageVersionList()
View
70 pyshop/views/json/pypi.py
@@ -0,0 +1,70 @@
+from ..base import ViewBase
+
+from pyshop.models import Release
+from pyshop.helpers import pypi
+
+def list_package(self, request):
+ return pypi.list_package(request.registry.settings['pypi.url'],
+ request.matchdict['package_name'])
+
+
+class PackageVersionList(ViewBase):
+
+ def render(self, request, session):
+
+ release = Release.by_version(session,
+ request.matchdict['package_name'],
+ request.matchdict['version'],
+ )
+ if not release:
+ release = list_package(request.registry.settings['pypi.url'],
+ request.matchdict['package_name'],
+ request.matchdict['version'],
+ )
+ release = Release.from_pypi(session, release)
+
+ return release
+
+'''
+{
+ "info": {
+ "maintainer": null,
+ "docs_url": "",
+ "requires_python": null,
+ "maintainer_email": null,
+ "cheesecake_code_kwalitee_id": null,
+ "keywords": "web wsgi pylons pyramid",
+ "package_url": "http://pypi.python.org/pypi/pyramid",
+ "author": "Chris McDonough, Agendaless Consulting",
+ "author_email": "pylons-discuss@googlegroups.com",
+ "download_url": "UNKNOWN",
+ "platform": "UNKNOWN",
+ "version": "1.4b1",
+ "cheesecake_documentation_id": null,
+ "_pypi_hidden": false,
+ "description": "Pyramid\n=======\n\nPyramid is a small, fast, down-to-earth, open source Python web application\ndevelopment framework. It makes real-world web application development and\ndeployment more fun, more predictable, and more productive.\n\nPyramid is produced by the `Pylons Project <http://pylonsproject.org/>`_.\n\nSupport and Documentation\n-------------------------\n\nSee the `Pylons Project website <http://pylonsproject.org/>`_ to view\ndocumentation, report bugs, and obtain support.\n\nLicense\n-------\n\nPyramid is offered under the BSD-derived `Repoze Public License\n<http://repoze.org/license.html>`_.\n\nAuthors\n-------\n\nPyramid is made available by `Agendaless Consulting <http://agendaless.com>`_\nand a team of contributors.\n\n\n\n1.4b1 (2012-11-21)\n==================\n\nFeatures\n--------\n\n- Small microspeed enhancement which anticipates that a\n ``pyramid.response.Response`` object is likely to be returned from a view.\n Some code is shortcut if the class of the object returned by a view is this\n class. A similar microoptimization was done to\n ``pyramid.request.Request.is_response``.\n\n- Make it possible to use variable arguments on ``p*`` commands (``pserve``,\n ``pshell``, ``pviews``, etc) in the form ``a=1 b=2`` so you can fill in\n values in parameterized ``.ini`` file, e.g. ``pshell etc/development.ini\n http_port=8080``. See https://github.com/Pylons/pyramid/pull/714\n\n- A somewhat advanced and obscure feature of Pyramid event handlers is their\n ability to handle \"multi-interface\" notifications. These notifications have\n traditionally presented multiple objects to the subscriber callable. For\n instance, if an event was sent by code like this::\n\n registry.notify(event, context)\n\n In the past, in order to catch such an event, you were obligated to write and\n register an event subscriber that mentioned both the event and the context in\n its argument list::\n\n @subscriber([SomeEvent, SomeContextType])\n def asubscriber(event, context):\n pass\n\n In many subscriber callables registered this way, it was common for the logic\n in the subscriber callable to completely ignore the second and following\n arguments (e.g. ``context`` in the above example might be ignored), because\n they usually existed as attributes of the event anyway. You could usually\n get the same value by doing ``event.context`` or similar.\n\n The fact that you needed to put an extra argument which you usually ignored\n in the subscriber callable body was only a minor annoyance until we added\n \"subscriber predicates\", used to narrow the set of circumstances under which\n a subscriber will be executed, in a prior 1.4 alpha release. Once those were\n added, the annoyance was escalated, because subscriber predicates needed to\n accept the same argument list and arity as the subscriber callables that they\n were configured against. So, for example, if you had these two subscriber\n registrations in your code::\n\n @subscriber([SomeEvent, SomeContextType])\n def asubscriber(event, context):\n pass\n\n @subscriber(SomeOtherEvent)\n def asubscriber(event):\n pass\n \n And you wanted to use a subscriber predicate::\n\n @subscriber([SomeEvent, SomeContextType], mypredicate=True)\n def asubscriber1(event, context):\n pass\n\n @subscriber(SomeOtherEvent, mypredicate=True)\n def asubscriber2(event):\n pass\n\n If an existing ``mypredicate`` subscriber predicate had been written in such\n a way that it accepted only one argument in its ``__call__``, you could not\n use it against a subscription which named more than one interface in its\n subscriber interface list. Similarly, if you had written a subscriber\n predicate that accepted two arguments, you couldn't use it against a\n registration that named only a single interface type.\n\n For example, if you created this predicate::\n\n class MyPredicate(object):\n # portions elided...\n def __call__(self, event):\n return self.val == event.context.foo\n\n It would not work against a multi-interface-registered subscription, so in\n the above example, when you attempted to use it against ``asubscriber1``, it\n would fail at runtime with a TypeError, claiming something was attempting to\n call it with too many arguments.\n\n To hack around this limitation, you were obligated to design the\n ``mypredicate`` predicate to expect to receive in its ``__call__`` either a\n single ``event`` argument (a SomeOtherEvent object) *or* a pair of arguments\n (a SomeEvent object and a SomeContextType object), presumably by doing\n something like this::\n\n class MyPredicate(object):\n # portions elided...\n def __call__(self, event, context=None):\n return self.val == event.context.foo\n\n This was confusing and bad.\n\n In order to allow people to ignore unused arguments to subscriber callables\n and to normalize the relationship between event subscribers and subscriber\n predicates, we now allow both subscribers and subscriber predicates to accept\n only a single ``event`` argument even if they've been subscribed for\n notifications that involve multiple interfaces. Subscribers and subscriber\n predicates that accept only one argument will receive the first object passed\n to ``notify``; this is typically (but not always) the event object. The\n other objects involved in the subscription lookup will be discarded. You can\n now write an event subscriber that accepts only ``event`` even if it\n subscribes to multiple interfaces::\n\n @subscriber([SomeEvent, SomeContextType])\n def asubscriber(event):\n # this will work!\n\n This prevents you from needing to match the subscriber callable parameters to\n the subscription type unnecessarily, especially when you don't make use of\n any argument in your subscribers except for the event object itself.\n\n Note, however, that if the event object is not the first\n object in the call to ``notify``, you'll run into trouble. For example, if\n notify is called with the context argument first::\n\n registry.notify(context, event)\n\n You won't be able to take advantage of the event-only feature. It will\n \"work\", but the object received by your event handler won't be the event\n object, it will be the context object, which won't be very useful::\n\n @subscriber([SomeContextType, SomeEvent])\n def asubscriber(event):\n # bzzt! you'll be getting the context here as ``event``, and it'll \n # be useless\n\n Existing multiple-argument subscribers continue to work without issue, so you\n should continue use those if your system notifies using multiple interfaces\n and the first interface is not the event interface. For example::\n\n @subscriber([SomeContextType, SomeEvent])\n def asubscriber(context, event):\n # this will still work!\n\n The event-only feature makes it possible to use a subscriber predicate that\n accepts only a request argument within both multiple-interface subscriber\n registrations and single-interface subscriber registrations. You needn't\n make slightly different variations of predicates depending on the\n subscription type arguments. Instead, just write all your subscriber\n predicates so they only accept ``event`` in their ``__call__`` and they'll be\n useful across all registrations for subscriptions that use an event as their\n first argument, even ones which accept more than just ``event``.\n\n However, the same caveat applies to predicates as to subscriber callables: if\n you're subscribing to a multi-interface event, and the first interface is not\n the event interface, the predicate won't work properly. In such a case,\n you'll need to match the predicate ``__call__`` argument ordering and\n composition to the ordering of the interfaces. For example, if the\n registration for the subscription uses ``[SomeContext, SomeEvent]``, you'll\n need to reflect that in the ordering of the parameters of the predicate's\n ``__call__`` method::\n\n def __call__(self, context, event):\n return event.request.path.startswith(self.val)\n\n tl;dr: 1) When using multi-interface subscriptions, always use the event type\n as the first subscription registration argument and 2) When 1 is true, use\n only ``event`` in your subscriber and subscriber predicate parameter lists,\n no matter how many interfaces the subscriber is notified with. This\n combination will result in the maximum amount of reusability of subscriber\n predicates and the least amount of thought on your part. Drink responsibly.\n\nBug Fixes\n---------\n\n- A failure when trying to locate the attribute ``__text__`` on route and view\n predicates existed when the ``debug_routematch`` setting was true or when the\n ``pviews`` command was used. See https://github.com/Pylons/pyramid/pull/727\n\nDocumentation\n-------------\n\n- Sync up tutorial source files with the files that are rendered by the\n scaffold that each uses.\n\n1.4a4 (2012-11-14)\n==================\n\nFeatures\n--------\n\n- ``pyramid.authentication.AuthTktAuthenticationPolicy`` has been updated to\n support newer hashing algorithms such as ``sha512``. Existing applications\n should consider updating if possible for improved security over the default\n md5 hashing.\n\n- Added an ``effective_principals`` route and view predicate.\n\n- Do not allow the userid returned from the ``authenticated_userid`` or the\n userid that is one of the list of principals returned by\n ``effective_principals`` to be either of the strings ``system.Everyone`` or\n ``system.Authenticated`` when any of the built-in authorization policies that\n live in ``pyramid.authentication`` are in use. These two strings are\n reserved for internal usage by Pyramid and they will not be accepted as valid\n userids.\n\n- Slightly better debug logging from\n ``pyramid.authentication.RepozeWho1AuthenticationPolicy``.\n\n- ``pyramid.security.view_execution_permitted`` used to return ``True`` if no\n view could be found. It now raises a ``TypeError`` exception in that case, as\n it doesn't make sense to assert that a nonexistent view is\n execution-permitted. See https://github.com/Pylons/pyramid/issues/299.\n\n- Allow a ``_depth`` argument to ``pyramid.view.view_config``, which will\n permit limited composition reuse of the decorator by other software that\n wants to provide custom decorators that are much like view_config.\n\n- Allow an iterable of decorators to be passed to\n ``pyramid.config.Configurator.add_view``. This allows views to be wrapped\n by more than one decorator without requiring combining the decorators\n yourself.\n\nBug Fixes\n---------\n\n- In the past if a renderer returned ``None``, the body of the resulting\n response would be set explicitly to the empty string. Instead, now, the body\n is left unchanged, which allows the renderer to set a body itself by using\n e.g. ``request.response.body = b'foo'``. The body set by the renderer will\n be unmolested on the way out. See\n https://github.com/Pylons/pyramid/issues/709\n\n- In uncommon cases, the ``pyramid_excview_tween_factory`` might have\n inadvertently raised a ``KeyError`` looking for ``request_iface`` as an\n attribute of the request. It no longer fails in this case. See\n https://github.com/Pylons/pyramid/issues/700\n\n- Be more tolerant of potential error conditions in ``match_param`` and\n ``physical_path`` predicate implementations; instead of raising an exception,\n return False.\n\n- ``pyramid.view.render_view`` was not functioning properly under Python 3.x\n due to a byte/unicode discrepancy. See\n http://github.com/Pylons/pyramid/issues/721\n\nDeprecations\n------------\n\n- ``pyramid.authentication.AuthTktAuthenticationPolicy`` will emit a warning if\n an application is using the policy without explicitly passing a ``hashalg``\n argument. This is because the default is \"md5\" which is considered\n theoretically subject to collision attacks. If you really want \"md5\" then you\n must specify it explicitly to get rid of the warning.\n\nDocumentation\n-------------\n\n- All of the tutorials that use\n ``pyramid.authentication.AuthTktAuthenticationPolicy`` now explicitly pass\n ``sha512`` as a ``hashalg`` argument.\n\n\nInternals\n---------\n\n- Move ``TopologicalSorter`` from ``pyramid.config.util`` to ``pyramid.util``,\n move ``CyclicDependencyError`` from ``pyramid.config.util`` to\n ``pyramid.exceptions``, rename ``Singleton`` to ``Sentinel`` and move from\n ``pyramid.config.util`` to ``pyramid.util``; this is in an effort to\n move that stuff that may be an API one day out of ``pyramid.config.util``,\n because that package should never be imported from non-Pyramid code.\n TopologicalSorter is still not an API, but may become one.\n\n- Get rid of shady monkeypatching of ``pyramid.request.Request`` and\n ``pyramid.response.Response`` done within the ``__init__.py`` of Pyramid.\n Webob no longer relies on this being done. Instead, the ResponseClass\n attribute of the Pyramid Request class is assigned to the Pyramid response\n class; that's enough to satisfy WebOb and behave as it did before with the\n monkeypatching.\n\n1.4a3 (2012-10-26)\n==================\n\nBug Fixes\n---------\n\n- The match_param predicate's text method was fixed to sort its values.\n Part of https://github.com/Pylons/pyramid/pull/705\n\n- 1.4a ``pyramid.scripting.prepare`` behaved differently than 1.3 series\n function of same name. In particular, if passed a request, it would not\n set the ``registry`` attribute of the request like 1.3 did. A symptom\n would be that passing a request to ``pyramid.paster.bootstrap`` (which uses\n the function) that did not have a ``registry`` attribute could assume that\n the registry would be attached to the request by Pyramid. This assumption\n could be made in 1.3, but not in 1.4. The assumption can now be made in\n 1.4 too (a registry is attached to a request passed to bootstrap or\n prepare).\n\n- When registering a view configuration that named a Chameleon ZPT renderer\n with a macro name in it (e.g. ``renderer='some/template#somemacro.pt``) as\n well as a view configuration without a macro name it it that pointed to the\n same template (e.g. ``renderer='some/template.pt'``), internal caching could\n confuse the two, and your code might have rendered one instead of the\n other.\n\nFeatures\n--------\n\n- Allow multiple values to be specified to the ``request_param`` view/route\n predicate as a sequence. Previously only a single string value was allowed.\n See https://github.com/Pylons/pyramid/pull/705\n\n- Comments with references to documentation sections placed in scaffold\n ``.ini`` files.\n\n- Added an HTTP Basic authentication policy\n at ``pyramid.authentication.BasicAuthAuthenticationPolicy``.\n\n- The Configurator ``testing_securitypolicy`` method now returns the policy\n object it creates.\n\n- The Configurator ``testing_securitypolicy`` method accepts two new\n arguments: ``remember_result`` and ``forget_result``. If supplied, these\n values influence the result of the policy's ``remember`` and ``forget``\n methods, respectively.\n\n- The DummySecurityPolicy created by ``testing_securitypolicy`` now sets a\n ``forgotten`` value on the policy (the value ``True``) when its ``forget``\n method is called.\n\n- The DummySecurityPolicy created by ``testing_securitypolicy`` now sets a\n ``remembered`` value on the policy, which is the value of the ``principal``\n argument it's called with when its ``remember`` method is called.\n\n- New ``physical_path`` view predicate. If specified, this value should be a\n string or a tuple representing the physical traversal path of the context\n found via traversal for this predicate to match as true. For example:\n ``physical_path='/'`` or ``physical_path='/a/b/c'`` or ``physical_path=('',\n 'a', 'b', 'c')``. This is not a path prefix match or a regex, it's a\n whole-path match. It's useful when you want to always potentially show a\n view when some object is traversed to, but you can't be sure about what kind\n of object it will be, so you can't use the ``context`` predicate. The\n individual path elements inbetween slash characters or in tuple elements\n should be the Unicode representation of the name of the resource and should\n not be encoded in any way.\n\n1.4a2 (2012-09-27)\n==================\n\nBug Fixes\n---------\n\n- When trying to determine Mako defnames and Chameleon macro names in asset\n specifications, take into account that the filename may have a hyphen in\n it. See https://github.com/Pylons/pyramid/pull/692\n\nFeatures\n--------\n\n- A new ``pyramid.session.check_csrf_token`` convenience function was added.\n\n- A ``check_csrf`` view predicate was added. For example, you can now do\n ``config.add_view(someview, check_csrf=True)``. When the predicate is\n checked, if the ``csrf_token`` value in ``request.params`` matches the CSRF\n token in the request's session, the view will be permitted to execute.\n Otherwise, it will not be permitted to execute.\n\n- Add ``Base.metadata.bind = engine`` to alchemy template, so that tables\n defined imperatively will work.\n\nDocumentation\n-------------\n\n- update wiki2 SQLA tutorial with the changes required after inserting\n ``Base.metadata.bind = engine`` into the alchemy scaffold.\n\n1.4a1 (2012-09-16)\n==================\n\nBug Fixes\n---------\n\n- Forward port from 1.3 branch: When no authentication policy was configured,\n a call to ``pyramid.security.effective_principals`` would unconditionally\n return the empty list. This was incorrect, it should have unconditionally\n returned ``[Everyone]``, and now does.\n\n- Explicit url dispatch regexes can now contain colons.\n https://github.com/Pylons/pyramid/issues/629\n\n- On at least one 64-bit Ubuntu system under Python 3.2, using the\n ``view_config`` decorator caused a ``RuntimeError: dictionary changed size\n during iteration`` exception. It no longer does. See\n https://github.com/Pylons/pyramid/issues/635 for more information.\n\n- In Mako Templates lookup, check if the uri is already adjusted and bring\n it back to an asset spec. Normally occurs with inherited templates or\n included components.\n https://github.com/Pylons/pyramid/issues/606\n https://github.com/Pylons/pyramid/issues/607\n\n- In Mako Templates lookup, check for absolute uri (using mako directories) \n when mixing up inheritance with asset specs.\n https://github.com/Pylons/pyramid/issues/662\n\n- HTTP Accept headers were not being normalized causing potentially\n conflicting view registrations to go unnoticed. Two views that only\n differ in the case ('text/html' vs. 'text/HTML') will now raise an error.\n https://github.com/Pylons/pyramid/pull/620\n\n- Forward-port from 1.3 branch: when registering multiple views with an\n ``accept`` predicate in a Pyramid application runing under Python 3, you\n might have received a ``TypeError: unorderable types: function() <\n function()`` exception.\n\nFeatures\n--------\n\n- Configurator.add_directive now accepts arbitrary callables like partials or\n objects implementing ``__call__`` which dont have ``__name__`` and\n ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621\n and https://github.com/Pylons/pyramid/pull/647.\n\n- Third-party custom view, route, and subscriber predicates can now be added\n for use by view authors via\n ``pyramid.config.Configurator.add_view_predicate``,\n ``pyramid.config.Configurator.add_route_predicate`` and\n ``pyramid.config.Configurator.add_subscriber_predicate``. So, for example,\n doing this::\n\n config.add_view_predicate('abc', my.package.ABCPredicate)\n\n Might allow a view author to do this in an application that configured that\n predicate::\n\n @view_config(abc=1)\n\n Similar features exist for ``add_route``, and ``add_subscriber``. See\n \"Adding A Third Party View, Route, or Subscriber Predicate\" in the Hooks\n chapter for more information.\n\n Note that changes made to support the above feature now means that only\n actions registered using the same \"order\" can conflict with one another.\n It used to be the case that actions registered at different orders could\n potentially conflict, but to my knowledge nothing ever depended on this\n behavior (it was a bit silly).\n\n- Custom objects can be made easily JSON-serializable in Pyramid by defining\n a ``__json__`` method on the object's class. This method should return\n values natively serializable by ``json.dumps`` (such as ints, lists,\n dictionaries, strings, and so forth).\n\n- The JSON renderer now allows for the definition of custom type adapters to\n convert unknown objects to JSON serializations.\n\n- As of this release, the ``request_method`` predicate, when used, will also\n imply that ``HEAD`` is implied when you use ``GET``. For example, using\n ``@view_config(request_method='GET')`` is equivalent to using\n ``@view_config(request_method=('GET', 'HEAD'))``. Using\n ``@view_config(request_method=('GET', 'POST')`` is equivalent to using\n ``@view_config(request_method=('GET', 'HEAD', 'POST')``. This is because\n HEAD is a variant of GET that omits the body, and WebOb has special support\n to return an empty body when a HEAD is used.\n\n- ``config.add_request_method`` has been introduced to support extending\n request objects with arbitrary callables. This method expands on the\n previous ``config.set_request_property`` by supporting methods as well as\n properties. This method now causes less code to be executed at\n request construction time than ``config.set_request_property`` in\n version 1.3.\n\n- Don't add a ``?`` to URLs generated by ``request.resource_url`` if the\n ``query`` argument is provided but empty.\n\n- Don't add a ``?`` to URLs generated by ``request.route_url`` if the\n ``_query`` argument is provided but empty.\n\n- The static view machinery now raises (rather than returns) ``HTTPNotFound``\n and ``HTTPMovedPermanently`` exceptions, so these can be caught by the\n NotFound view (and other exception views).\n\n- The Mako renderer now supports a def name in an asset spec. When the def\n name is present in the asset spec, the system will render the template def\n within the template and will return the result. An example asset spec is\n ``package:path/to/template#defname.mako``. This will render the def named\n ``defname`` inside the ``template.mako`` template instead of rendering the\n entire template. The old way of returning a tuple in the form\n ``('defname', {})`` from the view is supported for backward compatibility,\n\n- The Chameleon ZPT renderer now accepts a macro name in an asset spec. When\n the macro name is present in the asset spec, the system will render the\n macro listed as a ``define-macro`` and return the result instead of\n rendering the entire template. An example asset spec:\n ``package:path/to/template#macroname.pt``. This will render the macro\n defined as ``macroname`` within the ``template.pt`` template instead of the\n entire templae.\n\n- When there is a predicate mismatch exception (seen when no view matches for\n a given request due to predicates not working), the exception now contains\n a textual description of the predicate which didn't match.\n\n- An ``add_permission`` directive method was added to the Configurator. This\n directive registers a free-standing permission introspectable into the\n Pyramid introspection system. Frameworks built atop Pyramid can thus use\n the the ``permissions`` introspectable category data to build a\n comprehensive list of permissions supported by a running system. Before\n this method was added, permissions were already registered in this\n introspectable category as a side effect of naming them in an ``add_view``\n call, this method just makes it possible to arrange for a permission to be\n put into the ``permissions`` introspectable category without naming it\n along with an associated view. Here's an example of usage of\n ``add_permission``::\n\n config = Configurator()\n config.add_permission('view')\n\n- The ``UnencryptedCookieSessionFactoryConfig`` now accepts\n ``signed_serialize`` and ``signed_deserialize`` hooks which may be used\n to influence how the sessions are marshalled (by default this is done\n with HMAC+pickle).\n\n- ``pyramid.testing.DummyRequest`` now supports methods supplied by the\n ``pyramid.util.InstancePropertyMixin`` class such as ``set_property``.\n\n- Request properties and methods added via ``config.set_request_property`` or\n ``config.add_request_method`` are now available to tweens.\n\n- Request properties and methods added via ``config.set_request_property`` or\n ``config.add_request_method`` are now available in the request object\n returned from ``pyramid.paster.bootstrap``.\n\n- ``request.context`` of environment request during ``bootstrap`` is now the\n root object if a context isn't already set on a provided request.\n\n- The ``pyramid.decorator.reify`` function is now an API, and was added to\n the API documentation.\n\n- Added the ``pyramid.testing.testConfig`` context manager, which can be used\n to generate a configurator in a test, e.g. ``with testing.testConfig(...):``.\n\n- Users can now invoke a subrequest from within view code using a new\n ``request.invoke_subrequest`` API.\n\nDeprecations\n------------\n\n- The ``pyramid.config.Configurator.set_request_property`` has been\n documentation-deprecated. The method remains usable but the more\n featureful ``pyramid.config.Configurator.add_request_method`` should be\n used in its place (it has all of the same capabilities but can also extend\n the request object with methods).\n\nBackwards Incompatibilities\n---------------------------\n\n- The Pyramid router no longer adds the values ``bfg.routes.route`` or\n ``bfg.routes.matchdict`` to the request's WSGI environment dictionary.\n These values were docs-deprecated in ``repoze.bfg`` 1.0 (effectively seven\n minor releases ago). If your code depended on these values, use\n request.matched_route and request.matchdict instead.\n\n- It is no longer possible to pass an environ dictionary directly to\n ``pyramid.traversal.ResourceTreeTraverser.__call__`` (aka\n ``ModelGraphTraverser.__call__``). Instead, you must pass a request\n object. Passing an environment instead of a request has generated a\n deprecation warning since Pyramid 1.1.\n\n- Pyramid will no longer work properly if you use the\n ``webob.request.LegacyRequest`` as a request factory. Instances of the\n LegacyRequest class have a ``request.path_info`` which return a string.\n This Pyramid release assumes that ``request.path_info`` will\n unconditionally be Unicode.\n\n- The functions from ``pyramid.chameleon_zpt`` and ``pyramid.chameleon_text``\n named ``get_renderer``, ``get_template``, ``render_template``, and\n ``render_template_to_response`` have been removed. These have issued a\n deprecation warning upon import since Pyramid 1.0. Use\n ``pyramid.renderers.get_renderer()``,\n ``pyramid.renderers.get_renderer().implementation()``,\n ``pyramid.renderers.render()`` or ``pyramid.renderers.render_to_response``\n respectively instead of these functions.\n\n- The ``pyramid.configuration`` module was removed. It had been deprecated\n since Pyramid 1.0 and printed a deprecation warning upon its use. Use\n ``pyramid.config`` instead.\n\n- The ``pyramid.paster.PyramidTemplate`` API was removed. It had been\n deprecated since Pyramid 1.1 and issued a warning on import. If your code\n depended on this, adjust your code to import\n ``pyramid.scaffolds.PyramidTemplate`` instead.\n\n- The ``pyramid.settings.get_settings()`` API was removed. It had been\n printing a deprecation warning since Pyramid 1.0. If your code depended on\n this API, use ``pyramid.threadlocal.get_current_registry().settings``\n instead or use the ``settings`` attribute of the registry available from\n the request (``request.registry.settings``).\n\n- These APIs from the ``pyramid.testing`` module were removed. They have\n been printing deprecation warnings since Pyramid 1.0:\n\n * ``registerDummySecurityPolicy``, use\n ``pyramid.config.Configurator.testing_securitypolicy`` instead.\n\n * ``registerResources`` (aka ``registerModels``, use\n ``pyramid.config.Configurator.testing_resources`` instead.\n\n * ``registerEventListener``, use\n ``pyramid.config.Configurator.testing_add_subscriber`` instead.\n\n * ``registerTemplateRenderer`` (aka `registerDummyRenderer``), use\n ``pyramid.config.Configurator.testing_add_template`` instead.\n\n * ``registerView``, use ``pyramid.config.Configurator.add_view`` instead.\n\n * ``registerUtility``, use\n ``pyramid.config.Configurator.registry.registerUtility`` instead.\n\n * ``registerAdapter``, use\n ``pyramid.config.Configurator.registry.registerAdapter`` instead.\n\n * ``registerSubscriber``, use \n ``pyramid.config.Configurator.add_subscriber`` instead.\n\n * ``registerRoute``, use \n ``pyramid.config.Configurator.add_route`` instead.\n\n * ``registerSettings``, use \n ``pyramid.config.Configurator.add_settings`` instead.\n\n- In Pyramid 1.3 and previous, the ``__call__`` method of a Response object\n was invoked before any finished callbacks were executed. As of this\n release, the ``__call__`` method of a Response object is invoked *after*\n finished callbacks are executed. This is in support of the\n ``request.invoke_subrequest`` feature.\n\nDocumentation\n-------------\n\n- Added an \"Upgrading Pyramid\" chapter to the narrative documentation. It\n describes how to cope with deprecations and removals of Pyramid APIs and\n how to show Pyramid-generated deprecation warnings while running tests and\n while running a server.\n\n- Added a \"Invoking a Subrequest\" chapter to the documentation. It describes\n how to use the new ``request.invoke_subrequest`` API.\n\nDependencies\n------------\n\n- Pyramid now requires WebOb 1.2b3+ (the prior Pyramid release only relied on\n 1.2dev+). This is to ensure that we obtain a version of WebOb that returns\n ``request.path_info`` as text.",
+ "release_url": "http://pypi.python.org/pypi/pyramid/1.4b1",
+ "_pypi_ordering": 167,
+ "bugtrack_url": null,
+ "name": "pyramid",
+ "license": "BSD-derived (http://www.repoze.org/LICENSE.txt)",
+ "summary": "The Pyramid web application development framework, a Pylons project",
+ "home_page": "http://pylonsproject.org",
+ "stable_version": null,
+ "cheesecake_installability_id": null
+ },
+ "urls": [
+ {
+ "has_sig": false,
+ "upload_time": "2012-11-22T04:22:10",
+ "comment_text": "",
+ "python_version": "source",
+ "url": "http://pypi.python.org/packages/source/p/pyramid/pyramid-1.4b1.tar.gz",
+ "md5_digest": "c347a8cf81a2a2d6d6b4a5ba18b236c0",
+ "downloads": 4493,
+ "filename": "pyramid-1.4b1.tar.gz",
+ "packagetype": "sdist",
+ "size": 2422201
+ }
+ ]
+}'''
View
30 pyshop/views/package.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from pyramid.httpexceptions import HTTPNotFound
+from pyshop.models import User, Package, Classifier, Release, ReleaseFile
+
+from .base import View
+
+from pyshop.helpers.i18n import trans as _
+
+log = logging.getLogger(__name__)
+
+
+class List(View):
+
+ def render(self, request, session):
+
+ return {u'local_packages': Package.get_locals(session),
+ u'mirrored_packages': Package.get_mirrored(session),
+ }
+
+
+class Show(View):
+
+ def render(self, request, session):
+
+ package = Package.by_name(session, request.matchdict['package_name'])
+ if not package:
+ raise HTTPNotFound()
+ return { u'package': package}
View
24 pyshop/views/repository.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from pyshop.models import DBSession, ReleaseFile
+from pyshop.helpers.i18n import trans as _
+
+log = logging.getLogger(__name__)
+
+
+def get_release_file(root, request):
+ session = DBSession()
+
+ f = ReleaseFile.by_id(session, int(request.matchdict['file_id']))
+ rv = {'id': f.id,
+ 'url': f.url,
+ 'filename': f.filename,
+ }
+ f.downloads += 1
+ f.release.downloads += 1
+ f.release.package.downloads += 1
+ session.add(f.release.package)
+ session.add(f.release)
+ session.add(f)
+ return rv
View
227 pyshop/views/simple.py
@@ -0,0 +1,227 @@
+# -*- coding: utf-8 -*-
+import logging
+import os.path
+from datetime import datetime
+
+from sqlalchemy.sql.expression import func
+
+from pyramid import httpexceptions as exc
+from pyramid.url import route_url
+
+from pyshop.models import User, Package, Classifier, Release, ReleaseFile
+from pyshop.helpers import pypi
+from pyshop.helpers.i18n import trans as _
+from pyshop.tasks import MirrorPyPIRelease
+
+from .base import View
+
+
+log = logging.getLogger(__name__)
+
+
+class List(View):
+
+ def render(self, request, session):
+ if request.method == 'POST':
+ method, data = request.headers['Authorization'].split()
+ if method != 'Basic':
+ raise exc.HTTPBadRequest()
+
+ username, password = data.decode('base64').split(':', 1)
+ remote_user = User.by_credentials(session, username, password)
+ if not remote_user:
+ raise exc.HTTPForbidden()
+
+ params = request.params
+ pkg = Package.by_name(session, params['name'])
+
+ if pkg:
+ auth = [user for user in pkg.owners + pkg.maintainers
+ if user == remote_user]
+ if not auth:
+ raise exc.HTTPForbidden()
+ else:
+ pkg = Package(name=params['name'], local=True)
+ pkg.owners.append(remote_user)
+
+ input_file = request.POST['content'].file
+ filename = request.POST['content'].filename.split(os.path.sep)[-1]
+ dir_ = os.path.join(request.registry.settings['repository.root'],
+ filename[0].lower())
+ if not os.path.exists(dir_):
+ os.mkdir(dir_, 0750)
+
+ basename, ext = filename.split('.', 1)
+ filepath = os.path.join(dir_, filename)
+ cnt = 1
+ while os.path.exists(filepath):
+ filename = '%s[%i].%s' % (basename, cnt, ext)
+ filepath = os.path.join(dir_, filename)
+ cnt += 1
+
+ with open(filepath, 'wb') as output_file:
+ input_file.seek(0)
+ while True:
+ data = input_file.read(2<<16)
+ if not data:
+ break
+ output_file.write(data)
+ output_file.close()
+
+ release = Release.by_version(session, pkg.name,
+ params.get('version'))
+ if not release:
+ release = Release(package=pkg,
+ version = params.get('version'),
+ summary = params.get('summary'),
+ author=remote_user,
+ home_page=params.get('home_page'),
+ license=params.get('license'),
+ description=params.get('description'),
+ keywords=params.get('keywords'),
+ platform=params.get('platform'),
+ download_url=params.get('download_url'),
+ docs_url=params.get('docs_url'),
+ )
+
+ classifiers = params.getall('classifiers')
+ for name in classifiers:
+ classifier = Classifier.by_name(session, name)
+ if not classifier:
+ classifier = Classifier(name=name)
+ session.add(classifier)
+
+ rfile = ReleaseFile(release=release,
+ filename=filename,
+ md5_digest=params.get('md5_digest'),
+ package_type=params.get('filetype'),
+ python_version=params.get('pyversion'),
+ comment_text=params.get('comment'),
+ )
+
+ session.add(rfile)
+ session.add(release)
+ pkg.update_at = func.now()
+ session.add(pkg)
+
+ return {}
+
+ return {'packages': Package.all(session, order_by=Package.name)}
+
+
+class Show(View):
+
+ def _create_release(self, session, package, data):
+ release = Release(package=package,
+ summary=data.get('summary'),
+ version=data.get('version'),
+ stable_version=data.get('stable_version'),
+ home_page=data.get('home_page'),
+ license=data.get('license'),
+ description=data.get('description'),
+ keywords=data.get('keywords'),
+ platform=data.get('platform'),
+ download_url=data.get('download_url'),
+ bugtrack_url=data.get('bugtrack_url'),
+ docs_url=data.get('docs_url'),
+ )
+ if data.get('author'):
+ author = User.by_login(session, data['author'], local=False)
+ if not author:
+ author = User(login=data['author'],
+ local=False,
+ email=data.get('author_email'))
+ session.add(author)
+ release.author = author
+ session.flush()
+ if data.get('maintainer'):
+ maintainer = User.by_login(session, data['maintainer'], local=False)
+ if not maintainer:
+ maintainer = User(login=data['maintainer'],