Initial Commit

This commit is contained in:
tanshu 2017-02-25 00:55:47 +05:30
commit 170a2ee6ec
19 changed files with 623 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__pycache__
*.py[cod]
/.tox
/env*/
.idea/
*.egg-info/

4
CHANGES.txt Normal file
View File

@ -0,0 +1,4 @@
0.0
---
- Initial version.

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include *.txt *.ini *.cfg *.rst
recursive-include randy *.ico *.txt

51
README.txt Normal file
View File

@ -0,0 +1,51 @@
randy
===============================
Getting Started
---------------
- Change directory into your newly created project.
cd randy
- Create a Python virtual environment.
python3 -m venv env
- Upgrade packaging tools.
env/bin/pip install --upgrade pip setuptools
- Install the project in editable mode.
env/bin/pip install -e
- Configure the database.
env/bin/initialize_randy_db development.ini
- Once configured, scan the files by running the script.
env/bin/scan development.ini
- Run your project.
env/bin/pserve development.ini
Picture Model
---------------
- Uri:
Format:
name_of_directory_added:path/name
eg.
Pictures:Vanya/Italy/DSC001.jpg
- Hash
Format:
default is sha-2
algorithm:hash
- Weight
Default value is 4096. Every thumbs up double value, every thumbs down will halve it.

65
development.ini Normal file
View File

@ -0,0 +1,65 @@
###
# app configuration
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:randy
pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
sqlalchemy.url = postgresql://postgres:123456@localhost:5432/randy
directories = Pen Drive:c:\Users\tanshu\Desktop\Pen Drive\
Pictures:c:\Users\tanshu\Pictures\
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
listen = 127.0.0.1:5000 [::1]:5000
###
# logging configuration
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, randy, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[logger_randy]
level = DEBUG
handlers =
qualname = randy
[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:%(lineno)s][%(threadName)s] %(message)s

4
install.sh Normal file
View File

@ -0,0 +1,4 @@
python3 -m venv env
./env/bin/pip install --upgrade pip
./env/bin/pip install -e .

66
production.ini Normal file
View File

@ -0,0 +1,66 @@
###
# app configuration
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:randy
pyramid.reload_templates = false
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
sqlalchemy.url = postgresql://postgres:123456@localhost:5432/randy
directories = Pictures:/home/user/Pictures/
Others:c:\Users\user\Pictures\
###
# wsgi server configuration
###
[server:main]
use = egg:waitress#main
listen = *:5000
###
# logging configuration
# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, randy, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_randy]
level = WARN
handlers =
qualname = randy
[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:%(lineno)s][%(threadName)s] %(message)s

15
randy.service Normal file
View File

@ -0,0 +1,15 @@
[Unit]
Description=Tanshu's Random Image Server
After=multi-user.target
[Service]
Type=simple
Restart=always
ExecStart=/home/tanshu/Programming/randy/env/bin/pserve production.ini
User=tanshu
WorkingDirectory=/home/tanshu/Programming/randy
ExecStop=/usr/bin/pkill -f /home/tanshu/Programming/randy/env/bin/pserve
[Install]
WantedBy=multi-user.target

12
randy/__init__.py Normal file
View File

@ -0,0 +1,12 @@
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
config.include('.models')
config.include('.routes')
config.scan()
return config.make_wsgi_app()

73
randy/models/__init__.py Normal file
View File

@ -0,0 +1,73 @@
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
from .picture import Picture # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
configure_mappers()
def get_engine(settings, prefix='sqlalchemy.'):
return engine_from_config(settings, prefix)
def get_session_factory(engine):
factory = sessionmaker()
factory.configure(bind=engine)
return factory
def get_tm_session(session_factory, transaction_manager):
"""
Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
This function will hook the session to the transaction manager which
will take care of committing any changes.
- When using pyramid_tm it will automatically be committed or aborted
depending on whether an exception is raised.
- When using scripts you should wrap the session in a manager yourself.
For example::
import transaction
engine = get_engine(settings)
session_factory = get_session_factory(engine)
with transaction.manager:
dbsession = get_tm_session(session_factory, transaction.manager)
"""
dbsession = session_factory()
zope.sqlalchemy.register(
dbsession, transaction_manager=transaction_manager)
return dbsession
def includeme(config):
"""
Initialize the model for a Pyramid app.
Activate this setup using ``config.include('randy.models')``.
"""
settings = config.get_settings()
# use pyramid_tm to hook the transaction lifecycle to the request
config.include('pyramid_tm')
session_factory = get_session_factory(get_engine(settings))
config.registry['dbsession_factory'] = session_factory
# make request.dbsession available for use in Pyramid
config.add_request_method(
# r.tm is the transaction manager used by pyramid_tm
lambda r: get_tm_session(session_factory, r.tm),
'dbsession',
reify=True
)

57
randy/models/guidtype.py Normal file
View File

@ -0,0 +1,57 @@
import uuid
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.dialects.sqlite import BLOB
from sqlalchemy.types import TypeDecorator, CHAR, Binary
class GUID(TypeDecorator):
"""Platform-independent GUID type.
Uses Postgresql's UUID type, otherwise uses
CHAR(32), storing as stringified hex values.
"""
impl = Binary
# if dialect.value == 'postgresql':
# impl = CHAR
# elif dialect.value == 'mysql':
# impl = MSBinary
# elif dialect.valie == 'sqlite':
# impl = Binary
# else:
# impl = Binary
def load_dialect_impl(self, dialect):
if dialect.name == 'postgresql':
return dialect.type_descriptor(UUID())
elif dialect.name == 'sqlite':
return dialect.type_descriptor(BLOB())
else:
return dialect.type_descriptor(CHAR(32))
def process_bind_param(self, value, dialect):
if value is None:
return None
elif dialect.name == 'postgresql':
return str(value)
elif not isinstance(value, uuid.UUID):
raise ValueError('value %s is not a valid uuid.UUID' % value)
else:
return value.bytes
# if not isinstance(value, uuid.UUID):
# return "%.32x" % uuid.UUID(value)
# else:
# # hexstring
# return "%.32x" % value
def process_result_value(self, value, dialect=None):
if value is None:
return None
elif isinstance(value, bytes):
return uuid.UUID(bytes=value)
else:
return uuid.UUID(value)
def is_mutable(self):
return False

16
randy/models/meta.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
}
metadata = MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)

46
randy/models/picture.py Normal file
View File

@ -0,0 +1,46 @@
import uuid
from sqlalchemy import (
Column,
Integer,
Unicode,
DateTime,
SmallInteger,
UniqueConstraint
)
from sqlalchemy.dialects.postgresql import JSON
from randy.models.meta import Base
from randy.models.guidtype import GUID
class Picture(Base):
__tablename__ = 'pictures'
__table_args__ = (UniqueConstraint('modified_time', 'uri', 'hash', 'size'), )
id = Column('id', GUID(), primary_key=True, default=uuid.uuid4)
uri = Column('uri', Unicode(255), unique=True)
weight = Column('weight', SmallInteger)
file_hash = Column('hash', Unicode(255))
exif = Column('exif', JSON)
cloud_vision = Column('cloud_vision', JSON)
size = Column('size', Integer)
modified_time = Column('modified_time', DateTime)
def __init__(self,
uri=None,
weight=4096,
file_hash=None,
exif=None,
cloud_vision=None,
size=None,
modified_date=None,
picture_id=None):
self.uri = uri
self.weight = weight
self.file_hash = file_hash
self.exif = exif
self.cloud_vision = cloud_vision
self.size = size
self.modified_date = modified_date
self.id = picture_id

3
randy/routes.py Normal file
View File

@ -0,0 +1,3 @@
def includeme(config):
config.add_route('random', '/random')
config.add_route('id', '/{id}')

View File

@ -0,0 +1 @@
# package

View File

@ -0,0 +1,36 @@
import os
import sys
import transaction
from pyramid.paster import (
get_appsettings,
setup_logging,
)
from pyramid.scripts.common import parse_vars
from ..models.meta import Base
from ..models import (
get_engine,
get_session_factory,
get_tm_session,
)
def usage(argv):
cmd = os.path.basename(argv[0])
print('usage: %s <config_uri> [var=value]\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]
options = parse_vars(argv[2:])
setup_logging(config_uri)
settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
Base.metadata.create_all(engine)

86
randy/scripts/scan.py Normal file
View File

@ -0,0 +1,86 @@
import glob
import hashlib
import os
import sys
import time
import transaction
from pyramid.paster import (
get_appsettings,
setup_logging,
)
from pyramid.scripts.common import parse_vars
from ..models import (
get_engine,
get_session_factory,
get_tm_session,
Picture
)
def usage(argv):
cmd = os.path.basename(argv[0])
print('usage: %s <config_uri> [var=value]\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]
options = parse_vars(argv[2:])
setup_logging(config_uri)
settings = get_appsettings(config_uri, options=options)
engine = get_engine(settings)
session_factory = get_session_factory(engine)
with transaction.manager:
dbsession = get_tm_session(session_factory, transaction.manager)
scan(settings['directories'].splitlines(), dbsession)
def scan(directories, dbsession):
for item in directories:
name, path = item.split(':', 1)
getFiles(name, path, dbsession)
def getFiles(name, path, dbsession):
files = glob.glob(path + '**/*.jpg', recursive=True)
for file in files:
uri = name + ':' + file[len(path):]
size = os.path.getsize(file)
modified_time = time.gmtime(os.path.getmtime(file))
old = dbsession.query(Picture).filter(Picture.uri == uri).first()
# if old is not None and old.size == size and old.modified_time == modified_time:
if old is not None:
print('Old file with ' + old.file_hash + ' exists!')
continue
file_hash = getHash(file)
# if old is not None:
# old.size = size
# old.modified_time = modified_time
# print('updated old', old.modified_time, 'with', modified_time)
# old.file_hash = file_hash
# print('Updated file with ' + file_hash)
# else:
picture = Picture(uri, 4096, file_hash, {}, {}, size, modified_time)
dbsession.add(picture)
print('Added file with ' + file_hash)
def getHash(file):
# BUF_SIZE is totally arbitrary, change for your app!
BUF_SIZE = 1 * 1024 * 1024 # lets read stuff in 64kb chunks!
sha256 = hashlib.sha256()
with open(file, 'rb') as f:
while True:
data = f.read(BUF_SIZE)
if not data:
break
sha256.update(data)
return "SHA256:{0}".format(sha256.hexdigest())

31
randy/views.py Normal file
View File

@ -0,0 +1,31 @@
import random
from pyramid.response import Response, FileResponse
from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
from .models import Picture
@view_config(route_name='random')
def random_view(request):
try:
chosen = weighted_choice(request.dbsession.query(Picture.id, Picture.weight).all())
item = request.dbsession.query(Picture).filter(Picture.id == chosen).first()
except DBAPIError:
return Response("", content_type='text/plain', status=500)
directories = request.registry.settings['directories'].splitlines()
repo, path = item.uri.split(':', 1)
location = [i for i in directories if i.startswith(repo)][0]
file = location[len(repo) + 1:] + path
return FileResponse(file, request=request)
def weighted_choice(choices):
total = sum(w for c, w in choices)
r = random.uniform(0, total)
upto = 0
for c, w in choices:
upto += w
if upto >= r:
return c
assert False, "Shouldn't get here"

49
setup.py Normal file
View File

@ -0,0 +1,49 @@
import os
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'README.txt')) as f:
README = f.read()
with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
'pyramid',
'pyramid_tm',
'SQLAlchemy',
'psycopg2',
'transaction',
'zope.sqlalchemy',
'waitress',
]
setup(
name='randy',
version='0.0',
description='randy',
long_description=README + '\n\n' + CHANGES,
classifiers=[
'Programming Language :: Python',
'Framework :: Pyramid',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
],
author='',
author_email='',
url='',
keywords='web pyramid pylons',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=requires,
entry_points={
'paste.app_factory': [
'main = randy:main',
],
'console_scripts': [
'initialize_db = randy.scripts.initializedb:main',
'scan = randy.scripts.scan:main',
],
},
)