Skip to content

Commit 270ee9f

Browse files
committed
update for pyweb presentation
1 parent 633bd3f commit 270ee9f

29 files changed

+482
-464
lines changed

.env

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FLASK_ENV=development

docker-compose.yaml

+5-10
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,19 @@
1313

1414
version: '2'
1515
services:
16-
flog-pg:
16+
flog2-pg:
1717
image: postgres:12-alpine
18-
container_name: flog-pg
18+
container_name: flog2-pg
1919
ports:
2020
- '127.0.0.1:54321:5432'
2121
environment:
22-
# Kk for flog dev, potentially UNSAFE in other applications. Don't blindly copy & paste
22+
# Kk for flog2 dev, potentially UNSAFE in other applications. Don't blindly copy & paste
2323
# without considering implications.
2424
POSTGRES_HOST_AUTH_METHOD: trust
2525
# Could use this instead with a strong password if working outside a dev env
2626
# POSTGRES_PASSWORD: password
27-
flog-rabbitmq:
27+
flog2-rabbitmq:
2828
image: rabbitmq:3.8-alpine
29-
container_name: flog-rabbitmq
29+
container_name: flog2-rabbitmq
3030
ports:
3131
- '127.0.0.1:56721:5672'
32-
flog-redis:
33-
image: redis:5-alpine
34-
container_name: flog-redis
35-
ports:
36-
- '127.0.0.1:63791:6379'

flog-config.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
db_uri = 'postgresql://postgres@localhost:54321/flog'
2+
3+
4+
def development_config(app, config):
5+
config['SQLALCHEMY_DATABASE_URI'] = db_uri
6+
7+
return config
8+
9+
10+
def testing_config(app, config):
11+
config['SQLALCHEMY_DATABASE_URI'] = db_uri + '_tests'
12+
13+
return config

flog/actors.py

-35
This file was deleted.

flog/app.py

+63-18
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,77 @@
1-
import click
1+
import logging
2+
import pathlib
3+
from os import environ
24

3-
from flask import Flask
5+
import click
6+
import flask
47
from flask.cli import FlaskGroup
5-
from flask_sqlalchemy import SQLAlchemy
68

79
import flog.cli
10+
import flog.ext
11+
from flog.libs.config import init_config
12+
from flog.libs.logging import init_logging
13+
from flog.libs.testing import CLIRunner
14+
import flog.views
15+
16+
_app_name = 'flog'
17+
_root_path = pathlib.Path(__file__).parent
18+
19+
log = logging.getLogger(__name__)
20+
21+
22+
class FlogApp(flask.Flask):
23+
test_cli_runner_class = CLIRunner
24+
25+
@classmethod
26+
def create(cls, init_app=True, testing=False, **kwargs):
27+
"""
28+
For CLI app init blueprints but not config b/c we want to give the calling CLI group
29+
the ability to set values from the command line args/options before configuring the
30+
app. But if we don't init the blueprints right away, then the CLI doesn't know
31+
anything about the cli groups & commands added by blueprints.
32+
"""
33+
if testing:
34+
environ['FLASK_ENV'] = 'testing'
835

9-
db = SQLAlchemy()
36+
app = cls(_app_name, root_path=_root_path, **kwargs)
37+
app.testing = testing
1038

39+
app.init_blueprints()
40+
if init_app:
41+
app.init_app()
1142

12-
def create_app():
13-
# Circular imports require this import to go inside the function
14-
from flog import views
43+
return app
1544

16-
app = Flask(__name__)
45+
def init_blueprints(self):
46+
self.register_blueprint(flog.cli.cli_bp)
47+
self.register_blueprint(flog.views.public)
1748

18-
app.config['SQLALCHEMY_DATABASE_URI'] = \
19-
'postgresql://postgres:password@localhost:54321/postgres'
20-
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
49+
def init_app(self, log_level='info', with_sentry=False):
50+
init_config(self)
2151

22-
db.init_app(app)
52+
if not self.testing:
53+
init_logging(log_level, self.name)
2354

24-
app.register_blueprint(views.public)
25-
app.register_blueprint(flog.cli.cli_bp)
55+
if with_sentry:
56+
assert not self.testing, 'Sentry should not be enabled during testing.'
57+
sentry_dsn = self.config.get('SENTRY_DSN')
58+
if not sentry_dsn:
59+
raise ValueError('Sentry DSN expected but not configured.')
60+
else:
61+
import sentry_sdk
62+
sentry_sdk.init(sentry_dsn)
2663

27-
return app
64+
flog.ext.init_ext(self)
2865

2966

30-
@click.group(cls=FlaskGroup, create_app=create_app)
31-
def cli():
32-
"""Management script for the Wiki application."""
67+
@click.group(cls=FlaskGroup, create_app=lambda _: FlogApp.create(init_app=False))
68+
@click.option('--quiet', 'log_level', flag_value='quiet', help='Hide info level log messages')
69+
@click.option('--info', 'log_level', flag_value='info', default=True,
70+
help='Show info level log messages (default)')
71+
@click.option('--debug', 'log_level', flag_value='debug', help='Show debug level log messages')
72+
@click.option('--with-sentry', is_flag=True, default=False,
73+
help='Enable Sentry (usually only in production)')
74+
@flask.cli.pass_script_info
75+
def cli(scriptinfo, log_level, with_sentry):
76+
app = scriptinfo.load_app()
77+
app.init_app(log_level, with_sentry)

flog/cli.py

+18-107
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,29 @@
1-
import concurrent.futures
1+
import logging
22

3-
import arrow
43
import click
5-
import dramatiq
6-
from flask import Blueprint
7-
8-
from flog import actors
9-
from flog.libs import hackernews as hn
10-
from flog.libs import localhost
4+
from flask import Blueprint, current_app
115

6+
from flog.libs.database import create_db
127

8+
log = logging.getLogger(__name__)
139
cli_bp = Blueprint('cli', __name__, cli_group=None)
1410

1511

16-
@cli_bp.cli.command('hn-profile')
17-
@click.argument('username')
18-
def hn_profile(username):
19-
user = hn.User(username)
20-
print(user)
21-
22-
23-
@cli_bp.cli.command('hello')
24-
@click.argument('name', default='World')
25-
@click.option('--queue', 'use_queue', is_flag=True, default=False)
26-
def hello(name, use_queue):
27-
if use_queue:
28-
actors.say_hello.send(name)
29-
print('say_hello() job has been queued.')
30-
else:
31-
actors.say_hello(name)
32-
33-
34-
@cli_bp.cli.command()
35-
def top_stories():
36-
story_ids = hn.top_stories()
37-
messages = [actors.hn_story.message(sid) for sid in story_ids]
38-
group = dramatiq.group(messages).run()
39-
# 60 second timeout (in milliseconds)
40-
for result in group.get_results(block=True, timeout=60_000):
41-
print(result)
42-
43-
44-
@cli_bp.cli.command()
45-
@click.option('--queue', 'use_queue', is_flag=True, default=False)
46-
def falcon_page(use_queue):
47-
if use_queue:
48-
actors.falcon_page.send()
49-
print('random_article() job has been queued.')
50-
else:
51-
print(actors.falcon_page())
52-
53-
54-
@cli_bp.cli.command()
55-
@click.argument('fetch_number', default=10)
56-
def falcon_pages(fetch_number):
57-
page_messages = [actors.falcon_page.message() for x in range(fetch_number)]
58-
group = dramatiq.group(page_messages).run()
59-
# 60 second timeout (in milliseconds)
60-
for result in group.get_results(block=True, timeout=60_000):
61-
print(result)
62-
63-
64-
@cli_bp.cli.command()
65-
@click.argument('requests_num', type=int)
66-
@click.option('--workers-num', default=5)
67-
def falcon_pages_threadpool(workers_num, requests_num):
68-
with concurrent.futures.ThreadPoolExecutor(max_workers=workers_num) as executor:
69-
future_to_url = [executor.submit(localhost.fetch_page) for x in range(requests_num)]
70-
71-
start = arrow.now()
72-
for future in concurrent.futures.as_completed(future_to_url):
73-
print(future.result())
74-
75-
duration = arrow.now() - start
76-
secs = duration.total_seconds()
77-
78-
print('worker count:', workers_num)
79-
print('Requests / second:', requests_num / secs)
80-
81-
8212
@cli_bp.cli.command()
83-
@click.argument('requests_num', type=int)
84-
@click.option('--workers-num', default=4)
85-
def falcon_pages_procpool(workers_num, requests_num):
86-
with concurrent.futures.ProcessPoolExecutor(max_workers=workers_num) as executor:
87-
future_to_url = [executor.submit(localhost.fetch_page) for x in range(requests_num)]
88-
89-
start = arrow.now()
90-
for future in concurrent.futures.as_completed(future_to_url):
91-
print(future.result())
92-
93-
duration = arrow.now() - start
94-
secs = duration.total_seconds()
95-
96-
print('worker count:', workers_num)
97-
print('Requests / second:', requests_num / secs)
13+
@click.argument('name', default='World')
14+
def hello(name):
15+
log.debug('cli debug logging example')
16+
print(f'Hello, {name}!')
9817

9918

10019
@cli_bp.cli.command()
101-
@click.argument('requests_num', type=int)
102-
@click.option('--workers-num', default=4)
103-
def falcon_pages_async(workers_num, requests_num):
104-
import aiohttp
105-
import asyncio
106-
107-
async def fetch(session, url):
108-
async with session.get(url) as response:
109-
return await response.text()
110-
111-
async def main():
112-
async with aiohttp.ClientSession() as session:
113-
for x in range(requests_num):
114-
text = await fetch(session, 'http://localhost:5000/')
115-
print(text)
116-
117-
loop = asyncio.get_event_loop()
118-
loop.run_until_complete(main())
20+
@click.option('--drop-first', is_flag=True, default=False)
21+
@click.option('--for-tests', is_flag=True, default=False)
22+
def db_init(drop_first, for_tests):
23+
""" Initialize the database """
24+
app = current_app
25+
sa_url = app.config['SQLALCHEMY_DATABASE_URI']
26+
if for_tests:
27+
sa_url += '_tests'
28+
29+
create_db(sa_url, drop_first)

flog/conftest.py

+14-19
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
from mixer.backend.flask import mixer as flask_mixer
21
import pytest
32

43
import flog.app
4+
import flog.ext
55

66

77
@pytest.fixture(scope='session')
88
def app():
9-
app = flog.app.create_app()
10-
app.testing = True
9+
app = flog.app.FlogApp.create(testing=True)
1110

12-
flog.app.db.drop_all(app=app)
13-
flog.app.db.create_all(app=app)
11+
flog.ext.db.drop_all(app=app)
12+
flog.ext.db.create_all(app=app)
1413

1514
return app
1615

@@ -19,24 +18,20 @@ def app():
1918
def db(app):
2019
# print('db fixture')
2120
with app.app_context():
22-
yield flog.app.db
23-
flog.app.db.session.remove()
21+
yield flog.ext.db
22+
flog.ext.db.session.remove()
2423

2524

26-
@pytest.fixture(scope='session')
27-
def mixer_ext(app):
28-
# print('mixer_ext fixture')
29-
mixer = flask_mixer
30-
mixer.init_app(app)
31-
return mixer
25+
@pytest.fixture()
26+
def web(app):
27+
return app.test_client()
3228

3329

3430
@pytest.fixture()
35-
def mixer(mixer_ext, db):
36-
# print('mixer fixture')
37-
return flask_mixer
31+
def cli(app):
32+
return app.test_cli_runner()
3833

3934

40-
@pytest.fixture()
41-
def client(app):
42-
return app.test_client()
35+
@pytest.fixture(scope='session')
36+
def script_args():
37+
return ['python', '-c', 'from flog import app; app.cli()']

flog/ext.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from flask_sqlalchemy import SQLAlchemy
2+
3+
db = SQLAlchemy()
4+
5+
6+
def init_ext(app):
7+
db.init_app(app)

0 commit comments

Comments
 (0)