Skip to content
This repository has been archived by the owner on Oct 2, 2021. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
stdrc committed Mar 10, 2017
0 parents commit 49632bb
Show file tree
Hide file tree
Showing 28 changed files with 1,126 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
.idea
*.iml
.DS_Store

veripress_instance
21 changes: 21 additions & 0 deletions .travis.yml
@@ -0,0 +1,21 @@
language: python
python:
- '3.4'
- "3.5"

before_install:
- nvm install 4

install:
- pip install .
- pip install coverage
- pip install pytest
- pip install pytest-cov
- pip install coveralls

script:
export VERIPRESS_INSTANCE_PATH=$(pwd)/veripress_tests/instance && \
coverage run --source veripress -m py.test veripress_tests

after_success:
coveralls
19 changes: 19 additions & 0 deletions LICENSE
@@ -0,0 +1,19 @@
The MIT License (MIT)
Copyright (c) 2017 Richard Chien

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2 changes: 2 additions & 0 deletions MANIFEST.in
@@ -0,0 +1,2 @@
include veripress/config.sample.py
include veripress/site.sample.py
3 changes: 3 additions & 0 deletions README.md
@@ -0,0 +1,3 @@
# VeriPress

It's not finished yet.
22 changes: 22 additions & 0 deletions setup.py
@@ -0,0 +1,22 @@
from distutils.core import setup

setup(
name='veripress',
version='1.0.0',
packages=['veripress'],
url='',
license='The MIT License',
author='Richard Chien',
author_email='richardchienthebest@gmail.com',
description='A blog software for hackers.',
install_requires=[
'flask', 'flask-caching', 'pyyaml'
],
tests_require=[
'pytest'
],
include_package_data=True,
entry_points=dict(
console_scripts=['veripress=veripress.cli:main']
)
)
67 changes: 67 additions & 0 deletions veripress/__init__.py
@@ -0,0 +1,67 @@
import json
import os

from flask import Flask, send_from_directory
from werkzeug.exceptions import NotFound


class CustomFlask(Flask):
"""
Customize the original Flask class to fit the needs of this VeriPress project.
"""

def send_static_file(self, filename):
"""
Send static files from the static folder in the current selected theme prior to the global static folder.
:param filename: static filename
:return: response object
"""
theme_static_folder = getattr(self, 'theme_static_folder', None)
if theme_static_folder:
try:
return send_from_directory(theme_static_folder, filename)
except NotFound:
pass
return super(CustomFlask, self).send_static_file(filename)


def create_app(config_filename, instance_path=None):
"""
Factory function to create Flask application object.
:param config_filename: absolute or relative (rel to the instance path) filename of the config file
:param instance_path: the instance path to initialize or run a VeriPress app
:return: a Flask app object
"""
app_ = CustomFlask(__name__,
instance_path=instance_path or os.environ.get('VERIPRESS_INSTANCE_PATH') or os.getcwd(),
instance_relative_config=True)
app_.config.from_pyfile(config_filename)

theme_folder = os.path.join(app_.instance_path, 'themes', app_.config['THEME'])
app_.template_folder = os.path.join(theme_folder, 'templates') # use templates in the selected theme's folder
app_.theme_static_folder = os.path.join(theme_folder, 'static') # use static files in the selected theme's folder

return app_


app = create_app('config.py')

with app.open_instance_resource('site.json', mode='r') as f:
# load site meta info to the site object
site = json.load(f)


@app.context_processor
def inject_site():
"""
Inject the site object into the context of templates.
"""
return dict(site=site)


from flask_caching import Cache
cache = Cache(app, config=app.config) # create the cache object with the app's config

import veripress.model
75 changes: 75 additions & 0 deletions veripress/helpers.py
@@ -0,0 +1,75 @@
import os
import functools
from collections import Iterable
from datetime import date, datetime

from flask import render_template, request


def view(template=None):
"""
Decorate a view function with a default template name.
This will try template in the custom folder first, the theme's original ones second.
:param template: template name
"""

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
template_name = template
if template_name is None:
template_name = request.endpoint.replace('.', '/') + '.html'
context = func(*args, **kwargs)
if context is None:
context = {}
elif not isinstance(context, dict):
return context
return render_template([os.path.join('custom', template_name), template_name], **context)

return wrapper

return decorator


def url_rule(blueprint_or_app, rules, endpoint=None, view_func=None, **options):
"""
Add one or more url rules to the given Flask blueprint or app.
:param blueprint_or_app: Flask blueprint or app
:param rules: a single rule string or a list of rules
:param endpoint: endpoint
:param view_func: view function
:param options: other options
"""
for rule in to_list(rules):
blueprint_or_app.add_url_rule(rule, endpoint=endpoint, view_func=view_func, **options)


def to_list(item_or_list):
"""
Convert a single item, a tuple, a generator or anything else to a list.
:param item_or_list: single item or iterable to convert
:return: a list
"""
if isinstance(item_or_list, list):
return item_or_list
elif isinstance(item_or_list, (str, bytes)):
return [item_or_list]
elif isinstance(item_or_list, Iterable):
return list(item_or_list)
else:
return [item_or_list]


def to_datetime(date_or_datetime):
if isinstance(date_or_datetime, date) and not isinstance(date_or_datetime, datetime):
d = date_or_datetime
return datetime.strptime('-'.join([str(d.year), str(d.month), str(d.day)]), '%Y-%m-%d')
return date_or_datetime


class ConfigurationError(Exception):
"""Raise this when there's something wrong with the configuration."""
pass
58 changes: 58 additions & 0 deletions veripress/model/__init__.py
@@ -0,0 +1,58 @@
from datetime import datetime

from flask import current_app, g
from werkzeug.local import LocalProxy

import veripress.model.storages
from veripress.model.models import Base
from veripress.helpers import ConfigurationError


def get_storage():
"""
Get storage object of current app context, will create a new one if not exists.
:return: a storage object
:raise: ConfigurationError: storage type in current_app.config is not supported
"""
storage_ = getattr(g, '_storage', None)
if storage_ is None:
storage_type = current_app.config['STORAGE_TYPE']
if storage_type == 'file':
storage_ = g._storage = storages.FileStorage(current_app.config)
else:
raise ConfigurationError('Storage type "{}" is not supported.'.format(storage_type))
return storage_


storage = LocalProxy(get_storage)

from veripress import app


@app.teardown_appcontext
def teardown_storage(e):
"""
Automatically called when Flask tears down the app context.
This will close the storage object created at the beginning of the current app context.
"""
storage_ = getattr(g, '_storage', None)
if storage_ is not None:
storage_.close()


class CustomJSONEncoder(app.json_encoder):
"""
Converts model objects to dicts, datetime to timestamp,
so that they can be serialized correctly.
"""

def default(self, obj):
if isinstance(obj, Base):
return obj.to_dict()
elif isinstance(obj, datetime):
return obj.timestamp()
return super(CustomJSONEncoder, self).default(obj)


app.json_encoder = CustomJSONEncoder # use the customized JSON encoder when jsonify is called

0 comments on commit 49632bb

Please sign in to comment.