Burathar
4 years ago
commit
df3aa7fdcd
19 changed files with 666 additions and 0 deletions
@ -0,0 +1,144 @@ |
|||||||
|
# Source: https://github.com/github/gitignore/blob/master/Python.gitignore |
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files |
||||||
|
__pycache__/ |
||||||
|
*.py[cod] |
||||||
|
*$py.class |
||||||
|
|
||||||
|
# C extensions |
||||||
|
*.so |
||||||
|
|
||||||
|
# Distribution / packaging |
||||||
|
.Python |
||||||
|
build/ |
||||||
|
develop-eggs/ |
||||||
|
dist/ |
||||||
|
downloads/ |
||||||
|
eggs/ |
||||||
|
.eggs/ |
||||||
|
lib/ |
||||||
|
lib64/ |
||||||
|
parts/ |
||||||
|
sdist/ |
||||||
|
var/ |
||||||
|
wheels/ |
||||||
|
share/python-wheels/ |
||||||
|
*.egg-info/ |
||||||
|
.installed.cfg |
||||||
|
*.egg |
||||||
|
MANIFEST |
||||||
|
|
||||||
|
# PyInstaller |
||||||
|
# Usually these files are written by a python script from a template |
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
||||||
|
*.manifest |
||||||
|
*.spec |
||||||
|
|
||||||
|
# Installer logs |
||||||
|
pip-log.txt |
||||||
|
pip-delete-this-directory.txt |
||||||
|
|
||||||
|
# Unit test / coverage reports |
||||||
|
htmlcov/ |
||||||
|
.tox/ |
||||||
|
.nox/ |
||||||
|
.coverage |
||||||
|
.coverage.* |
||||||
|
.cache |
||||||
|
nosetests.xml |
||||||
|
coverage.xml |
||||||
|
*.cover |
||||||
|
*.py,cover |
||||||
|
.hypothesis/ |
||||||
|
.pytest_cache/ |
||||||
|
cover/ |
||||||
|
|
||||||
|
# Translations |
||||||
|
*.mo |
||||||
|
*.pot |
||||||
|
|
||||||
|
# Django stuff: |
||||||
|
*.log |
||||||
|
local_settings.py |
||||||
|
db.sqlite3 |
||||||
|
db.sqlite3-journal |
||||||
|
|
||||||
|
# Flask stuff: |
||||||
|
instance/ |
||||||
|
.webassets-cache |
||||||
|
|
||||||
|
# Scrapy stuff: |
||||||
|
.scrapy |
||||||
|
|
||||||
|
# Sphinx documentation |
||||||
|
docs/_build/ |
||||||
|
|
||||||
|
# PyBuilder |
||||||
|
.pybuilder/ |
||||||
|
target/ |
||||||
|
|
||||||
|
# Jupyter Notebook |
||||||
|
.ipynb_checkpoints |
||||||
|
|
||||||
|
# IPython |
||||||
|
profile_default/ |
||||||
|
ipython_config.py |
||||||
|
|
||||||
|
# pyenv |
||||||
|
# For a library or package, you might want to ignore these files since the code is |
||||||
|
# intended to run in multiple environments; otherwise, check them in: |
||||||
|
# .python-version |
||||||
|
|
||||||
|
# pipenv |
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. |
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies |
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not |
||||||
|
# install all needed dependencies. |
||||||
|
#Pipfile.lock |
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow |
||||||
|
__pypackages__/ |
||||||
|
|
||||||
|
# Celery stuff |
||||||
|
celerybeat-schedule |
||||||
|
celerybeat.pid |
||||||
|
|
||||||
|
# SageMath parsed files |
||||||
|
*.sage.py |
||||||
|
|
||||||
|
# Environments |
||||||
|
.env |
||||||
|
.venv |
||||||
|
env/ |
||||||
|
venv/ |
||||||
|
ENV/ |
||||||
|
env.bak/ |
||||||
|
venv.bak/ |
||||||
|
|
||||||
|
# Spyder project settings |
||||||
|
.spyderproject |
||||||
|
.spyproject |
||||||
|
|
||||||
|
# Rope project settings |
||||||
|
.ropeproject |
||||||
|
|
||||||
|
# mkdocs documentation |
||||||
|
/site |
||||||
|
|
||||||
|
# mypy |
||||||
|
.mypy_cache/ |
||||||
|
.dmypy.json |
||||||
|
dmypy.json |
||||||
|
|
||||||
|
# Pyre type checker |
||||||
|
.pyre/ |
||||||
|
|
||||||
|
# pytype static type analyzer |
||||||
|
.pytype/ |
||||||
|
|
||||||
|
# Cython debug symbols |
||||||
|
cython_debug/ |
||||||
|
|
||||||
|
app.db |
||||||
|
.vscode/ |
||||||
|
|
@ -0,0 +1,47 @@ |
|||||||
|
from flask import Flask |
||||||
|
from config import Config |
||||||
|
from flask_sqlalchemy import SQLAlchemy |
||||||
|
from flask_migrate import Migrate |
||||||
|
from flask_bootstrap import Bootstrap |
||||||
|
import logging |
||||||
|
from logging.handlers import SMTPHandler, RotatingFileHandler |
||||||
|
import os |
||||||
|
|
||||||
|
app = Flask(__name__) |
||||||
|
app.config.from_object(Config) |
||||||
|
db = SQLAlchemy(app) |
||||||
|
migrate = Migrate(app, db) |
||||||
|
bootstrap = Bootstrap(app) |
||||||
|
|
||||||
|
|
||||||
|
from app import routes, models, errors, database_cleaner |
||||||
|
|
||||||
|
if not app.debug: |
||||||
|
# Mail errors |
||||||
|
if app.config['MAIL_SERVER']: |
||||||
|
auth = None |
||||||
|
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']: |
||||||
|
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']) |
||||||
|
secure = None |
||||||
|
if app.config['MAIL_USE_TLS']: |
||||||
|
secure = () # The secure argument is an empty tuple to use TLS without providing a specific certificate and key. |
||||||
|
mail_handler = SMTPHandler( |
||||||
|
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), |
||||||
|
fromaddr='no-reply@' + app.config['MAIL_SERVER'], |
||||||
|
toaddrs=app.config['ADMINS'], subject='Microblog Failure', |
||||||
|
credentials=auth, secure=secure) |
||||||
|
mail_handler.setLevel(logging.ERROR) |
||||||
|
app.logger.addHandler(mail_handler) |
||||||
|
# Log to File |
||||||
|
if not os.path.exists('logs'): |
||||||
|
os.mkdir('logs') |
||||||
|
file_handler = RotatingFileHandler('logs/linkshortener.log', maxBytes=10240, backupCount=10) |
||||||
|
file_handler.setFormatter(logging.Formatter( |
||||||
|
'%(asctime)s %(levelname)s: %(message)s')) |
||||||
|
file_handler.setLevel(logging.INFO) |
||||||
|
app.logger.addHandler(file_handler) |
||||||
|
|
||||||
|
app.logger.setLevel(logging.INFO) |
||||||
|
app.logger.info('Link Shortener startup') |
||||||
|
|
||||||
|
database_cleaner.start_scheduler() |
@ -0,0 +1,46 @@ |
|||||||
|
import time |
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler |
||||||
|
import atexit |
||||||
|
from datetime import datetime |
||||||
|
|
||||||
|
from app import app, db |
||||||
|
from app.models import Url |
||||||
|
|
||||||
|
def remove_dead_urls(): |
||||||
|
for url in Url.query.all(): |
||||||
|
check_url(url) |
||||||
|
|
||||||
|
def check_url(url): |
||||||
|
if url.death is not None: |
||||||
|
if datetime.now() > url.death : |
||||||
|
app.logger.info(f"Removed hash '{url.hash}'' because its retention time has passed.") |
||||||
|
db.session.delete(url) |
||||||
|
db.session.commit() |
||||||
|
return "Timeout" |
||||||
|
if url.view_counter is not None: |
||||||
|
if url.view_counter <= 0: |
||||||
|
app.logger.info(f"Removed hash '{url.hash}'' because its viewcouner has run out.") |
||||||
|
db.session.delete(url) |
||||||
|
db.session.commit() |
||||||
|
return "Counter" |
||||||
|
return None |
||||||
|
|
||||||
|
scheduler = None |
||||||
|
|
||||||
|
def start_scheduler(): |
||||||
|
global scheduler |
||||||
|
if scheduler is not None: |
||||||
|
app.logger.info("Database_cleaner scheduler was already started, not launching a second instane.") |
||||||
|
return |
||||||
|
scheduler = BackgroundScheduler() |
||||||
|
scheduler.add_job(func=remove_dead_urls, trigger="interval", hours=1) |
||||||
|
scheduler.start() |
||||||
|
app.logger.info("Database_cleaner scheduler was started.") |
||||||
|
|
||||||
|
# Shut down the scheduler when exiting the app |
||||||
|
atexit.register(shutdown_scheduler) |
||||||
|
|
||||||
|
def shutdown_scheduler(): |
||||||
|
scheduler.shutdown() |
||||||
|
app.logger.info("Database_cleaner scheduler was shut down.") |
||||||
|
|
@ -0,0 +1,12 @@ |
|||||||
|
from flask import render_template |
||||||
|
from app import app |
||||||
|
|
||||||
|
@app.errorhandler(404) |
||||||
|
def not_found_error(error): |
||||||
|
return render_template('404.html'), 404 |
||||||
|
|
||||||
|
@app.errorhandler(500) |
||||||
|
def internal_error(error): |
||||||
|
return render_template('500.html'), 500 |
||||||
|
db.session.rollback() |
||||||
|
|
@ -0,0 +1,9 @@ |
|||||||
|
from flask_wtf import FlaskForm |
||||||
|
from wtforms import StringField, IntegerField, SelectField, SubmitField |
||||||
|
from wtforms.validators import DataRequired, NumberRange, Required |
||||||
|
|
||||||
|
class UrlForm(FlaskForm): |
||||||
|
url = StringField('Url', validators=[DataRequired()]) |
||||||
|
retention = IntegerField('Retention', default=5, validators = [NumberRange(min=0, max=100)]) |
||||||
|
retention_type = SelectField('Retention', choices = [ 'Minute', 'Hour', 'Day', 'Time'], default=2, validators = [Required()]) |
||||||
|
submit = SubmitField('Shorten Url') |
@ -0,0 +1,16 @@ |
|||||||
|
from datetime import datetime |
||||||
|
from app import app, db |
||||||
|
|
||||||
|
class Url(db.Model): |
||||||
|
id = db.Column(db.Integer, primary_key=True) |
||||||
|
url = db.Column(db.String(2000), nullable=False) |
||||||
|
hash = db.Column(db.String(20), index=True, unique=True, nullable=False) |
||||||
|
birth = db.Column(db.DateTime, default=datetime.utcnow) |
||||||
|
death = db.Column(db.DateTime) |
||||||
|
view_counter = db.Column(db.Integer) |
||||||
|
|
||||||
|
def __init__(self, **kwargs): |
||||||
|
super(Url, self).__init__(**kwargs) |
||||||
|
|
||||||
|
if not self.url.lower().startswith(('http://', 'https://')): |
||||||
|
self.url = f'https://{self.url}' |
@ -0,0 +1,76 @@ |
|||||||
|
from datetime import datetime, timedelta |
||||||
|
from secrets import token_urlsafe |
||||||
|
|
||||||
|
from flask import render_template, flash, redirect, url_for, abort, request, Markup |
||||||
|
from app import app, db |
||||||
|
from app.forms import UrlForm |
||||||
|
from app.models import Url |
||||||
|
from app.database_cleaner import check_url |
||||||
|
|
||||||
|
domain = 'http://127.0.0.1:5000' |
||||||
|
|
||||||
|
@app.before_request |
||||||
|
def before_request(): |
||||||
|
pass |
||||||
|
|
||||||
|
@app.route('/index', methods=['GET', 'POST']) |
||||||
|
def index(): |
||||||
|
form = UrlForm() |
||||||
|
if form.validate_on_submit(): |
||||||
|
hash = getHash() |
||||||
|
if hash is None: |
||||||
|
return redirect(url_for('index')) |
||||||
|
|
||||||
|
death = calcDeath(form.retention.data, form.retention_type.data) |
||||||
|
view_counter = None if form.retention_type.data != "Time" else form.retention.data |
||||||
|
if death is None and view_counter is None: |
||||||
|
app.logger.warning("Neither death nor view_counter was recieved for url") |
||||||
|
flash('Please specify a retention time') |
||||||
|
return redirect(url_for('index')) |
||||||
|
|
||||||
|
url = Url(url = form.url.data.strip(), hash=hash, death=death, view_counter=view_counter) |
||||||
|
db.session.add(url) |
||||||
|
db.session.commit() |
||||||
|
death = None if url.death is None else url.death.strftime('%Y-%m-%d %H:%M:%S') |
||||||
|
app.logger.info(f"{request.environ['REMOTE_ADDR']} created hash '{url.hash}' for '{url.url}'. Death: {death}, View Counter: {url.view_counter}") |
||||||
|
link = url_for("resolve_hash", hash=hash, _external = True) |
||||||
|
flash(Markup(f'Your url is shortend to <a href="{link}">{link}</a>')) |
||||||
|
return redirect(url_for('index')) |
||||||
|
return render_template("index.html", form=form) |
||||||
|
|
||||||
|
def getHash(): |
||||||
|
for i in range(200): |
||||||
|
hash = token_urlsafe(3) |
||||||
|
url = Url.query.filter_by(hash=hash).first() |
||||||
|
if (url is None and url != 'index'): |
||||||
|
return hash |
||||||
|
|
||||||
|
flash('Failed generating a unique hash. Please try again.') |
||||||
|
return None |
||||||
|
|
||||||
|
def calcDeath(retention, retention_type): |
||||||
|
if retention_type == 'Minute': |
||||||
|
return datetime.now() + timedelta(minutes=retention) |
||||||
|
if retention_type == 'Hour': |
||||||
|
return datetime.now() + timedelta(hours=retention) |
||||||
|
if retention_type == 'Day': |
||||||
|
return datetime.now() + timedelta(days=retention) |
||||||
|
if retention_type == 'Time': |
||||||
|
return None |
||||||
|
app.logger.error(f"Retention_type out of range: '{retention_type}'") |
||||||
|
|
||||||
|
@app.route('/', defaults={'hash' : 'index'}) |
||||||
|
@app.route('/<hash>') |
||||||
|
def resolve_hash(hash): |
||||||
|
if hash == 'index': |
||||||
|
return redirect(url_for('index')) |
||||||
|
|
||||||
|
url = Url.query.filter_by(hash=hash).first_or_404() |
||||||
|
if check_url(url) is None: |
||||||
|
if url.view_counter is not None: |
||||||
|
url.view_counter -= 1 |
||||||
|
db.session.commit() |
||||||
|
|
||||||
|
countermessage = "" if url.view_counter is None else f". View counter was lowered to {url.view_counter}" |
||||||
|
app.logger.info(f"{request.environ['REMOTE_ADDR']} requested hash '{url.hash}' which resolved to '{url.url}'{countermessage}") |
||||||
|
return redirect(url.url, 301) |
@ -0,0 +1,7 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<h1>File Not Found</h1> |
||||||
|
<p><a href="{{ url_for('index') }}">Back</a></p> |
||||||
|
{% endblock %} |
||||||
|
|
@ -0,0 +1,8 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
<h1>An unexpected error has occurred</h1> |
||||||
|
<p>The administrator has been notified. Sorry for the inconvenience!</p> |
||||||
|
<p><a href="{{ url_for('index') }}">Back</a></p> |
||||||
|
{% endblock %} |
||||||
|
|
@ -0,0 +1,30 @@ |
|||||||
|
{% extends 'bootstrap/base.html' %} |
||||||
|
|
||||||
|
{% block title %} |
||||||
|
Link Shortener |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="container"> |
||||||
|
<br> |
||||||
|
<div class="col-md-3"></div> |
||||||
|
<div class="col-md-5"> |
||||||
|
{% with messages = get_flashed_messages() %} |
||||||
|
{% if messages %} |
||||||
|
{% for message in messages %} |
||||||
|
<div class="alert alert-info" role="alert">{{ message }}</div> |
||||||
|
{% endfor %} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} |
||||||
|
|
||||||
|
{# application content needs to be provided in the app_content block #} |
||||||
|
{% block app_content %}{% endblock %} |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block scripts %} |
||||||
|
{{ super() }} |
||||||
|
{% endblock %} |
||||||
|
|
@ -0,0 +1,13 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
{% import 'bootstrap/wtf.html' as wtf %} |
||||||
|
|
||||||
|
{% block app_content %} |
||||||
|
|
||||||
|
{% if form %} |
||||||
|
{{ wtf.quick_form(form, button_map={'submit': 'primary'}) }} |
||||||
|
<br> |
||||||
|
|
||||||
|
{% endif %} |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
@ -0,0 +1,16 @@ |
|||||||
|
import os |
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__)) |
||||||
|
|
||||||
|
class Config(object): |
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' |
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ |
||||||
|
'sqlite:///' + os.path.join(basedir, 'app.db') |
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False |
||||||
|
|
||||||
|
MAIL_SERVER = os.environ.get('MAIL_SERVER') |
||||||
|
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) |
||||||
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None |
||||||
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') |
||||||
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') |
||||||
|
ADMINS = ['your-email@example.com'] |
||||||
|
|
@ -0,0 +1,6 @@ |
|||||||
|
from app import app, db |
||||||
|
from app.models import Url |
||||||
|
|
||||||
|
@app.shell_context_processor |
||||||
|
def make_shell_context(): |
||||||
|
return {'db': db, 'Url': Url} |
@ -0,0 +1,45 @@ |
|||||||
|
# A generic, single database configuration. |
||||||
|
|
||||||
|
[alembic] |
||||||
|
# template used to generate migration files |
||||||
|
# file_template = %%(rev)s_%%(slug)s |
||||||
|
|
||||||
|
# set to 'true' to run the environment during |
||||||
|
# the 'revision' command, regardless of autogenerate |
||||||
|
# revision_environment = false |
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration |
||||||
|
[loggers] |
||||||
|
keys = root,sqlalchemy,alembic |
||||||
|
|
||||||
|
[handlers] |
||||||
|
keys = console |
||||||
|
|
||||||
|
[formatters] |
||||||
|
keys = generic |
||||||
|
|
||||||
|
[logger_root] |
||||||
|
level = WARN |
||||||
|
handlers = console |
||||||
|
qualname = |
||||||
|
|
||||||
|
[logger_sqlalchemy] |
||||||
|
level = WARN |
||||||
|
handlers = |
||||||
|
qualname = sqlalchemy.engine |
||||||
|
|
||||||
|
[logger_alembic] |
||||||
|
level = INFO |
||||||
|
handlers = |
||||||
|
qualname = alembic |
||||||
|
|
||||||
|
[handler_console] |
||||||
|
class = StreamHandler |
||||||
|
args = (sys.stderr,) |
||||||
|
level = NOTSET |
||||||
|
formatter = generic |
||||||
|
|
||||||
|
[formatter_generic] |
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s |
||||||
|
datefmt = %H:%M:%S |
@ -0,0 +1,96 @@ |
|||||||
|
from __future__ import with_statement |
||||||
|
|
||||||
|
import logging |
||||||
|
from logging.config import fileConfig |
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config |
||||||
|
from sqlalchemy import pool |
||||||
|
|
||||||
|
from alembic import context |
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides |
||||||
|
# access to the values within the .ini file in use. |
||||||
|
config = context.config |
||||||
|
|
||||||
|
# Interpret the config file for Python logging. |
||||||
|
# This line sets up loggers basically. |
||||||
|
fileConfig(config.config_file_name) |
||||||
|
logger = logging.getLogger('alembic.env') |
||||||
|
|
||||||
|
# add your model's MetaData object here |
||||||
|
# for 'autogenerate' support |
||||||
|
# from myapp import mymodel |
||||||
|
# target_metadata = mymodel.Base.metadata |
||||||
|
from flask import current_app |
||||||
|
config.set_main_option( |
||||||
|
'sqlalchemy.url', |
||||||
|
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) |
||||||
|
target_metadata = current_app.extensions['migrate'].db.metadata |
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py, |
||||||
|
# can be acquired: |
||||||
|
# my_important_option = config.get_main_option("my_important_option") |
||||||
|
# ... etc. |
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline(): |
||||||
|
"""Run migrations in 'offline' mode. |
||||||
|
|
||||||
|
This configures the context with just a URL |
||||||
|
and not an Engine, though an Engine is acceptable |
||||||
|
here as well. By skipping the Engine creation |
||||||
|
we don't even need a DBAPI to be available. |
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the |
||||||
|
script output. |
||||||
|
|
||||||
|
""" |
||||||
|
url = config.get_main_option("sqlalchemy.url") |
||||||
|
context.configure( |
||||||
|
url=url, target_metadata=target_metadata, literal_binds=True |
||||||
|
) |
||||||
|
|
||||||
|
with context.begin_transaction(): |
||||||
|
context.run_migrations() |
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online(): |
||||||
|
"""Run migrations in 'online' mode. |
||||||
|
|
||||||
|
In this scenario we need to create an Engine |
||||||
|
and associate a connection with the context. |
||||||
|
|
||||||
|
""" |
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated |
||||||
|
# when there are no changes to the schema |
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html |
||||||
|
def process_revision_directives(context, revision, directives): |
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False): |
||||||
|
script = directives[0] |
||||||
|
if script.upgrade_ops.is_empty(): |
||||||
|
directives[:] = [] |
||||||
|
logger.info('No changes in schema detected.') |
||||||
|
|
||||||
|
connectable = engine_from_config( |
||||||
|
config.get_section(config.config_ini_section), |
||||||
|
prefix='sqlalchemy.', |
||||||
|
poolclass=pool.NullPool, |
||||||
|
) |
||||||
|
|
||||||
|
with connectable.connect() as connection: |
||||||
|
context.configure( |
||||||
|
connection=connection, |
||||||
|
target_metadata=target_metadata, |
||||||
|
process_revision_directives=process_revision_directives, |
||||||
|
**current_app.extensions['migrate'].configure_args |
||||||
|
) |
||||||
|
|
||||||
|
with context.begin_transaction(): |
||||||
|
context.run_migrations() |
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode(): |
||||||
|
run_migrations_offline() |
||||||
|
else: |
||||||
|
run_migrations_online() |
@ -0,0 +1,24 @@ |
|||||||
|
"""${message} |
||||||
|
|
||||||
|
Revision ID: ${up_revision} |
||||||
|
Revises: ${down_revision | comma,n} |
||||||
|
Create Date: ${create_date} |
||||||
|
|
||||||
|
""" |
||||||
|
from alembic import op |
||||||
|
import sqlalchemy as sa |
||||||
|
${imports if imports else ""} |
||||||
|
|
||||||
|
# revision identifiers, used by Alembic. |
||||||
|
revision = ${repr(up_revision)} |
||||||
|
down_revision = ${repr(down_revision)} |
||||||
|
branch_labels = ${repr(branch_labels)} |
||||||
|
depends_on = ${repr(depends_on)} |
||||||
|
|
||||||
|
|
||||||
|
def upgrade(): |
||||||
|
${upgrades if upgrades else "pass"} |
||||||
|
|
||||||
|
|
||||||
|
def downgrade(): |
||||||
|
${downgrades if downgrades else "pass"} |
@ -0,0 +1,38 @@ |
|||||||
|
"""initial migration |
||||||
|
|
||||||
|
Revision ID: 3c65fc2aac0a |
||||||
|
Revises: |
||||||
|
Create Date: 2020-12-15 23:06:09.507454 |
||||||
|
|
||||||
|
""" |
||||||
|
from alembic import op |
||||||
|
import sqlalchemy as sa |
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic. |
||||||
|
revision = '3c65fc2aac0a' |
||||||
|
down_revision = None |
||||||
|
branch_labels = None |
||||||
|
depends_on = None |
||||||
|
|
||||||
|
|
||||||
|
def upgrade(): |
||||||
|
# ### commands auto generated by Alembic - please adjust! ### |
||||||
|
op.create_table('url', |
||||||
|
sa.Column('id', sa.Integer(), nullable=False), |
||||||
|
sa.Column('url', sa.String(length=2000), nullable=False), |
||||||
|
sa.Column('hash', sa.String(length=20), nullable=False), |
||||||
|
sa.Column('birth', sa.DateTime(), nullable=True), |
||||||
|
sa.Column('death', sa.DateTime(), nullable=True), |
||||||
|
sa.Column('view_counter', sa.Integer(), nullable=True), |
||||||
|
sa.PrimaryKeyConstraint('id') |
||||||
|
) |
||||||
|
op.create_index(op.f('ix_url_hash'), 'url', ['hash'], unique=True) |
||||||
|
# ### end Alembic commands ### |
||||||
|
|
||||||
|
|
||||||
|
def downgrade(): |
||||||
|
# ### commands auto generated by Alembic - please adjust! ### |
||||||
|
op.drop_index(op.f('ix_url_hash'), table_name='url') |
||||||
|
op.drop_table('url') |
||||||
|
# ### end Alembic commands ### |
@ -0,0 +1,32 @@ |
|||||||
|
alembic==1.4.3 |
||||||
|
APScheduler==3.6.3 |
||||||
|
astroid==2.4.2 |
||||||
|
click==7.1.2 |
||||||
|
dominate==2.6.0 |
||||||
|
Flask==1.1.2 |
||||||
|
Flask-Bootstrap==3.3.7.1 |
||||||
|
Flask-Migrate==2.5.3 |
||||||
|
Flask-SQLAlchemy==2.4.4 |
||||||
|
Flask-WTF==0.14.3 |
||||||
|
isort==5.6.4 |
||||||
|
itsdangerous==1.1.0 |
||||||
|
Jinja2==2.11.2 |
||||||
|
lazy-object-proxy==1.4.3 |
||||||
|
Mako==1.1.3 |
||||||
|
MarkupSafe==1.1.1 |
||||||
|
mccabe==0.6.1 |
||||||
|
pylint==2.6.0 |
||||||
|
pylint-flask-sqlalchemy==0.2.0 |
||||||
|
python-dateutil==2.8.1 |
||||||
|
python-dotenv==0.15.0 |
||||||
|
python-editor==1.0.4 |
||||||
|
pytz==2020.4 |
||||||
|
six==1.15.0 |
||||||
|
SQLAlchemy==1.3.20 |
||||||
|
toml==0.10.2 |
||||||
|
typed-ast==1.4.1 |
||||||
|
tzlocal==2.1 |
||||||
|
visitor==0.1.3 |
||||||
|
Werkzeug==1.0.1 |
||||||
|
wrapt==1.12.1 |
||||||
|
WTForms==2.3.3 |
Loading…
Reference in new issue