Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

DB restructured. Application adapted

Due to sqlalchemy-migrate limitation, the old DB must be deleted :/
  • Loading branch information...
commit 7c08af8d1f8ca7193f8ab546b4e5dfe2934d6316 1 parent 77f6079
@exhuma authored
View
126 db_repo/versions/001_Initial_DB_creation.py
@@ -3,65 +3,114 @@
metadata = MetaData()
-projects_table = Table(
- 'project', metadata,
+packages_table = Table(
+ 'package', metadata,
Column('name', String, primary_key=True),
Column('inserted', DateTime, nullable=False, default=func.now()),
- Column('updated', DateTime, nullable=False, default=func.now())
+ Column('updated', DateTime, nullable=False, default=func.now()),
+
+ #doc="A python package",
)
users_table = Table(
'user', metadata,
- Column('email', String, primary_key=True),
- Column('password', String),
- Column('inserted', DateTime, nullable=False, default=func.now()),
- Column('updated', DateTime, nullable=False, default=func.now())
+ Column('email', String, primary_key=True,
+ doc="An email uniquely identifies a user account"),
+ Column('full_name', String,
+ doc="The user name"),
+ Column('password', String,
+ doc="A password hash"),
+ Column('verified', Boolean, default=False,
+ doc="Whether the user has verified his/her e-mail"),
+ Column('verification_token', String,
+ doc="A token used to verify the user's e-mail"),
+ Column('verification_token_exires', DateTime,
+ doc="The verification token expires after this date"),
+ Column('inserted', DateTime, nullable=False, default=func.now(),
+ doc="The timestamp when this user was registered in the system"),
+ Column('updated', DateTime, nullable=False, default=func.now(),
+ doc="The timestamp when the user was last modified"),
+ #doc="A user in the system",
+ )
+
+package_auth = Table(
+ 'package_auth', metadata,
+ Column('user', String, ForeignKey('user.email')),
+ Column('package', String, ForeignKey('package.name')),
+ Column('grant_mask', Integer,
+ doc="Bitmask defining the rights granted to this user for this project"),
+ PrimaryKeyConstraint('user', 'package'),
+ #doc="Defines access rights to packages for users"
)
release_table = Table(
'release', metadata,
- Column('project', String, ForeignKey('project.name')),
- Column('license', String),
- Column('metadata_version', String),
- Column('author', String),
- Column('author_email', String, ForeignKey('user.email')),
- Column('home_page', String),
- Column('download_url', String),
- Column('summary', String),
- Column('version', String),
- Column('platform', String),
- Column('description', String),
- Column('inserted', DateTime, nullable=False, default=func.now()),
- Column('updated', DateTime, nullable=False, default=func.now()),
- PrimaryKeyConstraint('project', 'author_email', 'version'),
+ Column('package', String, ForeignKey('package.name'),
+ doc="The reference to the package"),
+ Column('license', String,
+ doc="The license name"),
+ Column('metadata_version', String,
+ doc="The metadata version (from setup.py)"),
+ Column('author_email', String, ForeignKey('user.email'),
+ doc="The author email (=the user uploading the package)"),
+ Column('home_page', String,
+ doc="The home page for this release"),
+ Column('download_url', String,
+ doc="A URL where the release can be downloaded"),
+ Column('summary', String,
+ doc="Summary description"),
+ Column('version', String,
+ doc="Release version string"),
+ Column('platform', String,
+ doc="Target platform for this release"),
+ Column('description', String,
+ doc="Long description"),
+ Column('inserted', DateTime, nullable=False, default=func.now(),
+ doc="Timestamp when the release was added/uploaded"),
+ Column('updated', DateTime, nullable=False, default=func.now(),
+ doc="Timestamp when this release was last edited"),
+ PrimaryKeyConstraint('package', 'version'),
+ #doc="Metadata for one package release"
)
files_table = Table(
'file', metadata,
- Column('project', String),
- Column('author_email', String),
- Column('version', String),
- Column('md5_digest', String(32)),
-
- Column('filename', Unicode),
- Column('comment', String),
- Column('filetype', String),
- Column('pyversion', String),
+ Column('package', String,
+ doc="The package to which this file belongs to"),
+ Column('author_email', String,
+ ForeignKey('user.email'),
+ doc="The user who uploaded this file"),
+ Column('version', String,
+ doc="The package version for this file"),
+ Column('md5_digest', String(32),
+ doc="MD5 Hash of the file contents"),
+ Column('filename', Unicode,
+ doc="The filename"),
+ Column('comment', String,
+ doc="Comment as specified by the uploader"),
+ Column('filetype', String,
+ doc="File type"),
+ Column('pyversion', String,
+ doc="Target python version"),
Column('protcol_version', Integer),
+ Column('data', LargeBinary),
+ Column('inserted', DateTime, nullable=False, default=func.now(),
+ doc="Timestamp when this file was uploaded"),
+ Column('updated', DateTime, nullable=False, default=func.now(),
+ doc="Timestamp when this file was last edited"),
- Column('inserted', DateTime, nullable=False, default=func.now()),
- Column('updated', DateTime, nullable=False, default=func.now()),
-
- PrimaryKeyConstraint('project', 'author_email', 'version', 'md5_digest'),
- ForeignKeyConstraint(('project', 'author_email', 'version'),
- ('release.project', 'release.author_email', 'release.version')),
+ PrimaryKeyConstraint('package', 'version', 'filetype'),
+ UniqueConstraint('package', 'filename'),
+ ForeignKeyConstraint(('package', 'version'),
+ ('release.package', 'release.version')),
)
def upgrade(migrate_engine):
metadata.bind = migrate_engine
users_table.create()
- projects_table.create()
+ packages_table.create()
+ package_auth.create()
release_table.create()
files_table.create()
@@ -70,5 +119,6 @@ def downgrade(migrate_engine):
metadata.bind = migrate_engine
files_table.drop()
release_table.drop()
- projects_table.drop()
+ packages_table.drop()
users_table.drop()
+ package_auth.drop()
View
1  mod_wsgi/app.wsgi
@@ -29,6 +29,5 @@ rebind('sqlite:///app.db', False)
# Application config
application.debug = False
-application.config['UPLOAD_FOLDER'] = abspath('files')
# vim: set ft=python :
View
169 mypi/db.py
@@ -5,16 +5,20 @@
from sqlalchemy import (
create_engine,
+ Table,
Column,
String,
Unicode,
Integer,
ForeignKey,
DateTime,
+ Boolean,
+ LargeBinary,
PrimaryKeyConstraint,
ForeignKeyConstraint)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
+from sqlalchemy.exc import IntegrityError
engine = create_engine('sqlite:///dev.db', echo=True)
@@ -22,6 +26,9 @@
Session = scoped_session(sessionmaker(bind=engine))
LOG = logging.getLogger(__name__)
+# Bit-fields for package <-> user rights
+GRANT_READ = 1
+GRANT_WRITE = 2
def rebind(uri, echo=False):
"""
@@ -39,49 +46,59 @@ def rebind(uri, echo=False):
engine.echo = True
Session = scoped_session(sessionmaker(bind=engine))
+package_auth = Table(
+ 'package_auth', Base.metadata,
+ Column('user', String, ForeignKey('user.email')),
+ Column('package', String, ForeignKey('package.name')),
+ Column('grant_mask', Integer,
+ doc="Bitmask defining the rights granted to this user for this project"),
+ PrimaryKeyConstraint('user', 'package'),
+ #doc="Defines access rights to packages for users"
+ )
-class Project(Base):
- __tablename__ = 'project'
+class Package(Base):
+ __tablename__ = 'package'
name = Column(String, primary_key=True)
inserted = Column(DateTime, nullable=False, default=datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.now)
- releases = relationship('Release', lazy="joined")
+ users = relationship("User", secondary=package_auth, backref="packages")
+ releases = relationship('Release')
@classmethod
def get_or_add(self, session, name):
"""
- Return a project reference. If the project does not yet exist, create a
+ Return a package reference. If the package does not yet exist, create a
new one and return that one
"""
- q = session.query(Project)
- q = q.filter(Project.name == name)
+ q = session.query(Package)
+ q = q.filter(Package.name == name)
proj = q.first()
if proj:
return proj
- proj = Project(name)
+ proj = Package(name)
session.add(proj)
return proj
@classmethod
def get(self, session, name):
"""
- Return a project reference
+ Return a package reference
"""
- q = session.query(Project)
- q = q.filter(Project.name == name)
+ q = session.query(Package)
+ q = q.filter(Package.name == name)
proj = q.first()
return proj
@classmethod
def all(self, session):
"""
- Return a list of projects
+ Return a list of packages
"""
- q = session.query(Project)
- q = q.order_by(Project.name)
+ q = session.query(Package)
+ q = q.order_by(Package.name)
return q
def __init__(self, name):
@@ -89,7 +106,7 @@ def __init__(self, name):
def __eq__(self, other):
- if not isinstance(other, Project):
+ if not isinstance(other, Package):
return False
return other.name == self.name and other.author_email == self.author_email
@@ -100,6 +117,10 @@ class User(Base):
email = Column(String, primary_key=True)
password = Column(String)
+ full_name = Column(Unicode)
+ verified = Column(Boolean)
+ verification_token = Column(String)
+ verification_token_exires = Column(DateTime)
inserted = Column(DateTime, nullable=False, default=datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.now)
@@ -110,9 +131,38 @@ def by_auth(self, sess, email, passwd):
q = q.filter(User.password == md5(passwd).hexdigest())
return q.first()
- def __init__(self, email, passwd):
+ @classmethod
+ def by_email(self, sess, email):
+ q = sess.query(User)
+ q = q.filter(User.email == email)
+ return q.first()
+
+ @classmethod
+ def get_or_add(self, session, email, passwd=None, name=None):
+ """
+ Return a user reference. If the user does not yet exist, create a new
+ one and return it
+ """
+ q = session.query(User)
+ q = q.filter(User.email == email)
+ user = q.first()
+ if user:
+ return user
+
+ user = User(email, passwd, name)
+ session.add(user)
+ return user
+
+ def __init__(self, email, passwd=None, name=None):
self.email = email
- self.passwd = md5(passwd).hexdigest() #TODO: add salt
+ if passwd:
+ self.passwd = md5(passwd).hexdigest() #TODO: add salt
+ if name:
+ self.full_name = name
+ # TODO: Create verification-token and send verification-email
+ # TODO: self.verification_token = ...
+ # TODO: self.verification_token_exires = ...
+ self.verified = False
def __eq__(self, other):
@@ -125,16 +175,15 @@ def __eq__(self, other):
class Release(Base):
__tablename__ = 'release'
__table_args__ = (
- PrimaryKeyConstraint('project', 'author_email', 'version'),
+ PrimaryKeyConstraint('package', 'version'),
{}
)
files = relationship('File')
- project = Column(String, ForeignKey('project.name'))
+ package = Column(String, ForeignKey('package.name'))
license = Column(String)
metadata_version = Column(String)
- author = Column(String)
home_page = Column(String)
author_email = Column(String, ForeignKey('user.email'))
download_url = Column(String)
@@ -149,7 +198,7 @@ class Release(Base):
def get(self, session, author_email, name, version):
q = session.query(Release)
q = q.filter(Release.author_email == author_email)
- q = q.filter(Release.project == name)
+ q = q.filter(Release.package == name)
q = q.filter(Release.version == version)
rel = q.first()
return rel
@@ -157,17 +206,17 @@ def get(self, session, author_email, name, version):
@classmethod
def get_or_add(self, session, author_email, name, version):
- # ensure the project exists
- Project.get(session, author_email, name)
+ # ensure the package exists
+ Package.get(session, author_email, name)
q = session.query(Release)
q = q.filter(Release.author_email == author_email)
- q = q.filter(Release.project == name)
+ q = q.filter(Release.package == name)
q = q.filter(Release.version == version)
rel = q.first()
if rel:
raise ValueError("Duplicate release! We already have a release "
- "for this project by this author and version!")
+ "for this package by this author and version!")
rel = Release(author_email, name, version)
@@ -175,8 +224,26 @@ def get_or_add(self, session, author_email, name, version):
return rel
@classmethod
- def add(self, session, data):
- proj = Project.get_or_add(session, data['name'])
+ def register(self, session, data):
+ """
+ Takes metadata sent by "setup.py register" to create a new release
+ """
+
+ #first, we need a package reference to attach this relase to...
+ package = Package.get_or_add(session, data['name'])
+
+ # now let's see if we have a matching user, and if he/she may write to
+ # this package
+ user = User.get_or_add(session, data['author_email'], name=data['author'])
+
+ if user in package.users:
+ # TODO: Check access rights and bail out on denial
+ pass
+ else:
+ # This package does not have any users assigned (it's most likely
+ # a new one. Add the sending user the the mappings
+ # TODO: add the proper privilege bitmask (GRANT_READ & GRANT_WRITE) to this as well.
+ package.users.append(user)
release = Release.get(session,
data['author_email'],
@@ -198,20 +265,11 @@ def add(self, session, data):
release.platform = data["platform"]
release.description = data["description"]
- ## data["author"], TODO: do sth. with this field?
return release
- @classmethod
- def register(self):
- """
- Registers a new project release.
- If the project does not yet exist, it will be created automatically
- """
- pass
-
- def __init__(self, author_email, project, version):
+ def __init__(self, author_email, package, version):
self.author_email = author_email
- self.project = project
+ self.package = package
self.version = version
def __eq__(self, other):
@@ -219,7 +277,7 @@ def __eq__(self, other):
if not isinstance(other, Release):
return False
- return (self.project == other.project
+ return (self.package == other.package
and self.author_email == other.author_email
and self.version == other.version)
@@ -227,27 +285,32 @@ def __eq__(self, other):
class File(Base):
__tablename__ = "file"
__table_args__ = (
- PrimaryKeyConstraint('project', 'author_email', 'version', 'md5_digest'),
- ForeignKeyConstraint(('project', 'author_email', 'version'),
- ('release.project', 'release.author_email', 'release.version')),
+ PrimaryKeyConstraint('package', 'version', 'md5_digest'),
+ ForeignKeyConstraint(('package', 'version'),
+ ('release.package', 'release.version')),
{}
)
- project = Column(String)
+ package = Column(String)
author_email = Column(String)
version = Column(String)
md5_digest = Column(String(32))
+ filename = Column(Unicode)
comment = Column(String)
filetype = Column(String)
pyversion = Column(String)
- filename = Column(Unicode)
protcol_version = Column(Integer)
+ data = Column(LargeBinary)
inserted = Column(DateTime, nullable=False, default=datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.now)
@classmethod
- def add(self, session, data, filename):
+ def upload(self, session, data, filename, fileobj):
+ """
+ This method takes a dictionary of data as sent by "setup.py upload" to
+ create new files.
+ """
# ensure the release exists
rel = Release.get(session, data['author_email'], data['name'], data['version'])
@@ -262,31 +325,33 @@ def add(self, session, data, filename):
file.filetype = data["filetype"]
file.pyversion = data["pyversion"]
file.protcol_version = data["protcol_version"]
+ # TODO: verify MD5sum
+ file.data = fileobj.read()
session.add(file)
@classmethod
- def find(self, session, project, md5):
+ def find(self, session, package, md5):
"""
- Finds a file by project and MD5-digest
+ Finds a file by package and MD5-digest
"""
q = session.query(File)
- q = q.filter(File.project == project)
+ q = q.filter(File.package == package)
q = q.filter(File.md5_digest == md5)
return q.first()
@classmethod
- def find_by_filename(self, session, project, filename):
+ def find_by_filename(self, session, package, filename):
"""
Finds a file by filename
"""
q = session.query(File)
- q = q.filter(File.project == project)
+ q = q.filter(File.package == package)
q = q.filter(File.filename == filename)
return q.first()
- def __init__(self, project, author_email, version, filename, md5_digest):
- self.project = project
+ def __init__(self, package, author_email, version, filename, md5_digest):
+ self.package = package
self.author_email = author_email
self.version = version
self.filename = filename
@@ -297,7 +362,7 @@ def __eq__(self, other):
if not isinstance(other, Release):
return False
- return (self.project == other.project
+ return (self.package == other.package
and self.author_email == other.author_email
and self.version == other.version
and self.md5_digest == other.md5_digest)
View
84 mypi/server.py
@@ -1,11 +1,11 @@
-from os.path import join, exists
-from os import makedirs
+import logging
-from flask import Flask, g, abort, render_template, send_from_directory
+from flask import Flask, g, abort, render_template
from werkzeug.utils import secure_filename
from mypi import db as model
+LOG = logging.getLogger(__name__)
app = Flask(__name__)
@app.before_request
@@ -17,16 +17,17 @@ def teardown_request(exception):
g.db.close()
@app.route("/", methods=['GET'])
-@app.route("/project")
-def hello():
- projects = model.Project.all(g.db)
- return render_template("project_list.html", projects=projects)
+@app.route("/package")
+def index():
+ packages = model.Package.all(g.db)
+ return render_template("package_list.html", packages=packages)
@app.route("/", methods=['POST'])
def post():
from flask import request
frm = request.form
+ #TODO: raise error if :action not in frm
action_name = "_do_%s" % frm[":action"]
action = globals().get(action_name, None)
if action:
@@ -34,69 +35,66 @@ def post():
return abort(501, description="Action %s is not yet implemented" % frm[":action"])
-@app.route("/project/<author_email>/<name>")
-def project(author_email, name):
+@app.route("/package/<name>")
+def package(name):
"""
- Display the Project details
+ Display the Package details
"""
- proj = model.Project.get(g.db, author_email, name)
+ proj = model.Package.get(g.db, name)
if not proj:
- abort(404, description = "No such project")
+ return abort(404, description = "No such package")
- return render_template("project.html", project=proj)
+ return render_template("package.html", package=proj)
-@app.route("/download/<project>/<filename>")
-def download(project, filename):
- file = model.File.find_by_filename(g.db, project, filename)
+@app.route("/download/<package>/<filename>")
+def download(package, filename):
+ file = model.File.find_by_filename(g.db, package, filename)
if not file:
return abort(404, description="File not found")
- folder = join(
- app.config['UPLOAD_FOLDER'],
- file.author_email.replace("@", "_at_"),
- project)
+ from werkzeug.wrappers import Response
+ resp = Response(file.data)
+ resp.headers['Content-Type'] = 'application/octet-stream'
+ resp.headers['Content-Disposition'] = 'attachement'
- return send_from_directory(
- folder, file.filename, as_attachment=True)
+ return resp
@app.route("/simple")
def simple():
"""
- List all available projects
+ List all available packages
"""
- projects = model.Project.all(g.db)
- return render_template("simple/projects.html", projects=projects)
+ packages = model.Package.all(g.db)
+ return render_template("simple/packages.html", packages=packages)
-@app.route("/simple/<project>")
-def simple_project(project):
+@app.route("/simple/<package>")
+@app.route("/simple/<package>/")
+def simple_package(package):
"""
- List all available project releases
+ List all available package releases
"""
- project = model.Project.get(g.db, project)
- if not project:
- abort(404)
- return render_template("simple/releases.html", project=project)
+ package = model.Package.get(g.db, package)
+ if not package:
+ abort(404, description = "No such package")
+ return render_template("simple/releases.html", package=package)
def _do_file_upload(data):
from flask import request
file = request.files['content']
filename = secure_filename(file.filename)
+
+ model.File.upload(g.db, data, filename, file.stream)
+
try:
- model.File.add(g.db, data, filename)
- except ValueError, exc:
- abort(409, description=str(exc))
-
- author_path = data['author_email'].replace('@', '_at_')
- target_dir = join(app.config["UPLOAD_FOLDER"], author_path, data['name'])
- if not exists(target_dir):
- makedirs(target_dir)
- file.save(join(target_dir, filename))
- g.db.commit()
+ g.db.commit()
+ except model.IntegrityError, exc:
+ LOG.error(exc)
+ abort(409, description="This file exists already!")
return "OK"
def _do_submit(data):
try:
- rel = model.Release.add(g.db, data)
+ rel = model.Release.register(g.db, data)
g.db.commit()
return "added release %s" % rel
except ValueError, ex:
View
12 mypi/templates/project.html → mypi/templates/package.html
@@ -2,13 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8">
- <title>Project: {{project.name}}</title>
+ <title>Package: {{package.name}}</title>
</head>
<body>
- <h1>{{project.name}}</h1>
- <h2>{{project.author_email}}</h2>
- <pre>{{project.description}}</pre>
+ <h1>{{package.name}}</h1>
+ <h2>{{package.author_email}}</h2>
+ <pre>{{package.description}}</pre>
<h2>Releases</h2>
<table>
@@ -21,7 +21,7 @@
</tr>
</thead>
<tbody>
- {% for rel in project.releases %}
+ {% for rel in package.releases %}
<tr>
<td>{{ rel.version }}</td>
<td>{{ rel.platform }}</td>
@@ -40,7 +40,7 @@
<tbody>
{% for file in rel.files %}
<tr>
- <td><a href="{{url_for('download', project=project.name, md5=file.md5_digest) }}">{{ file.filename }}</a></td>
+ <td><a href="{{url_for('download', package=package.name, filename=file.filename) }}#md5={{file.md5_digest}}">{{ file.filename }}</a></td>
<td>{{ file.filetype}}</td>
<td>{{ file.pyversion }}</td>
<td>{{ file.md5_digest }}</td>
View
4 mypi/templates/project_list.html → mypi/templates/package_list.html
@@ -7,8 +7,8 @@
<body>
<ul>
- {% for p in projects %}
- <li><a href="{{url_for('project', name=p.name, author_email=p.author_email)}}">{{p.name}}</a></li>
+ {% for p in packages %}
+ <li><a href="{{url_for('package', name=p.name)}}">{{p.name}}</a></li>
{% endfor %}
</ul>
View
4 mypi/templates/simple/projects.html → mypi/templates/simple/packages.html
@@ -6,8 +6,8 @@
</head>
<body>
- {% for p in projects %}
- <a href="{{url_for('simple_project', project=p.name)}}">{{p.name}}</a><br />
+ {% for p in packages %}
+ <a href="{{url_for('simple_package', package=p.name)}}">{{p.name}}</a><br />
{% endfor %}
View
41 mypi/templates/simple/releases.html
@@ -1,22 +1,33 @@
<!DOCTYPE HTML>
<html lang="en">
-<head>
- <meta charset="UTF-8">
- <title>Private Package Index [simple] - Releases for {{project.name}}</title>
-</head>
-<body>
+ <head>
+ <meta charset="UTF-8">
+ <title>Private Package Index [simple] - Releases for {{package.name}}</title>
+ </head>
+ <body>
- {% for rel in project.releases %}
- {% if rel.home_page and rel.home_page != 'UNKNOWN' %}
- <a rel="homepage" href="{{rel.home_page}}">{{rel.version}} homepage</a><br />
- {% endif %}
- {% for file in rel.files %}
- <a href="{{url_for('download', project=project.name,
- filename=file.filename) }}#md5={{file.md5_digest}}">{{ file.filename }}</a><br />
- {% endfor %}
- {% endfor %}
+ <table border="1">
+ <thead>
+ <tr>
+ <th>Homepage</th>
+ <th>Filename</th>
+ <th>MD5</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for rel in package.releases %}
+ {% for file in rel.files %}
+ <tr>
+ <td><a rel="homepage" href="{{rel.home_page}}">{{rel.home_page}}</a></td>
+ <td><a href="{{url_for('download', package=package.name, filename=file.filename) }}#md5={{file.md5_digest}}">{{ file.filename }}</a></td>
+ <td>{{ file.md5_digest }}</td>
+ </tr>
+ {% endfor %}
+ {% endfor %}
+ </tbody>
+ </table>
-</body>
+ </body>
</html>
View
9 runserver.py
@@ -1,9 +1,12 @@
+import logging
+
+logging.basicConfig()
+#logging.getLogger('sqlalchemy.engine').setLevel(logging.CRITICAL)
+
from mypi.db import rebind
from mypi.server import app
-from os.path import abspath
-rebind('sqlite:///app.db', True)
+rebind('sqlite:///app.db', echo=True)
app.debug = True
-app.config['UPLOAD_FOLDER'] = abspath('files')
app.run(host="0.0.0.0", port=8080)
Please sign in to comment.
Something went wrong with that request. Please try again.