Skip to content

Commit

Permalink
Extension Factory 🐹
Browse files Browse the repository at this point in the history
  • Loading branch information
rochacbruno committed Sep 30, 2017
1 parent b515f1a commit dcbb269
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*.build*
build/
*__pycache__*
cms/database
201 changes: 147 additions & 54 deletions cms/README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
# 1) Arquitetura do Projeto e dicas de estrutura e qualidade

Instalado através do `setup.py` com `python setup.py develop` e irá prover
a ferramenta de linha de comando `cms` a partir da qual iremos rodar
`cms runserver` e `cms shell` e `cms adduser`

A estrutura do projeto será:
# 3) Extension Factory

```bash
Makefile # Utilidades `install`, `clean`, `pep8` e `test`
setup.py # Instalador do projeto `python setup.py develop`
tests/ # Testes com py.test
cms/ # module root
├── app/ # Application Factory (Flask app será iniciada aqui)
├── config/ # Configuration Factory (Load de configurações)
├── ext/ # Extensões (Blueprints) do app
├── static/ # Arquivos estáticos (.css, .js, .images)
├── templates/ # Templates Jinja2
├── cli.py # Ferramenta de linha de comando `cms --help`
├── __init__.py # Python module init
├── README.md # Este arquivo
└── settings.yml # Configurações que serão carregadas
```

O Flask pode ler configurações de objetos, módulos Python e arquivos json
de acordo com o https://12factor.net/pt_br/config devemos ter configurações
default na aplicação mas todas as configurações variáveis deve ficar no ambiente.
Hora de carregar as extensões que usaremos no projeto sendo elas:

- **TinyMongo** - Banco de dados NoSQL baseado em arquivos json
- **Flask Admin** - Interface administrativa
- **Flask SimpleLogin** - Autenticação
- **Flask Debug Toolbar** - Ferramenta para debug

Para carregar essas extensões usaremos o conceito de `extension factory` e de
forma dinâmica carregamos as extensões que serão definidas no `settings.yml`

Para isto usaremos a extensão `FlaskDynaconf` do módulo `dynaconf` com esta
extensão podemos ler as configurações a partir de arquivos, bancos de dados e
variáveis de ambiente.
Vamos começar inicializando as extensões básicas e depois customizaremos o
`admin` e a parte de `autenticação`

Em nosso CMS iremos ler as configurações de um arquivo `settings.yml` e também opcionalmente
de variáveis de ambiente.
No `settings.yml` adicionaremos o item `EXTENSIONS`

```yaml
```py
CMS:
SECRET_KEY: 'real_secret_here'
DB_NAME: cms_db
Expand All @@ -42,67 +32,170 @@ CMS:
PORT: 5000
DEBUG: true
RELOADER: true

EXTENSIONS:
- cms.ext.database.configure
- cms.ext.admin.configure
- cms.ext.auth.configure
- cms.ext.debug.configure

```

Para começar a implentação do **config factory** no `config/__init__.py`
Carregaremos dinamicamente todos os módulos definidos na lista `EXTENSIONS` e
para cada um esperamos a existencia de uma função `configure` que recebe `app`
como único argumento.

a primeira coisa a fazer é implementar o `extension factory` em `ext/__init__.py`

```py
from dynaconf.contrib.flask_dynaconf import FlaskDynaconf
import import_string


def configure(app):
"""Configure Dynaconf Flask Extension"""
FlaskDynaconf(
app=app,
DYNACONF_NAMESPACE='CMS',
SETTINGS_MODULE=f'{app.root_path}/settings.yml'
)
"""Extension Factory, carrega as extensões definidas em
app.config.EXTENSIONS
"""
for extension in app.config.get('EXTENSIONS', []):
try:
factory = import_string(extension)
factory(app)
except Exception as e:
app.logger.error(f'Erro ao carregar {extension}: {e}')
else:
app.logger.info(f'Extensão {extension} carregada com sucesso!')

```

E então invocaremos essa função no `app factory` em `app/__init__.py`
Agora é só invocar este factory no `app/__init__.py` app factory `create_app`

```py
from flask import Flask
from cms import config
from cms import config, ext


def create_app():
app = Flask(__name__)
def create_app(import_name):
"""import_name tem que ser sempre a raiz do projeto
onde está o cli.py, templates/ e static/"""

app = Flask(import_name)

# Iniciar o sistema de configurações dinâmicas
config.configure(app)

# Carregar as extensões
ext.configure(app)

return app
```

# Extensões

A primeira extensão que carregamos é a de banco de dados

## TinyMongo

No `ext/database.py`

```py
from pathlib import Path
from tinymongo import TinyMongoClient

def configure(app):
"""Inicia o client do TinyMongo e adiciona `app.db`
*para usar MongoDB basta mudar para `pymongo.MongoClient`
"""
db_folder = app.config.get('DB_FOLDER', 'database')
db_name = app.config.get('DB_NAME', 'cms_db')
foldername = Path(db_folder) / Path(app.root_path) / Path('database')
client = TinyMongoClient(foldername=foldername)
app.db = client[db_name]
```

e então no `cli.py` podemos utilizar alguns valores default a partir do
`app.config`
## Autenticação

Nas mensagens podemos fazer algo como:
No `ext/auth.py`

```py
click.echo(f'Iniciando o shell do {app.config.SITENAME}')
from flask import current_app
from flask_simplelogin import SimpleLogin
from werkzeug.security import check_password_hash, generate_password_hash


def configure(app):
"""Inicializa o Flask Simple Login"""
SimpleLogin(app, login_checker=login_checker)
app.db.create_user = create_user

def login_checker(...):

def create_user(...):

```

e também nas opções dos comandos.
## Interface admin

No `ext/admin.py`

```py
...
@click.option('--debug/--no-debug', default=app.config.DEBUG)
@click.option('--reloader/--no-reloader', default=app.config.RELOADER)
@click.option('--host', default=app.config.HOST)
@click.option('--port', default=app.config.PORT)
def runserver(debug, reloader, host, port):
...
from flask_admin import Admin
from flask_admin.base import AdminIndexView
from flask_admin.contrib.pymongo import ModelView
from flask_simplelogin import login_required

# decorate Flask-Admin view via Monkey Patching
AdminIndexView._handle_view = login_required(AdminIndexView._handle_view)
ModelView._handle_view = login_required(ModelView._handle_view)

def configure(app):
"""Inicia uma instância do Flask-Admin"""
app.admin = Admin(
app,
name=app.config.get('ADMIN_NAME', 'Flask CMS'),
template_mode=app.config.get('ADMIN_STYLE', 'bootstrap3')
)
```

Pronto agora em qualquer momento podemos reescrever as configs no arquivo `settings.yml` ou exportar como variáveis de ambiente
## Debug Toolbar

```bash
export CMS_PORT='@int 3000'
export CMS_HOST='127.0.0.1'
export CMS_SITENAME='Meu Flask CMS!'
No `ext/debug.py`

```py
from flask_debugtoolbar import DebugToolbarExtension


def configure(app):
if app.config.get('DEBUG_TOOLBAR_ENABLED'):
DebugToolbarExtension(app)
```

O próximo passo é carregar algumas extensões no `extension factory`
# Configuração das extensões

Algumas extensões requerem configurações adicionais, basta incluir no `settings.yml`

```yml
CMS:
SECRET_KEY: 'real_secret_here'
DB_NAME: cms_db
SITENAME: Flask CMS
HOST: '0.0.0.0'
PORT: 5000
DEBUG: true
RELOADER: true

EXTENSIONS:
- cms.ext.database.configure
- cms.ext.admin.configure
- cms.ext.auth.configure
- cms.ext.debug.configure

DEBUG_TOOLBAR_ENABLED: true
DEBUG_TB_INTERCEPT_REDIRECTS: false
DEBUG_TB_PROFILER_ENABLED: true
DEBUG_TB_TEMPLATE_EDITOR_ENABLED: true

SIMPLE_LOGIN_HOME_URL: /admin

FLASK_ADMIN_NAME: Flask CMS!
FLASK_ADMIN_TEMPLATE_MODE: bootstrap3
FLASK_ADMIN_SWATCH: default
```
5 changes: 4 additions & 1 deletion cms/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask import Flask
from cms import config
from cms import config, ext


def create_app(import_name):
Expand All @@ -11,4 +11,7 @@ def create_app(import_name):
# Iniciar o sistema de configurações dinâmicas
config.configure(app)

# Carregar as extensões
ext.configure(app)

return app
18 changes: 10 additions & 8 deletions cms/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def shell():
@click.option('--port', default=app.config.PORT)
def runserver(debug, reloader, host, port):
"""Inicia o servidor em modo dev/debug"""
app.run(debug=debug, use_reloader=reloader, host=host, port=port)
app.run(debug=debug, use_reloader=reloader, host=host, port=port,
extra_files=[f'{app.root_path}/settings.yml'])


@main.command()
Expand All @@ -41,10 +42,11 @@ def runserver(debug, reloader, host, port):
confirmation_prompt=True)
def adduser(username, password):
"""Cria um novo usuário"""
try:
app.db.create_user(username, password)
except Exception as e:
click.echo(f'Não foi possivel criar o usuário {username}')
raise
else:
click.echo(f"Usuário {username} criado com sucesso!")
with app.app_context():
try:
app.db.create_user(username, password)
except Exception as e:
click.echo(f'Não foi possivel criar o usuário {username}')
raise
else:
click.echo(f"Usuário {username} criado com sucesso!")
15 changes: 15 additions & 0 deletions cms/ext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import import_string


def configure(app):
"""Extension Factory, carrega as extensões definidas em
app.config.EXTENSIONS
"""
for extension in app.config.get('EXTENSIONS', []):
try:
factory = import_string(extension)
factory(app)
except Exception as e:
app.logger.error(f'Erro ao carregar {extension}: {e}')
else:
app.logger.debug(f'Extensão {extension} carregada com sucesso!')
17 changes: 17 additions & 0 deletions cms/ext/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from flask_admin import Admin
from flask_admin.base import AdminIndexView
from flask_admin.contrib.pymongo import ModelView
from flask_simplelogin import login_required

# decorate Flask-Admin view via Monkey Patching
AdminIndexView._handle_view = login_required(AdminIndexView._handle_view)
ModelView._handle_view = login_required(ModelView._handle_view)


def configure(app):
"""Inicia uma instância do Flask-Admin"""
app.admin = Admin(
app,
name=app.config.get('FLASK_ADMIN_NAME', 'Flask CMS'),
template_mode=app.config.get('FLASK_ADMIN_TEMPLATE_MODE', 'bootstrap3')
)
39 changes: 39 additions & 0 deletions cms/ext/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from flask import current_app
from flask_simplelogin import SimpleLogin
from werkzeug.security import check_password_hash, generate_password_hash


def configure(app):
"""Inicializa o Flask Simple Login"""
SimpleLogin(app, login_checker=login_checker)
app.db.create_user = create_user

# Functions


def login_checker(user):
"""Valida o usuário e senha para efetuar o login"""
username = user.get('username')
password = user.get('password')
if not username or not password:
return False

existing_user = current_app.db.users.find_one({'username': username})
if not existing_user:
return False

if check_password_hash(existing_user.get('password'), password):
return True

return False


def create_user(username, password):
"""Registra um novo usuário caso não esteja cadastrado"""
if current_app.db.users.find_one({'username': username}):
raise RuntimeError(f'{username} já está cadastrado')

user = {'username': username,
'password': generate_password_hash(password)}

current_app.db.users.insert_one(user)
15 changes: 15 additions & 0 deletions cms/ext/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path
from tinymongo import TinyMongoClient


def configure(app):
"""Inicia o client do TinyMongo e adiciona `app.db`
*para usar MongoDB basta mudar para `pymongo.MongoClient`
"""
db_folder = app.config.get('DB_FOLDER', 'database')
db_name = app.config.get('DB_NAME', 'cms_db')

foldername = Path(db_folder) / Path(app.root_path) / Path('database')
client = TinyMongoClient(foldername=foldername)

app.db = client[db_name]
Loading

0 comments on commit dcbb269

Please sign in to comment.