Skip to content

Commit a093430

Browse files
committed
Initial commit
0 parents  commit a093430

File tree

7 files changed

+867
-0
lines changed

7 files changed

+867
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist
2+
.venv
3+
*.pyc
4+
__pycache__
5+
.coverage

LICENSE

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024 Maxime Bouroumeau-Fuseau
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Flask-SQLORM
2+
3+
Flask integration for [sqlorm](https://github.com/hyperflask/sqlorm)
4+
5+
## Setup
6+
7+
Install:
8+
9+
$ pip install flask-sqlorm
10+
11+
Setup:
12+
13+
```python
14+
from flask import Flask
15+
from flask_sqlorm import FlaskSQLORM
16+
17+
app = Flask()
18+
db = FlaskSQLORM(app, "sqlite://:memory:")
19+
```
20+
21+
## Usage
22+
23+
All exports from the `sqlorm` package are available from the extension instance.
24+
25+
Define some models:
26+
27+
```python
28+
class Task(db.Model):
29+
id: db.PrimaryKey[int]
30+
title: str
31+
done: bool = db.Column(default=False)
32+
```
33+
34+
Start a transaction using the db object:
35+
36+
```python
37+
with db:
38+
Task.create(title="first task")
39+
```
40+
41+
In views, the current session is available using `db.session`
42+
43+
## Configuration
44+
45+
Configure the sqlorm engine using the extension's constructor or `init_app()`. Configuration of the engine is performed using the URI method.
46+
Additional engine parameters can be provided as keyword arguments.
47+
48+
Configuration can also be provided via the app config under the `SQLORM_` namespace. Use `SQLORM_URI` to define the database URI.
49+
50+
## Additional utilities provided by Flask-SQLORM
51+
52+
Model classes have the additional methods:
53+
54+
- `find_one_or_404`: same as `find_one` but throw a 404 when no results are returned
55+
- `get_or_404`: same as `get` but throw a 404 when no results are returned
56+
57+
## Managing the schema
58+
59+
Some CLI commands are available under the *db* command group. Check out `flask db --help` for a list of subcommands.

example.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from flask import Flask, render_template_string, request, redirect, url_for
2+
from flask_sqlorm import FlaskSQLORM
3+
import logging
4+
5+
6+
app = Flask(__name__)
7+
app.logger.setLevel(logging.DEBUG)
8+
db = FlaskSQLORM(app)
9+
10+
11+
class Task(db.Model):
12+
id: db.PrimaryKey[int]
13+
title: str
14+
done: bool
15+
16+
def toggle(self):
17+
"UPDATE Task SET done = not done WHERE id = %(self.id)s RETURNING done"
18+
19+
20+
db.create_all()
21+
22+
23+
@app.route("/")
24+
def index():
25+
tasks = Task.find_all()
26+
return render_template_string("""
27+
<ul>
28+
{% for task in tasks %}
29+
<li>
30+
<form action="{{url_for("toggle", task_id=task.id)}}" method="post">
31+
<label>
32+
<input type="checkbox" {% if task.done %}checked{% endif %} onchange="event.target.form.submit()">
33+
{{task.title}}
34+
</label>
35+
</form>
36+
</li>
37+
{% endfor %}
38+
</ul>
39+
<form method="post" action="{{url_for("create")}}">
40+
<input type="text" name="title">
41+
<button type="submit">Add</button>
42+
</form>
43+
""", tasks=tasks)
44+
45+
46+
@app.post("/create")
47+
def create():
48+
with db:
49+
Task.create(title=request.form["title"], done=False)
50+
return redirect(url_for("index"))
51+
52+
53+
@app.post("/toggle/<task_id>")
54+
def toggle(task_id):
55+
with db:
56+
task = Task.get_or_404(task_id)
57+
task.toggle()
58+
return redirect(url_for("index"))
59+
60+
61+
if __name__ == "__main__":
62+
app.run(debug=True, port=6600)

flask_sqlorm.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from sqlorm import Engine, Model as _Model, get_current_session, init_db, migrate, create_all
2+
from sqlorm.engine import session_context
3+
import sqlorm
4+
import abc
5+
import os
6+
import click
7+
from flask import g, abort, has_request_context
8+
from flask.cli import AppGroup
9+
from werkzeug.local import LocalProxy
10+
11+
12+
class FlaskSQLORM:
13+
def __init__(self, app=None, *args, **kwargs):
14+
if app:
15+
self.init_app(app, *args, **kwargs)
16+
17+
def init_app(self, app, database_uri="sqlite://:memory:", **engine_kwargs):
18+
self.app = app
19+
20+
for key in dir(sqlorm):
21+
if not key.startswith("_") and not hasattr(self, key):
22+
setattr(self, key, getattr(sqlorm, key))
23+
24+
config = app.config.get_namespace("SQLORM_")
25+
database_uri = config.pop("uri", database_uri)
26+
for key, value in engine_kwargs.items():
27+
config.setdefault(key, value)
28+
config.setdefault("logger", app.logger)
29+
if database_uri.startswith("sqlite://"):
30+
config.setdefault("fine_tune", True)
31+
32+
self.engine = Engine.from_uri(database_uri, **config)
33+
self.session = LocalProxy(get_current_session)
34+
self.Model = Model.bind(self.engine)
35+
36+
@app.before_request
37+
def start_db_session():
38+
g.sqlorm_session = self.engine.make_session()
39+
session_context.push(g.sqlorm_session)
40+
41+
@app.after_request
42+
def close_db_session(response):
43+
session_context.pop()
44+
g.sqlorm_session.close()
45+
return response
46+
47+
cli = AppGroup("db", help="Commands to manage your database")
48+
49+
@cli.command()
50+
def init():
51+
"""Initializes the database, either creating tables for models or running migrations if some exists"""
52+
self.init_db()
53+
54+
@cli.command()
55+
def create_all():
56+
"""Create all tables associated to models"""
57+
self.create_all()
58+
59+
@cli.command()
60+
@click.option("--from", "from_", type=int)
61+
@click.option("--to", type=int)
62+
@click.option("--dryrun", is_flag=True)
63+
@click.option("--ignore-schema-version", is_flag=True)
64+
def migrate(from_, to, dryrun, ignore_schema_version):
65+
"""Run database migrations from the migrations folder in your app root path"""
66+
self.migrate(from_version=from_, to_version=to, dryrun=dryrun, use_schema_version=not ignore_schema_version)
67+
68+
app.cli.add_command(cli)
69+
70+
def __enter__(self):
71+
if has_request_context():
72+
return g.sqlorm_session.__enter__()
73+
return self.engine.__enter__()
74+
75+
def __exit__(self, exc_type, exc_value, exc_tb):
76+
if has_request_context():
77+
g.sqlorm_session.__exit__(exc_type, exc_value, exc_tb)
78+
else:
79+
self.engine.__exit__(exc_type, exc_value, exc_tb)
80+
81+
def create_all(self, **kwargs):
82+
kwargs.setdefault("model_registry", self.Model.__model_registry__)
83+
with self.engine:
84+
create_all(**kwargs)
85+
86+
def init_db(self, **kwargs):
87+
kwargs.setdefault("path", os.path.join(self.app.root_path, "migrations"))
88+
kwargs.setdefault("model_registry", self.Model.__model_registry__)
89+
kwargs.setdefault("logger", self.app.logger)
90+
with self.engine:
91+
init_db(**kwargs)
92+
93+
def migrate(self, **kwargs):
94+
kwargs.setdefault("path", os.path.join(self.app.root_path, "migrations"))
95+
kwargs.setdefault("logger", self.app.logger)
96+
with self.engine:
97+
migrate(**kwargs)
98+
99+
100+
class Model(_Model, abc.ABC):
101+
@classmethod
102+
def find_one_or_404(cls, *args, **kwargs):
103+
obj = cls.find_one(*args, **kwargs)
104+
if not obj:
105+
abort(404)
106+
return obj
107+
108+
@classmethod
109+
def get_or_404(cls, *args, **kwargs):
110+
obj = cls.get(*args, **kwargs)
111+
if not obj:
112+
abort(404)
113+
return obj

0 commit comments

Comments
 (0)