Merge branch 'master' of ssh://git.autonomic.zone:2222/autonomic-cooperative/magic-app
This commit is contained in:
commit
d6bf70e5bc
20
.drone.yml
Normal file
20
.drone.yml
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
matrix:
|
||||
include:
|
||||
- IMAGE: 3.8-buster
|
||||
TOXENV: py38
|
||||
- IMAGE: 3.8-buster
|
||||
TOXENV: lint
|
||||
- IMAGE: 3.8-buster
|
||||
TOXENV: sort
|
||||
- IMAGE: 3.8-buster
|
||||
TOXENV: format
|
||||
- IMAGE: 3.8-buster
|
||||
TOXENV: type
|
||||
|
||||
pipeline:
|
||||
build:
|
||||
image: python:${IMAGE}
|
||||
commands:
|
||||
- pip install tox
|
||||
- tox -e ${TOXENV}
|
8
.envrc.sample
Normal file
8
.envrc.sample
Normal file
@ -0,0 +1,8 @@
|
||||
export CELERY_BROKER_URL=redis://localhost:6379
|
||||
export CELERY_RESULT_BACKEND=redis://localhost:6379
|
||||
export FLASK_APP=scripts.wsgi:app
|
||||
export FLASK_ENV=development
|
||||
export REDIS_HOST=localhost
|
||||
export REDIS_PORT=6379
|
||||
export REDIS_SESSION_DB=0
|
||||
export SECRET_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ build/
|
||||
data/
|
||||
dist/
|
||||
pip-wheel-metadata/
|
||||
celerybeat-schedule
|
||||
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
FROM python:3.8-alpine
|
||||
|
||||
ENV APP_ROOT /magic_app/
|
||||
|
||||
WORKDIR ${APP_ROOT}
|
||||
COPY . ${APP_ROOT}
|
||||
|
||||
RUN apk add --update \
|
||||
build-base \
|
||||
curl \
|
||||
git \
|
||||
libffi-dev \
|
||||
libsasl \
|
||||
openssl-dev \
|
||||
python3-dev
|
||||
|
||||
RUN pip install "poetry>=1.0.9,<2.0"
|
||||
|
||||
RUN poetry install \
|
||||
--no-dev \
|
||||
--no-interaction
|
141
LICENSE
141
LICENSE
@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
61
README.md
61
README.md
@ -1,42 +1,29 @@
|
||||
# Magic App
|
||||
# magic-app
|
||||
|
||||
[![Build Status](https://drone.autonomic.zone/api/badges/autonomic-cooperative/magic-app/status.svg)](https://drone.autonomic.zone/autonomic-cooperative/magic-app)
|
||||
|
||||
A swarm of dreams.
|
||||
|
||||
## first.py
|
||||
# Development
|
||||
|
||||
Ideas contained within...
|
||||
|
||||
- List apps from an `app.json` (points to https://git.autonomic.zone/compose-stacks)
|
||||
- Clone selected app template and parse configuration for inputs (env vars and secrets)
|
||||
- Generate a form so those values can be filled out and allow it to be saved
|
||||
- Save the form inputs to a `db.json` (as a start)
|
||||
- Deploy an applicaiton to a local swarm (assumes access to local docker socket)
|
||||
- Create an "edit app" page where the `db.json` is re-called and can be updated
|
||||
- Make sure re-deploy works (taking care of updating secret and app versions)
|
||||
|
||||
## second.py
|
||||
|
||||
More ideas...
|
||||
|
||||
- Don't try to be smart with the auto-generation, hard-code everything. We
|
||||
maintain the app template (`compose.yml`) and this code anyway, so we just
|
||||
need to be aware of each other and keep in sync. This would optimise for
|
||||
trust and collaboration and not "smart" code.
|
||||
- Hard-code the secrets/configs required to make the code for generating
|
||||
versions simpler as well.
|
||||
|
||||
## MVP screens
|
||||
|
||||
- **App selection screen**: a list of apps, you choose which one you want
|
||||
- **Install app screen**:a form for installing the application
|
||||
- **Edit app screen**: re-load the configuration for an app, ability to update it
|
||||
- **Delete app screen**: remove the app
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ python first.py / second.py / third.py
|
||||
```
|
||||
$ git clone https://git.autonomic.zone/autonomic-cooperative/magic-app
|
||||
$ cd magic-app
|
||||
```
|
||||
|
||||
### Docker Way
|
||||
|
||||
```
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
### Manual Way
|
||||
|
||||
```
|
||||
$ python3 -m pip install --user pipx
|
||||
$ pipx install poetry
|
||||
$ poetry install
|
||||
$ cp .envrc.sample .envrc
|
||||
$ direnv allow
|
||||
$ poetry run flask run
|
||||
```
|
||||
|
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
version: "3.8"
|
||||
|
||||
x-env: &env
|
||||
environment:
|
||||
CELERY_BROKER_URL: "redis://redis:6379"
|
||||
CELERY_RESULT_BACKEND: "redis://redis:6379"
|
||||
FLASK_APP: "scripts.wsgi:app"
|
||||
FLASK_ENV: "development"
|
||||
REDIS_HOST: "redis"
|
||||
REDIS_PORT: "6379"
|
||||
REDIS_SESSION_DB: "0"
|
||||
SECRET_KEY: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
services:
|
||||
flask:
|
||||
build: .
|
||||
<<: *env
|
||||
ports:
|
||||
- "5000:5000"
|
||||
command: |
|
||||
poetry run
|
||||
flask run --host 0.0.0.0
|
||||
volumes:
|
||||
- "./:/magic_app/"
|
||||
|
||||
celery:
|
||||
build: .
|
||||
<<: *env
|
||||
command: |
|
||||
poetry run
|
||||
celery worker -A scripts.celworker.celery
|
||||
volumes:
|
||||
- "./:/magic_app/"
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
1
magic_app/__init__.py
Normal file
1
magic_app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The main application package."""
|
74
magic_app/app.py
Normal file
74
magic_app/app.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""The main application factory."""
|
||||
from logging import DEBUG, ERROR, getLogger
|
||||
|
||||
from celery import Celery
|
||||
from flask import Flask
|
||||
from redis import Redis
|
||||
|
||||
from magic_app.config import Base, Production
|
||||
|
||||
celery = Celery(
|
||||
__name__, backend=Base.CELERY_RESULT_BACKEND, broker=Base.CELERY_BROKER_URL
|
||||
)
|
||||
|
||||
|
||||
def create_app(config=Production):
|
||||
"""Main application factory."""
|
||||
app = Flask(__name__.split(".")[0])
|
||||
app.config.from_object(config)
|
||||
|
||||
configure_celery(app)
|
||||
configure_redis(app)
|
||||
configure_views(app)
|
||||
configure_logging(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def configure_redis(app):
|
||||
"""Configure Redis connection."""
|
||||
from magic_app.session import RedisSessionDB
|
||||
|
||||
host = Base.REDIS_HOST
|
||||
port = Base.REDIS_PORT
|
||||
db_num = Base.REDIS_SESSION_DB
|
||||
|
||||
if app.testing:
|
||||
from fakeredis import FakeRedis
|
||||
|
||||
connection = FakeRedis()
|
||||
else:
|
||||
connection = Redis(host=host, port=port, db=db_num)
|
||||
|
||||
app.config["SESSION"] = RedisSessionDB(connection)
|
||||
|
||||
|
||||
def configure_celery(app):
|
||||
"""Configure celery."""
|
||||
celery.conf.update(app.config)
|
||||
|
||||
class ContextTask(celery.Task):
|
||||
def __call__(self, *args, **kwargs):
|
||||
with app.app_context():
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
celery.Task = ContextTask
|
||||
|
||||
|
||||
def configure_views(app):
|
||||
"""Configure API resource views."""
|
||||
from magic_app.views import apps
|
||||
|
||||
app.register_blueprint(apps)
|
||||
|
||||
|
||||
def configure_logging(app):
|
||||
"""Configure application logging."""
|
||||
app_logger = getLogger(__name__)
|
||||
|
||||
if not app.debug:
|
||||
app_logger.setLevel(ERROR)
|
||||
else:
|
||||
app_logger.setLevel(DEBUG)
|
||||
|
||||
app.logging = app_logger
|
52
magic_app/config.py
Normal file
52
magic_app/config.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""The Application settings."""
|
||||
from os import environ, pardir
|
||||
from os.path import abspath, dirname, join
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Base:
|
||||
"""The base configuration."""
|
||||
|
||||
DEBUG = False
|
||||
JSON_AS_ASCII = False
|
||||
SECRET_KEY = environ["SECRET_KEY"]
|
||||
|
||||
APP_DIR = abspath(dirname(__file__))
|
||||
PROJECT_ROOT = abspath(join(APP_DIR, pardir))
|
||||
|
||||
REDIS_HOST = environ["REDIS_HOST"]
|
||||
REDIS_PORT = environ["REDIS_PORT"]
|
||||
REDIS_SESSION_DB = int(environ["REDIS_SESSION_DB"])
|
||||
|
||||
CELERY_BROKER_URL = environ["CELERY_BROKER_URL"]
|
||||
CELERY_RESULT_BACKEND = environ["CELERY_RESULT_BACKEND"]
|
||||
|
||||
|
||||
class Development(Base):
|
||||
"""The Development configuration."""
|
||||
|
||||
ENV = "development"
|
||||
CELERY_ALWAYS_EAGER = True
|
||||
DEBUG = True
|
||||
DATA_DIR = Path(Base.PROJECT_ROOT) / "data"
|
||||
|
||||
|
||||
class Testing(Base):
|
||||
"""The testing configuration."""
|
||||
|
||||
ENV = "testing"
|
||||
TESTING = True
|
||||
|
||||
|
||||
class Production(Base):
|
||||
"""The production configuration."""
|
||||
|
||||
ENV = "production"
|
||||
DATA_DIR = "/data"
|
||||
|
||||
|
||||
CONFIG = {
|
||||
"development": Development,
|
||||
"testing": Testing,
|
||||
"production": Production,
|
||||
}
|
50
magic_app/forms.py
Normal file
50
magic_app/forms.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Forms for app installation."""
|
||||
from os import environ
|
||||
|
||||
from flask import request
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
|
||||
|
||||
class GiteaInstallForm(FlaskForm):
|
||||
"""Gitea installation form."""
|
||||
|
||||
# "simple"
|
||||
app_name = StringField("Application name", default="Git with a cup of tea")
|
||||
domain = StringField("Domain name", validators=[DataRequired()],)
|
||||
stack_name = StringField("Stack name", default="magic-app-gitea")
|
||||
|
||||
# "advanced"
|
||||
db_host = StringField("Database host", default="mariadb:3306")
|
||||
db_name = StringField("Database name", default="gitea")
|
||||
db_type = StringField("Database type", default="mysql")
|
||||
db_user = StringField("Database user", default="mysql")
|
||||
ssh_host_port = StringField("SSH host port", default="2225")
|
||||
|
||||
# secrets
|
||||
db_passwd = PasswordField(
|
||||
"Database password", validators=[DataRequired(), Length(min=32)],
|
||||
)
|
||||
db_root_passwd = PasswordField(
|
||||
"Database root password", validators=[DataRequired(), Length(min=32)],
|
||||
)
|
||||
internal_token = PasswordField(
|
||||
"Internal secret token", validators=[DataRequired(), Length(min=105)],
|
||||
)
|
||||
jwt_secret = PasswordField(
|
||||
"JWT secret", validators=[DataRequired(), Length(min=43)],
|
||||
)
|
||||
secret_key = PasswordField(
|
||||
"Secret key", validators=[DataRequired(), Length(min=64)],
|
||||
)
|
||||
|
||||
|
||||
def form_to_env(app_name, request_form):
|
||||
"""Load form data into a environment."""
|
||||
env = environ.copy()
|
||||
|
||||
for key in request_form.keys():
|
||||
env[key.upper()] = request.form[key]
|
||||
|
||||
return env
|
32
magic_app/session.py
Normal file
32
magic_app/session.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Session management interface module."""
|
||||
from json import dumps, loads
|
||||
|
||||
|
||||
class RedisSessionDB(dict):
|
||||
"""Dict interface for Redis session management."""
|
||||
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def keys(self):
|
||||
return self.connection.keys()
|
||||
|
||||
def flushall(self):
|
||||
self.connection.flushall()
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = self.connection.get(key)
|
||||
if value is None:
|
||||
raise KeyError(key)
|
||||
utf8ified = str(value, "utf-8")
|
||||
return loads(utf8ified)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
value = dumps(value)
|
||||
self.connection.set(key, value)
|
||||
|
||||
def __contains__(self, item):
|
||||
return self.connection.exists(item)
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.connection.delete(key)
|
14
magic_app/tasks.py
Normal file
14
magic_app/tasks.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""Celery tasks module."""
|
||||
from magic_app.app import celery
|
||||
from magic_app.templates import clone_app_template, create_data_dir
|
||||
|
||||
|
||||
@celery.task
|
||||
def install_app(app_name: str, form_data) -> None:
|
||||
"""Install an application."""
|
||||
create_data_dir()
|
||||
clone_app_template(app_name)
|
||||
|
||||
# Note(decentral1se): this is where I left off...
|
||||
# from magic_app.forms import form_to_env
|
||||
# env = form_to_env(app_name, form_data) # noqa
|
29
magic_app/templates.py
Normal file
29
magic_app/templates.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Compose template handling."""
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from shlex import split
|
||||
from shutil import rmtree
|
||||
from subprocess import run
|
||||
|
||||
from flask import current_app
|
||||
|
||||
APP_TEMPLATES = {
|
||||
"gitea": "https://git.autonomic.zone/compose-stacks/gitea",
|
||||
}
|
||||
|
||||
|
||||
def create_data_dir() -> None:
|
||||
"""Create data directory for compose templates."""
|
||||
try:
|
||||
mkdir(current_app.config["DATA_DIR"])
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
|
||||
def clone_app_template(app_name: str) -> None:
|
||||
"""Clone an application template repository."""
|
||||
clone_path = current_app.config["DATA_DIR"] / app_name
|
||||
clone_url = APP_TEMPLATES[app_name]
|
||||
if exists(clone_path):
|
||||
rmtree(clone_path)
|
||||
run(split(f"git clone {clone_url} {clone_path}"))
|
19
magic_app/templates/app_install.html
Normal file
19
magic_app/templates/app_install.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% from "macros.html" import with_errors %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<title>Install {{ app_name | capitalize }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Install {{ app_name | capitalize }}</p>
|
||||
<form method="POST" action="{{ url_for("apps.install", app_name=app_name) }}">
|
||||
{% for field in form %}
|
||||
{{ field.label() }} {{ with_errors(field, style='font-weight: bold') }}
|
||||
{% endfor %}
|
||||
<input type="submit" value="Install" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
17
magic_app/templates/app_list.html
Normal file
17
magic_app/templates/app_list.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<title>Application Listing</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
{% for app in apps %}
|
||||
<li>
|
||||
<a href="{{ url_for('apps.install', app_name=app) }}">{{ app }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
27
magic_app/templates/app_status.html
Normal file
27
magic_app/templates/app_status.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ app_name | capitalize }} Status</title>
|
||||
<style>
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
<th>App</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ app_name }}</th>
|
||||
<th>{{ status }}</th>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
34
magic_app/views.py
Normal file
34
magic_app/views.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""View routing."""
|
||||
from flask import Blueprint, redirect, render_template, url_for
|
||||
|
||||
from magic_app.forms import GiteaInstallForm
|
||||
|
||||
apps = Blueprint("apps", __name__)
|
||||
|
||||
|
||||
@apps.route("/")
|
||||
def listing():
|
||||
return render_template("app_list.html", apps=["gitea"])
|
||||
|
||||
|
||||
@apps.route("/install/<app_name>", methods=("GET", "POST"))
|
||||
def install(app_name):
|
||||
"""Install an application."""
|
||||
from magic_app.tasks import install_app
|
||||
|
||||
if app_name == "gitea":
|
||||
form = GiteaInstallForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
install_app.apply_async(args=[app_name, form.data])
|
||||
return redirect(url_for("apps.status", app_name=app_name))
|
||||
|
||||
return render_template("app_install.html", app_name=app_name, form=form)
|
||||
|
||||
|
||||
@apps.route("/status/<app_name>")
|
||||
def status(app_name):
|
||||
"""Show status of applications."""
|
||||
return render_template(
|
||||
"app_status.html", status="UNKNOWN", app_name=app_name
|
||||
)
|
698
poetry.lock
generated
Normal file
698
poetry.lock
generated
Normal file
@ -0,0 +1,698 @@
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Low-level AMQP client for Python (fork of amqplib)."
|
||||
name = "amqp"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.6.0"
|
||||
|
||||
[package.dependencies]
|
||||
vine = ">=1.1.3,<5.0.0a1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
name = "appdirs"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.4.4"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Classes Without Boilerplate"
|
||||
name = "attrs"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "19.3.0"
|
||||
|
||||
[package.extras]
|
||||
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
|
||||
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
|
||||
docs = ["sphinx", "zope.interface"]
|
||||
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python multiprocessing fork with improvements and bugfixes"
|
||||
name = "billiard"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.6.3.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "The uncompromising code formatter."
|
||||
name = "black"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
version = "19.10b0"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = "*"
|
||||
attrs = ">=18.1.0"
|
||||
click = ">=6.5"
|
||||
pathspec = ">=0.6,<1"
|
||||
regex = "*"
|
||||
toml = ">=0.9.4"
|
||||
typed-ast = ">=1.4.0"
|
||||
|
||||
[package.extras]
|
||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Distributed Task Queue."
|
||||
name = "celery"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "4.4.6"
|
||||
|
||||
[package.dependencies]
|
||||
billiard = ">=3.6.3.0,<4.0"
|
||||
future = ">=0.18.0"
|
||||
kombu = ">=4.6.10,<4.7"
|
||||
pytz = ">0.0-dev"
|
||||
vine = "1.3.0"
|
||||
|
||||
[package.extras]
|
||||
arangodb = ["pyArango (>=1.3.2)"]
|
||||
auth = ["cryptography"]
|
||||
azureblockblob = ["azure-storage (0.36.0)", "azure-common (1.1.5)", "azure-storage-common (1.1.0)"]
|
||||
brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
|
||||
cassandra = ["cassandra-driver (<3.21.0)"]
|
||||
consul = ["python-consul"]
|
||||
cosmosdbsql = ["pydocumentdb (2.3.2)"]
|
||||
couchbase = ["couchbase-cffi (<3.0.0)", "couchbase (<3.0.0)"]
|
||||
couchdb = ["pycouchdb"]
|
||||
django = ["Django (>=1.11)"]
|
||||
dynamodb = ["boto3 (>=1.9.178)"]
|
||||
elasticsearch = ["elasticsearch"]
|
||||
eventlet = ["eventlet (>=0.24.1)"]
|
||||
gevent = ["gevent"]
|
||||
librabbitmq = ["librabbitmq (>=1.5.0)"]
|
||||
lzma = ["backports.lzma"]
|
||||
memcache = ["pylibmc"]
|
||||
mongodb = ["pymongo (>=3.3.0)"]
|
||||
msgpack = ["msgpack"]
|
||||
pymemcache = ["python-memcached"]
|
||||
pyro = ["pyro4"]
|
||||
redis = ["redis (>=3.2.0)"]
|
||||
riak = ["riak (>=2.0)"]
|
||||
s3 = ["boto3 (>=1.9.125)"]
|
||||
slmq = ["softlayer-messaging (>=1.0.3)"]
|
||||
solar = ["ephem"]
|
||||
sqlalchemy = ["sqlalchemy"]
|
||||
sqs = ["boto3 (>=1.9.125)", "pycurl (7.43.0.5)"]
|
||||
tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
|
||||
yaml = ["PyYAML (>=3.10)"]
|
||||
zookeeper = ["kazoo (>=1.3.1)"]
|
||||
zstd = ["zstandard"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Composable command line interface toolkit"
|
||||
name = "click"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "7.1.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Fake implementation of redis API for testing purposes."
|
||||
name = "fakeredis"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.4.1"
|
||||
|
||||
[package.dependencies]
|
||||
redis = "<3.6.0"
|
||||
six = ">=1.12"
|
||||
sortedcontainers = "*"
|
||||
|
||||
[package.extras]
|
||||
aioredis = ["aioredis"]
|
||||
lua = ["lupa"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
name = "flake8"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
version = "3.8.3"
|
||||
|
||||
[package.dependencies]
|
||||
mccabe = ">=0.6.0,<0.7.0"
|
||||
pycodestyle = ">=2.6.0a1,<2.7.0"
|
||||
pyflakes = ">=2.2.0,<2.3.0"
|
||||
|
||||
[package.dependencies.importlib-metadata]
|
||||
python = "<3.8"
|
||||
version = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A simple framework for building complex web applications."
|
||||
name = "flask"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "1.1.2"
|
||||
|
||||
[package.dependencies]
|
||||
Jinja2 = ">=2.10.1"
|
||||
Werkzeug = ">=0.15"
|
||||
click = ">=5.1"
|
||||
itsdangerous = ">=0.24"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
|
||||
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Simple integration of Flask and WTForms."
|
||||
name = "flask-wtf"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.14.3"
|
||||
|
||||
[package.dependencies]
|
||||
Flask = "*"
|
||||
WTForms = "*"
|
||||
itsdangerous = "*"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Clean single-source support for Python 3 and 2"
|
||||
name = "future"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "0.18.2"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "WSGI HTTP Server for UNIX"
|
||||
name = "gunicorn"
|
||||
optional = false
|
||||
python-versions = ">=3.4"
|
||||
version = "20.0.4"
|
||||
|
||||
[package.dependencies]
|
||||
setuptools = ">=3.0"
|
||||
|
||||
[package.extras]
|
||||
eventlet = ["eventlet (>=0.9.7)"]
|
||||
gevent = ["gevent (>=0.13)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
tornado = ["tornado (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Read metadata from Python packages"
|
||||
marker = "python_version < \"3.8\""
|
||||
name = "importlib-metadata"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
version = "1.7.0"
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "rst.linker"]
|
||||
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
name = "isort"
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4.0"
|
||||
version = "5.0.2"
|
||||
|
||||
[package.extras]
|
||||
pipfile_deprecated_finder = ["pipreqs", "requirementslib", "tomlkit (>=0.5.3)"]
|
||||
requirements_deprecated_finder = ["pipreqs", "pip-api"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Various helpers to pass data to untrusted environments and back."
|
||||
name = "itsdangerous"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.1.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A very fast and expressive template engine."
|
||||
name = "jinja2"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.11.2"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.23"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=0.8)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Messaging library for Python."
|
||||
name = "kombu"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "4.6.11"
|
||||
|
||||
[package.dependencies]
|
||||
amqp = ">=2.6.0,<2.7"
|
||||
|
||||
[package.dependencies.importlib-metadata]
|
||||
python = "<3.8"
|
||||
version = ">=0.18"
|
||||
|
||||
[package.extras]
|
||||
azureservicebus = ["azure-servicebus (>=0.21.1)"]
|
||||
azurestoragequeues = ["azure-storage-queue"]
|
||||
consul = ["python-consul (>=0.6.0)"]
|
||||
librabbitmq = ["librabbitmq (>=1.5.2)"]
|
||||
mongodb = ["pymongo (>=3.3.0)"]
|
||||
msgpack = ["msgpack"]
|
||||
pyro = ["pyro4"]
|
||||
qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
|
||||
redis = ["redis (>=3.3.11)"]
|
||||
slmq = ["softlayer-messaging (>=1.0.3)"]
|
||||
sqlalchemy = ["sqlalchemy"]
|
||||
sqs = ["boto3 (>=1.4.4)", "pycurl (7.43.0.2)"]
|
||||
yaml = ["PyYAML (>=3.10)"]
|
||||
zookeeper = ["kazoo (>=1.3.1)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
name = "markupsafe"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
version = "1.1.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
name = "mccabe"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.6.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
name = "pathspec"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.8.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python style guide checker"
|
||||
name = "pycodestyle"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.6.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "passive checker of Python programs"
|
||||
name = "pyflakes"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
name = "pytz"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python client for Redis key-value store"
|
||||
name = "redis"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "3.5.3"
|
||||
|
||||
[package.extras]
|
||||
hiredis = ["hiredis (>=0.1.3)"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
name = "regex"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.6.8"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
|
||||
name = "ruamel.yaml"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.16.10"
|
||||
|
||||
[package.dependencies]
|
||||
[package.dependencies."ruamel.yaml.clib"]
|
||||
python = "<3.9"
|
||||
version = ">=0.1.2"
|
||||
|
||||
[package.extras]
|
||||
docs = ["ryd"]
|
||||
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
|
||||
marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\""
|
||||
name = "ruamel.yaml.clib"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
name = "six"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
version = "1.15.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
|
||||
name = "sortedcontainers"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.2.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
name = "toml"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.10.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
name = "typed-ast"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.4.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Promises, promises, promises."
|
||||
name = "vine"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "1.3.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
name = "werkzeug"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "1.0.1"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A flexible forms validation and rendering library for Python web development."
|
||||
name = "wtforms"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.3.1"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = "*"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator"]
|
||||
ipaddress = ["ipaddress"]
|
||||
locale = ["Babel (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
marker = "python_version < \"3.8\""
|
||||
name = "zipp"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
version = "3.1.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||
testing = ["jaraco.itertools", "func-timeout"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "5b4178ff61e370e7f1ea8005c263dad7a00915c8494536db6cddbc8642df9ee3"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.files]
|
||||
amqp = [
|
||||
{file = "amqp-2.6.0-py2.py3-none-any.whl", hash = "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"},
|
||||
{file = "amqp-2.6.0.tar.gz", hash = "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b"},
|
||||
]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
|
||||
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
|
||||
]
|
||||
billiard = [
|
||||
{file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"},
|
||||
{file = "billiard-3.6.3.0.tar.gz", hash = "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
||||
]
|
||||
celery = [
|
||||
{file = "celery-4.4.6-py2.py3-none-any.whl", hash = "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916"},
|
||||
{file = "celery-4.4.6.tar.gz", hash = "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
]
|
||||
fakeredis = [
|
||||
{file = "fakeredis-1.4.1-py3-none-any.whl", hash = "sha256:647b2593d349d9d4e566c8dadb2e4c71ba35be5bdc4f1f7ac2d565a12a965053"},
|
||||
{file = "fakeredis-1.4.1.tar.gz", hash = "sha256:4d170886865a91dbc8b7f8cbd4e5d488f4c5f2f25dfae127f001617bbe9e8f97"},
|
||||
]
|
||||
flake8 = [
|
||||
{file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"},
|
||||
{file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"},
|
||||
]
|
||||
flask = [
|
||||
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
|
||||
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
|
||||
]
|
||||
flask-wtf = [
|
||||
{file = "Flask-WTF-0.14.3.tar.gz", hash = "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"},
|
||||
{file = "Flask_WTF-0.14.3-py2.py3-none-any.whl", hash = "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2"},
|
||||
]
|
||||
future = [
|
||||
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||
]
|
||||
gunicorn = [
|
||||
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
|
||||
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
|
||||
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.0.2-py3-none-any.whl", hash = "sha256:52530356f89f009c3e8e0bb36d3e0dc6ddb0a59926cb918d488e35582ecbfe36"},
|
||||
{file = "isort-5.0.2.tar.gz", hash = "sha256:ba8e494fc13974ec02a623c4aaead306bb86a3102a1df8d399748445f32a78f2"},
|
||||
]
|
||||
itsdangerous = [
|
||||
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
|
||||
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
]
|
||||
kombu = [
|
||||
{file = "kombu-4.6.11-py2.py3-none-any.whl", hash = "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a"},
|
||||
{file = "kombu-4.6.11.tar.gz", hash = "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
|
||||
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
|
||||
]
|
||||
mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
|
||||
{file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
|
||||
]
|
||||
pycodestyle = [
|
||||
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
|
||||
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
|
||||
]
|
||||
pyflakes = [
|
||||
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
|
||||
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
|
||||
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
|
||||
]
|
||||
redis = [
|
||||
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
||||
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"},
|
||||
{file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"},
|
||||
{file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"},
|
||||
{file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"},
|
||||
{file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"},
|
||||
{file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"},
|
||||
]
|
||||
"ruamel.yaml" = [
|
||||
{file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"},
|
||||
{file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"},
|
||||
]
|
||||
"ruamel.yaml.clib" = [
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"},
|
||||
{file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
sortedcontainers = [
|
||||
{file = "sortedcontainers-2.2.2-py2.py3-none-any.whl", hash = "sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"},
|
||||
{file = "sortedcontainers-2.2.2.tar.gz", hash = "sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
|
||||
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
|
||||
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
|
||||
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
|
||||
]
|
||||
vine = [
|
||||
{file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"},
|
||||
{file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
|
||||
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
|
||||
]
|
||||
wtforms = [
|
||||
{file = "WTForms-2.3.1-py2.py3-none-any.whl", hash = "sha256:6ff8635f4caeed9f38641d48cfe019d0d3896f41910ab04494143fc027866e1b"},
|
||||
{file = "WTForms-2.3.1.tar.gz", hash = "sha256:861a13b3ae521d6700dac3b2771970bd354a63ba7043ecc3a82b5288596a1972"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
|
||||
{file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
|
||||
]
|
87
pyproject.toml
Normal file
87
pyproject.toml
Normal file
@ -0,0 +1,87 @@
|
||||
[tool.poetry]
|
||||
name = "magic-app"
|
||||
version = "0.1.0"
|
||||
description = "A swarm of dreams"
|
||||
authors = ["Autonomic Co-operative <helo@autonomic.zone>"]
|
||||
maintainers = ["Autonomic Co-operative <helo@autonomic.zone>"]
|
||||
license = "AGPLv3"
|
||||
readme = "README.md"
|
||||
homepage = "https://git.autonomic.zone/autonomic-cooperative/magic-app"
|
||||
repository = "https://git.autonomic.zone/autonomic-cooperative/magic-app"
|
||||
documentation = "https://git.autonomic.zone/autonomic-cooperative/magic-app"
|
||||
keywords = ["docker", "swarm", "packaging"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
"ruamel.yaml" = "^0.16.10"
|
||||
celery = "^4.4.6"
|
||||
flask = "^1.1.2"
|
||||
flask-wtf = "^0.14.3"
|
||||
gunicorn = "^20.0.4"
|
||||
python = "^3.8"
|
||||
redis = "^3.5.3"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^19.10b0"
|
||||
fakeredis = "^1.4.1"
|
||||
flake8 = "^3.8.3"
|
||||
isort = "^5.0.2"
|
||||
|
||||
[tool.poetry.urls]
|
||||
issues = "https://git.autonomic.zone/autonomic-cooperative/magic-app/issues"
|
||||
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
target-version = ["py38"]
|
||||
include = '\.pyi?$'
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=1.0.9,<2.0"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py38 lint sort format type
|
||||
skip_missing_interpreters = True
|
||||
isolated_build = True
|
||||
|
||||
[testenv]
|
||||
|
||||
[testenv:lint]
|
||||
skipdist = True
|
||||
deps = flake8
|
||||
commands = flake8 {posargs:--max-line-length 80} magic_app/ test/
|
||||
|
||||
[testenv:sort]
|
||||
skipdist = True
|
||||
deps = isort
|
||||
commands = isort {posargs:-c} -sp setup.cfg magic_app/ test/
|
||||
|
||||
[testenv:format]
|
||||
skipdist = True
|
||||
deps = black
|
||||
commands = black {posargs:--check} magic_app/ test/
|
||||
|
||||
[testenv:type]
|
||||
skipdist = True
|
||||
deps = mypy
|
||||
commands = mypy {posargs:--ignore-missing-imports} magic_app/ test/
|
||||
|
||||
[testenv:release]
|
||||
deps = twine
|
||||
commands =
|
||||
rm -rf {toxworkdir}/dist
|
||||
python -m setup sdist --dist-dir {toxworkdir}/dist bdist_wheel
|
||||
python -m setup sdist --dist-dir {toxworkdir}/dist bdist_egg
|
||||
twine upload {toxworkdir}/dist/*
|
||||
whitelist_externals =
|
||||
rm
|
||||
"""
|
||||
|
||||
[tool.isort]
|
||||
include_trailing_comma = true
|
||||
known_first_party = "magic_app"
|
||||
known_third_party = "pytest"
|
||||
line_length = 80
|
||||
multi_line_output = 3
|
||||
skip = ".venv,.tox"
|
0
scripts/__init__.py
Normal file
0
scripts/__init__.py
Normal file
9
scripts/celworker.py
Normal file
9
scripts/celworker.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""Bootstrap the application for Celery."""
|
||||
from os import environ
|
||||
|
||||
from magic_app.app import celery, create_app # noqa
|
||||
from magic_app.config import CONFIG
|
||||
|
||||
config = CONFIG[environ["FLASK_ENV"]]
|
||||
app = create_app(config=config)
|
||||
app.app_context().push()
|
8
scripts/wsgi.py
Normal file
8
scripts/wsgi.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""Bootstrap the application for web serving."""
|
||||
import os
|
||||
|
||||
from magic_app.app import create_app
|
||||
from magic_app.config import CONFIG
|
||||
|
||||
config = CONFIG[os.environ["FLASK_ENV"]]
|
||||
app = create_app(config=config)
|
27
spikes/README.md
Normal file
27
spikes/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Run It
|
||||
|
||||
```bash
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ python first.py / second.py / third.py
|
||||
```
|
||||
|
||||
## first.py
|
||||
|
||||
- List apps from an `app.json` (points to https://git.autonomic.zone/compose-stacks)
|
||||
- Clone selected app template and parse configuration for inputs (env vars and secrets)
|
||||
- Generate a form so those values can be filled out and allow it to be saved
|
||||
- Save the form inputs to a `db.json` (as a start)
|
||||
- Deploy an applicaiton to a local swarm (assumes access to local docker socket)
|
||||
- Create an "edit app" page where the `db.json` is re-called and can be updated
|
||||
- Make sure re-deploy works (taking care of updating secret and app versions)
|
||||
|
||||
## second.py
|
||||
|
||||
- Don't try to be smart with the auto-generation, hard-code everything. We
|
||||
maintain the app template (`compose.yml`) and this code anyway, so we just
|
||||
need to be aware of each other and keep in sync. This would optimise for
|
||||
trust and collaboration and not "smart" code.
|
||||
- Hard-code the secrets/configs required to make the code for generating
|
||||
versions simpler as well.
|
16
spikes/templates/second/macros.html
Normal file
16
spikes/templates/second/macros.html
Normal file
@ -0,0 +1,16 @@
|
||||
{#
|
||||
From: https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#rendering-errors
|
||||
Usage: with_errors(form.field, style='font-weight: bold')
|
||||
#}
|
||||
|
||||
{% macro with_errors(field) %}
|
||||
<div class="form_field">
|
||||
{% if field.errors %}
|
||||
{% set css_class = 'has_error ' + kwargs.pop('class', '') %}
|
||||
{{ field(class=css_class, **kwargs) }}
|
||||
<ul class="errors">{% for error in field.errors %}<li>{{ error|e }}</li>{% endfor %}</ul>
|
||||
{% else %}
|
||||
{{ field(**kwargs) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
Reference in New Issue
Block a user