chiron 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,667 @@
1
+ # Flask Development Workflow
2
+
3
+ Comprehensive guide for developing Flask applications with best practices and modern patterns.
4
+
5
+ ## Project Setup
6
+
7
+ ### Basic Flask Application Structure
8
+ ```
9
+ myapp/
10
+ ├── app/
11
+ │ ├── __init__.py # Application factory
12
+ │ ├── models/ # Database models
13
+ │ ├── views/ # Route handlers
14
+ │ │ ├── __init__.py
15
+ │ │ ├── auth.py
16
+ │ │ └── main.py
17
+ │ ├── templates/ # Jinja2 templates
18
+ │ ├── static/ # CSS, JS, images
19
+ │ └── utils/ # Helper functions
20
+ ├── migrations/ # Database migrations
21
+ ├── tests/ # Test files
22
+ ├── config.py # Configuration
23
+ ├── requirements.txt # Dependencies
24
+ └── run.py # Application entry point
25
+ ```
26
+
27
+ ### Application Factory Pattern
28
+ ```python
29
+ # app/__init__.py
30
+ from flask import Flask
31
+ from flask_sqlalchemy import SQLAlchemy
32
+ from flask_migrate import Migrate
33
+ from flask_login import LoginManager
34
+ import os
35
+
36
+ db = SQLAlchemy()
37
+ migrate = Migrate()
38
+ login_manager = LoginManager()
39
+
40
+ def create_app(config_class=None):
41
+ app = Flask(__name__)
42
+
43
+ # Configuration
44
+ if config_class:
45
+ app.config.from_object(config_class)
46
+ else:
47
+ app.config.from_object('config.Config')
48
+
49
+ # Initialize extensions
50
+ db.init_app(app)
51
+ migrate.init_app(app, db)
52
+ login_manager.init_app(app)
53
+ login_manager.login_view = 'auth.login'
54
+
55
+ # Register blueprints
56
+ from app.views.main import bp as main_bp
57
+ app.register_blueprint(main_bp)
58
+
59
+ from app.views.auth import bp as auth_bp
60
+ app.register_blueprint(auth_bp, url_prefix='/auth')
61
+
62
+ return app
63
+
64
+ # Import models for migrations
65
+ from app import models
66
+ ```
67
+
68
+ ### Configuration Management
69
+ ```python
70
+ # config.py
71
+ import os
72
+ from dotenv import load_dotenv
73
+
74
+ basedir = os.path.abspath(os.path.dirname(__file__))
75
+ load_dotenv(os.path.join(basedir, '.env'))
76
+
77
+ class Config:
78
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
79
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
80
+ 'sqlite:///' + os.path.join(basedir, 'app.db')
81
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
82
+ MAIL_SERVER = os.environ.get('MAIL_SERVER')
83
+ MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
84
+ MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
85
+ MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
86
+ MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
87
+
88
+ class DevelopmentConfig(Config):
89
+ DEBUG = True
90
+
91
+ class TestingConfig(Config):
92
+ TESTING = True
93
+ SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
94
+
95
+ class ProductionConfig(Config):
96
+ DEBUG = False
97
+
98
+ config = {
99
+ 'development': DevelopmentConfig,
100
+ 'testing': TestingConfig,
101
+ 'production': ProductionConfig,
102
+ 'default': DevelopmentConfig
103
+ }
104
+ ```
105
+
106
+ ## Models and Database
107
+
108
+ ### SQLAlchemy Models
109
+ ```python
110
+ # app/models.py
111
+ from app import db, login_manager
112
+ from flask_login import UserMixin
113
+ from werkzeug.security import generate_password_hash, check_password_hash
114
+ from datetime import datetime
115
+
116
+ class User(UserMixin, db.Model):
117
+ id = db.Column(db.Integer, primary_key=True)
118
+ username = db.Column(db.String(64), index=True, unique=True)
119
+ email = db.Column(db.String(120), index=True, unique=True)
120
+ password_hash = db.Column(db.String(128))
121
+ posts = db.relationship('Post', backref='author', lazy='dynamic')
122
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
123
+
124
+ def set_password(self, password):
125
+ self.password_hash = generate_password_hash(password)
126
+
127
+ def check_password(self, password):
128
+ return check_password_hash(self.password_hash, password)
129
+
130
+ def __repr__(self):
131
+ return f'<User {self.username}>'
132
+
133
+ class Post(db.Model):
134
+ id = db.Column(db.Integer, primary_key=True)
135
+ title = db.Column(db.String(100), nullable=False)
136
+ body = db.Column(db.Text)
137
+ timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
138
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
139
+
140
+ def __repr__(self):
141
+ return f'<Post {self.title}>'
142
+
143
+ @login_manager.user_loader
144
+ def load_user(id):
145
+ return User.query.get(int(id))
146
+ ```
147
+
148
+ ### Database Migrations
149
+ ```bash
150
+ # Initialize migrations
151
+ flask db init
152
+
153
+ # Create migration
154
+ flask db migrate -m "Add user and post tables"
155
+
156
+ # Apply migration
157
+ flask db upgrade
158
+
159
+ # Downgrade migration
160
+ flask db downgrade
161
+ ```
162
+
163
+ ## Views and Routing
164
+
165
+ ### Blueprint Structure
166
+ ```python
167
+ # app/views/main.py
168
+ from flask import Blueprint, render_template, request, flash, redirect, url_for
169
+ from flask_login import login_required, current_user
170
+ from app import db
171
+ from app.models import Post
172
+
173
+ bp = Blueprint('main', __name__)
174
+
175
+ @bp.route('/')
176
+ @bp.route('/index')
177
+ def index():
178
+ posts = Post.query.order_by(Post.timestamp.desc()).all()
179
+ return render_template('index.html', posts=posts)
180
+
181
+ @bp.route('/create', methods=['GET', 'POST'])
182
+ @login_required
183
+ def create_post():
184
+ if request.method == 'POST':
185
+ title = request.form['title']
186
+ body = request.form['body']
187
+
188
+ if not title:
189
+ flash('Title is required!')
190
+ else:
191
+ post = Post(title=title, body=body, author=current_user)
192
+ db.session.add(post)
193
+ db.session.commit()
194
+ flash('Your post has been created!')
195
+ return redirect(url_for('main.index'))
196
+
197
+ return render_template('create_post.html')
198
+
199
+ @bp.route('/post/<int:id>')
200
+ def show_post(id):
201
+ post = Post.query.get_or_404(id)
202
+ return render_template('post.html', post=post)
203
+ ```
204
+
205
+ ### Authentication Blueprint
206
+ ```python
207
+ # app/views/auth.py
208
+ from flask import Blueprint, render_template, redirect, url_for, flash, request
209
+ from flask_login import login_user, logout_user, login_required, current_user
210
+ from werkzeug.urls import url_parse
211
+ from app import db
212
+ from app.models import User
213
+
214
+ bp = Blueprint('auth', __name__)
215
+
216
+ @bp.route('/login', methods=['GET', 'POST'])
217
+ def login():
218
+ if current_user.is_authenticated:
219
+ return redirect(url_for('main.index'))
220
+
221
+ if request.method == 'POST':
222
+ username = request.form['username']
223
+ password = request.form['password']
224
+ remember_me = bool(request.form.get('remember_me'))
225
+
226
+ user = User.query.filter_by(username=username).first()
227
+
228
+ if user is None or not user.check_password(password):
229
+ flash('Invalid username or password')
230
+ return redirect(url_for('auth.login'))
231
+
232
+ login_user(user, remember=remember_me)
233
+
234
+ next_page = request.args.get('next')
235
+ if not next_page or url_parse(next_page).netloc != '':
236
+ next_page = url_for('main.index')
237
+
238
+ return redirect(next_page)
239
+
240
+ return render_template('auth/login.html')
241
+
242
+ @bp.route('/logout')
243
+ @login_required
244
+ def logout():
245
+ logout_user()
246
+ return redirect(url_for('main.index'))
247
+
248
+ @bp.route('/register', methods=['GET', 'POST'])
249
+ def register():
250
+ if current_user.is_authenticated:
251
+ return redirect(url_for('main.index'))
252
+
253
+ if request.method == 'POST':
254
+ username = request.form['username']
255
+ email = request.form['email']
256
+ password = request.form['password']
257
+
258
+ # Validation
259
+ if User.query.filter_by(username=username).first():
260
+ flash('Username already exists')
261
+ return redirect(url_for('auth.register'))
262
+
263
+ if User.query.filter_by(email=email).first():
264
+ flash('Email already registered')
265
+ return redirect(url_for('auth.register'))
266
+
267
+ user = User(username=username, email=email)
268
+ user.set_password(password)
269
+ db.session.add(user)
270
+ db.session.commit()
271
+
272
+ flash('Registration successful')
273
+ return redirect(url_for('auth.login'))
274
+
275
+ return render_template('auth/register.html')
276
+ ```
277
+
278
+ ## Forms and Validation
279
+
280
+ ### Flask-WTF Forms
281
+ ```python
282
+ # app/forms.py
283
+ from flask_wtf import FlaskForm
284
+ from wtforms import StringField, TextAreaField, PasswordField, BooleanField, SubmitField
285
+ from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
286
+ from app.models import User
287
+
288
+ class LoginForm(FlaskForm):
289
+ username = StringField('Username', validators=[DataRequired()])
290
+ password = PasswordField('Password', validators=[DataRequired()])
291
+ remember_me = BooleanField('Remember Me')
292
+ submit = SubmitField('Sign In')
293
+
294
+ class RegistrationForm(FlaskForm):
295
+ username = StringField('Username', validators=[
296
+ DataRequired(),
297
+ Length(min=4, max=64)
298
+ ])
299
+ email = StringField('Email', validators=[DataRequired(), Email()])
300
+ password = PasswordField('Password', validators=[
301
+ DataRequired(),
302
+ Length(min=8)
303
+ ])
304
+ password2 = PasswordField('Repeat Password', validators=[
305
+ DataRequired(),
306
+ EqualTo('password')
307
+ ])
308
+ submit = SubmitField('Register')
309
+
310
+ def validate_username(self, username):
311
+ user = User.query.filter_by(username=username.data).first()
312
+ if user is not None:
313
+ raise ValidationError('Please use a different username.')
314
+
315
+ def validate_email(self, email):
316
+ user = User.query.filter_by(email=email.data).first()
317
+ if user is not None:
318
+ raise ValidationError('Please use a different email.')
319
+
320
+ class PostForm(FlaskForm):
321
+ title = StringField('Title', validators=[
322
+ DataRequired(),
323
+ Length(min=1, max=100)
324
+ ])
325
+ body = TextAreaField('Content', validators=[
326
+ DataRequired(),
327
+ Length(min=1, max=1000)
328
+ ])
329
+ submit = SubmitField('Create Post')
330
+ ```
331
+
332
+ ### Using Forms in Views
333
+ ```python
334
+ # Updated view using forms
335
+ @bp.route('/create', methods=['GET', 'POST'])
336
+ @login_required
337
+ def create_post():
338
+ form = PostForm()
339
+ if form.validate_on_submit():
340
+ post = Post(
341
+ title=form.title.data,
342
+ body=form.body.data,
343
+ author=current_user
344
+ )
345
+ db.session.add(post)
346
+ db.session.commit()
347
+ flash('Your post has been created!')
348
+ return redirect(url_for('main.index'))
349
+
350
+ return render_template('create_post.html', form=form)
351
+ ```
352
+
353
+ ## Templates and Frontend
354
+
355
+ ### Base Template
356
+ ```html
357
+ <!-- app/templates/base.html -->
358
+ <!DOCTYPE html>
359
+ <html lang="en">
360
+ <head>
361
+ <meta charset="UTF-8">
362
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
363
+ <title>{% if title %}{{ title }} - MyApp{% else %}MyApp{% endif %}</title>
364
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
365
+ </head>
366
+ <body>
367
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
368
+ <div class="container">
369
+ <a class="navbar-brand" href="{{ url_for('main.index') }}">MyApp</a>
370
+ <div class="navbar-nav ms-auto">
371
+ {% if current_user.is_authenticated %}
372
+ <a class="nav-link" href="{{ url_for('main.create_post') }}">New Post</a>
373
+ <a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
374
+ {% else %}
375
+ <a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
376
+ <a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
377
+ {% endif %}
378
+ </div>
379
+ </div>
380
+ </nav>
381
+
382
+ <main class="container mt-4">
383
+ {% with messages = get_flashed_messages() %}
384
+ {% if messages %}
385
+ {% for message in messages %}
386
+ <div class="alert alert-info alert-dismissible fade show" role="alert">
387
+ {{ message }}
388
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
389
+ </div>
390
+ {% endfor %}
391
+ {% endif %}
392
+ {% endwith %}
393
+
394
+ {% block content %}{% endblock %}
395
+ </main>
396
+
397
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
398
+ </body>
399
+ </html>
400
+ ```
401
+
402
+ ### Form Template with Error Handling
403
+ ```html
404
+ <!-- app/templates/create_post.html -->
405
+ {% extends "base.html" %}
406
+
407
+ {% block content %}
408
+ <h2>Create New Post</h2>
409
+
410
+ <form method="POST">
411
+ {{ form.hidden_tag() }}
412
+
413
+ <div class="mb-3">
414
+ {{ form.title.label(class="form-label") }}
415
+ {{ form.title(class="form-control" + (" is-invalid" if form.title.errors else "")) }}
416
+ {% if form.title.errors %}
417
+ {% for error in form.title.errors %}
418
+ <div class="invalid-feedback">{{ error }}</div>
419
+ {% endfor %}
420
+ {% endif %}
421
+ </div>
422
+
423
+ <div class="mb-3">
424
+ {{ form.body.label(class="form-label") }}
425
+ {{ form.body(class="form-control" + (" is-invalid" if form.body.errors else ""), rows="5") }}
426
+ {% if form.body.errors %}
427
+ {% for error in form.body.errors %}
428
+ <div class="invalid-feedback">{{ error }}</div>
429
+ {% endfor %}
430
+ {% endif %}
431
+ </div>
432
+
433
+ <div class="mb-3">
434
+ {{ form.submit(class="btn btn-primary") }}
435
+ <a href="{{ url_for('main.index') }}" class="btn btn-secondary">Cancel</a>
436
+ </div>
437
+ </form>
438
+ {% endblock %}
439
+ ```
440
+
441
+ ## API Development
442
+
443
+ ### RESTful API with Flask-RESTful
444
+ ```python
445
+ # app/api/__init__.py
446
+ from flask import Blueprint
447
+ from flask_restful import Api
448
+
449
+ bp = Blueprint('api', __name__)
450
+ api = Api(bp)
451
+
452
+ from app.api import users, posts
453
+ ```
454
+
455
+ ### API Resources
456
+ ```python
457
+ # app/api/posts.py
458
+ from flask_restful import Resource, request, marshal_with, fields
459
+ from flask_login import login_required, current_user
460
+ from app.models import Post
461
+ from app import db
462
+
463
+ post_fields = {
464
+ 'id': fields.Integer,
465
+ 'title': fields.String,
466
+ 'body': fields.String,
467
+ 'timestamp': fields.DateTime(dt_format='iso8601'),
468
+ 'author': fields.String(attribute='author.username')
469
+ }
470
+
471
+ class PostResource(Resource):
472
+ @marshal_with(post_fields)
473
+ def get(self, id):
474
+ post = Post.query.get_or_404(id)
475
+ return post
476
+
477
+ @login_required
478
+ def delete(self, id):
479
+ post = Post.query.get_or_404(id)
480
+ if post.author != current_user:
481
+ return {'message': 'Permission denied'}, 403
482
+
483
+ db.session.delete(post)
484
+ db.session.commit()
485
+ return {'message': 'Post deleted'}, 200
486
+
487
+ class PostListResource(Resource):
488
+ @marshal_with(post_fields)
489
+ def get(self):
490
+ posts = Post.query.order_by(Post.timestamp.desc()).all()
491
+ return posts
492
+
493
+ @login_required
494
+ @marshal_with(post_fields)
495
+ def post(self):
496
+ data = request.get_json()
497
+
498
+ post = Post(
499
+ title=data['title'],
500
+ body=data.get('body', ''),
501
+ author=current_user
502
+ )
503
+
504
+ db.session.add(post)
505
+ db.session.commit()
506
+
507
+ return post, 201
508
+
509
+ # Register resources
510
+ from app.api import api
511
+ api.add_resource(PostResource, '/posts/<int:id>')
512
+ api.add_resource(PostListResource, '/posts')
513
+ ```
514
+
515
+ ## Testing Flask Applications
516
+
517
+ ### Test Configuration
518
+ ```python
519
+ # tests/conftest.py
520
+ import pytest
521
+ from app import create_app, db
522
+ from app.models import User, Post
523
+ from config import TestingConfig
524
+
525
+ @pytest.fixture
526
+ def app():
527
+ app = create_app(TestingConfig)
528
+
529
+ with app.app_context():
530
+ db.create_all()
531
+ yield app
532
+ db.drop_all()
533
+
534
+ @pytest.fixture
535
+ def client(app):
536
+ return app.test_client()
537
+
538
+ @pytest.fixture
539
+ def runner(app):
540
+ return app.test_cli_runner()
541
+
542
+ @pytest.fixture
543
+ def user(app):
544
+ user = User(username='testuser', email='test@example.com')
545
+ user.set_password('testpass')
546
+ db.session.add(user)
547
+ db.session.commit()
548
+ return user
549
+ ```
550
+
551
+ ### Testing Views
552
+ ```python
553
+ # tests/test_views.py
554
+ def test_index_page(client):
555
+ response = client.get('/')
556
+ assert response.status_code == 200
557
+ assert b'Welcome' in response.data
558
+
559
+ def test_login_required(client):
560
+ response = client.get('/create')
561
+ assert response.status_code == 302 # Redirect to login
562
+
563
+ def test_user_login(client, user):
564
+ response = client.post('/auth/login', data={
565
+ 'username': 'testuser',
566
+ 'password': 'testpass'
567
+ }, follow_redirects=True)
568
+
569
+ assert response.status_code == 200
570
+ assert b'Welcome' in response.data
571
+
572
+ def test_create_post(client, user):
573
+ # Login first
574
+ client.post('/auth/login', data={
575
+ 'username': 'testuser',
576
+ 'password': 'testpass'
577
+ })
578
+
579
+ # Create post
580
+ response = client.post('/create', data={
581
+ 'title': 'Test Post',
582
+ 'body': 'This is a test post'
583
+ }, follow_redirects=True)
584
+
585
+ assert response.status_code == 200
586
+ assert b'Test Post' in response.data
587
+ ```
588
+
589
+ ## Deployment
590
+
591
+ ### Production Configuration
592
+ ```python
593
+ # app/__init__.py additions for production
594
+ def create_app(config_class=None):
595
+ app = Flask(__name__)
596
+
597
+ # ... existing configuration ...
598
+
599
+ if not app.debug and not app.testing:
600
+ # Logging setup
601
+ import logging
602
+ from logging.handlers import RotatingFileHandler
603
+
604
+ if not os.path.exists('logs'):
605
+ os.mkdir('logs')
606
+
607
+ file_handler = RotatingFileHandler(
608
+ 'logs/myapp.log', maxBytes=10240, backupCount=10
609
+ )
610
+ file_handler.setFormatter(logging.Formatter(
611
+ '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
612
+ ))
613
+ file_handler.setLevel(logging.INFO)
614
+ app.logger.addHandler(file_handler)
615
+
616
+ app.logger.setLevel(logging.INFO)
617
+ app.logger.info('MyApp startup')
618
+
619
+ return app
620
+ ```
621
+
622
+ ### Docker Setup
623
+ ```dockerfile
624
+ # Dockerfile
625
+ FROM python:3.11-slim
626
+
627
+ WORKDIR /app
628
+
629
+ COPY requirements.txt .
630
+ RUN pip install --no-cache-dir -r requirements.txt
631
+
632
+ COPY . .
633
+
634
+ EXPOSE 5000
635
+
636
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "run:app"]
637
+ ```
638
+
639
+ ### Environment Variables
640
+ ```bash
641
+ # .env
642
+ SECRET_KEY=your-secret-key-here
643
+ DATABASE_URL=sqlite:///app.db
644
+ MAIL_SERVER=smtp.gmail.com
645
+ MAIL_PORT=587
646
+ MAIL_USE_TLS=1
647
+ MAIL_USERNAME=your-email@gmail.com
648
+ MAIL_PASSWORD=your-password
649
+ ```
650
+
651
+ ## Best Practices Checklist
652
+
653
+ - [ ] Use application factory pattern
654
+ - [ ] Organize code with blueprints
655
+ - [ ] Implement proper error handling
656
+ - [ ] Use Flask-WTF for forms and CSRF protection
657
+ - [ ] Implement user authentication and authorization
658
+ - [ ] Write comprehensive tests
659
+ - [ ] Use environment variables for configuration
660
+ - [ ] Implement logging for production
661
+ - [ ] Use database migrations
662
+ - [ ] Follow REST conventions for APIs
663
+ - [ ] Implement input validation
664
+ - [ ] Use HTTPS in production
665
+ - [ ] Set up proper session management
666
+ - [ ] Implement rate limiting for APIs
667
+ - [ ] Document your API endpoints