Skip to content

Commit

Permalink
write docs
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism committed Jun 7, 2024
1 parent fe710c5 commit 163e43a
Show file tree
Hide file tree
Showing 9 changed files with 520 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/_static/theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap');
12 changes: 12 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# API

Anything documented here is part of the public API that Flask-SQLAlchemy
provides, unless otherwise indicated. Anything not documented here is considered
internal or private and may change at any time.

```{eval-rst}
.. currentmodule:: flask_sqlalchemy_lite
.. autoclass:: SQLAlchemy
:members:
```
4 changes: 4 additions & 0 deletions docs/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changes

```{include} ../CHANGES.md
```
55 changes: 55 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import importlib.metadata

# Project --------------------------------------------------------------

project = "Flask-SQLAlchemy-Lite"
version = release = importlib.metadata.version("flask-sqlalchemy-lite").partition(
".dev"
)[0]

# General --------------------------------------------------------------

default_role = "code"
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"myst_parser",
]
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_preserve_defaults = True
extlinks = {
"issue": ("https://github.com/davidism/flask-sqlalchemy-lite/issues/%s", "#%s"),
"pr": ("https://github.com/davidism/flask-sqlalchemy-lite/pull/%s", "#%s"),
}
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"flask": ("https://flask.palletsprojects.com", None),
"sqlalchemy": ("https://docs.sqlalchemy.org", None),
}
myst_enable_extensions = [
"fieldlist",
]
myst_heading_anchors = 2

# HTML -----------------------------------------------------------------

html_theme = "furo"
html_static_path = ["_static"]
html_css_files = ["theme.css"]
html_copy_source = False
html_theme_options = {
"source_repository": "https://github.com/davidism/flask-sqlalchemy-lite/",
"source_branch": "main",
"source_directory": "docs/",
"light_css_variables": {
"font-stack": "'Atkinson Hyperlegible', sans-serif",
"font-stack--monospace": "'Source Code Pro', monospace",
},
}
pygments_style = "default"
pygments_style_dark = "github-dark"
html_show_copyright = False
html_use_index = False
html_domain_indices = False
113 changes: 113 additions & 0 deletions docs/engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Engines

One or more SQLAlchemy {class}`engines <sqlalchemy.engine.Engine>` can be
configured through Flask's {attr}`app.config <flask.Flask.config>`. The engines
are created when {meth}`.SQLAlchemy.init_app` is called, changing config after
that will have no effect. Both sync and async engines can be configured.


## Flask Config

```{currentmodule} flask_sqlalchemy_lite
```

```{data} SQLALCHEMY_ENGINES
A dictionary defining sync engine configurations. Each key is a name for an
engine, used to refer to them later. Each value is the engine configuration.
If the value is a dict, it consists of keyword arguments to be passed to
{func}`sqlalchemy.create_engine`. The `'url'` key is required; it can be a
connection string (`dialect://user:pass@host:port/name?args`), a
{class}`sqlalchemy.engine.URL` instance, or a dict representing keyword
arguments to pass to {meth}`sqlalchemy.engine.URL.create`.
As a shortcut, if you only need to specify the URL and no other arguments, the
value can be a connection string or `URL` instance.
```

```{data} SQLALCHEMY_ASYNC_ENGINES
The same as {data}`SQLALCHEMY_ENGINES`, but for async engine configurations.
```

### URL Examples

The following configurations are all equivalent.

```python
SQLALCHEMY_ENGINES = {
"default": "sqlite:///default.sqlite"
}
```

```python
from sqlalchemy import URL
SQLALCHEMY_ENGINES = {
"default": URL.create("sqlite", database="default.sqlite")
}
```

```python
SQLALCHEMY_ENGINES = {
"default": {"url": "sqlite:///default.sqlite"}
}
```

```python
from sqlalchemy import URL
SQLALCHEMY_ENGINES = {
"default": {"url": URL.create("sqlite", database="default.sqlite")}
}
```

```python
SQLALCHEMY_ENGINES = {
"default": {"url": {"drivername": "sqlite", "database": "default.sqlite"}}
}
```


## Default Options

Default engine options can be passed as the `engine_options` parameter when
creating the {class}`.SQLAlchemy` instance. The config for each engine will be
merged with these default options, overriding any shared keys. This applies to
both sync and async engines. You can use specific config if you need different
options for each.


### SQLite Defaults

A relative database path will be relative to the app's
{attr}`~flask.Flask.instance_path` instead of the current directory. The
instance folder will be created if it does not exist.

When using a memory database (no path, or `:memory:`), a static pool will be
used, and `check_same_thread=False` will be passed. This allows multiple workers
to share the database.


### MySQL Defaults

When using a queue pool (default), `pool_recycle` is set to 7200 seconds
(2 hours), forcing SQLAlchemy to reconnect before MySQL would discard the idle
connection.

The connection charset is set to `utf8mb4`.


## The Default Engine and Bind

The `"default"` key is special, and will be used for {attr}`.SQLAlchemy.engine`
and as the default bind for {attr}`.SQLAlchemy.sessionmaker`. By default, it is
an error not to configure it for one of sync or async engines.


## Custom Engines

You can ignore the Flask config altogether and create engines yourself. In that
case, you pass `require_default_engine=False` when creating the extension to
ignore the check for default config. Adding custom engines to the
{attr}`.SQLAlchemy.engines` map will make them accessible through the extension,
but that's not required either. You will want to call
`db.sessionmaker.configure(bind=..., binds=...)` to set up these custom engines
if you plan to use the provided session management though.
51 changes: 51 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Flask-SQLAlchemy-Lite

This [Flask]/[Quart] extension manages [SQLAlchemy] engines and sessions as part
of your web application. Engines can be configured through Flask config, and
sessions are manages and cleaned up as part of the app/request context.
SQLAlchemy's async capabilities are supported as well, and both sync and async
can be configured and used at the same time.

[Flask]: https://flask.palletsprojects.com
[Quart]: https://quart.palletsprojects.com
[SQLAlchemy]: https://www.sqlalchemy.org

Install it from PyPI using an installer such as pip:

```
$ pip install Flask-SQLAlchemy-Lite
```

This is intended to be a replacement for the [Flask-SQLAlchemy] extension. It
provides the same `db.engine` and `db.session` interface. However, this
extension avoids pretty much every other thing the former extension managed. It
does not create the base model, table class, or metadata itself. It does not
implement a custom bind system. It does not provide automatic table naming for
models. It does not provide query recording, pagination, query methods, etc.

[Flask-SQLAlchemy]: https://flask-sqlalchemy.palletsprojects.com

This extension tries to do as little as possible and as close to plain
SQLAlchemy as possible. You define your base model using whatever SQLAlchemy
pattern you want, old or modern. You use SQLAlchemy's `session.binds` API for
mapping different models to different engines. You import all names from
SQLAlchemy directly, rather than using `db.Mapped`, `db.select`, etc. Sessions
are tied directly to request lifetime, but can also be created and managed
directly, and do not use the `scoped_session` interface.

These docs cover how the extension works, _not_ how to use SQLAlchemy. Read the
[SQLAlchemy docs], which include a comprehensive tutorial, to learn how to use
SQLAlchemy.

[SQLAlchemy docs]: https://docs.sqlalchemy.org

```{toctree}
:hidden:
start
engine
session
api
changes
license
```
5 changes: 5 additions & 0 deletions docs/license.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# MIT License

```{literalinclude} ../LICENSE.txt
:language: text
```
76 changes: 76 additions & 0 deletions docs/session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Sessions

A SQLAlchemy {class}`~sqlalchemy.orm.sessionmaker` is created when
{meth}`.SQLAlchemy.init_app` is called. Both sync and async sessionmakers
are created regardless of if any sync or async engines are defined.


## Default Options

Default session options can be passed as the `session_options` parameter when
creating the {class}`.SQLAlchemy` instance. This applies to both sync and async
sessions. You can call each sessionmaker's `configure` method if you need
different options for each.


## Session Management

Most use cases will use one session, and tie it to the lifetime of each request.
Use {attr}`db.session <SQLAlchemy.session>` for this. It will return the same
session throughout a request, then close it when the request ends. SQLAlchemy
will rollback any uncomitted state in the session when it is closed.

You can also create other sessions besides the default. Calling
{meth}`db.get_session(name)` will create separate sessions that are also closed
at the end of the request.

The sessions are closed when the application context is torn down. This happens
for each request, but also at the end of CLI commands, and for manual
`with app.app_context()` blocks.


### Manual Sessions

You can also use {attr}`db.sessionmaker <SQLAlchemy.sessionmaker>` directly to
create sessions. These will not be closed automatically at the end of requests,
so you'll need to manage them manually. An easy way to do that is using a `with`
block.

```python
with db.sessionmaker() as session:
...
```


### Async

SQLAlchemy warns that the async sessions it provides are _not_ safe to be used
across concurrent tasks. For example, the same session should not be passed to
multiple tasks when using `asyncio.gather`. Either use
{meth}`db.get_async_session(name) <SQLAlchemy.get_async_session>` with a unique
name for each task, or use {attr}`db.async_sessionmaker` to manage sessions
and their lifetime manually. The latter is what SQLAlchemy recommends.


## Multiple Binds

If the `"default"` engine key is defined when initializing the extension, it
will be set as the default bind for sessions. This is optional, but if you don't
configure it up front, you'll want to call `db.sessionmaker.configure(bind=...)`
later to set the default bind, or otherwise specify a bind for each query.

SQLAlchemy supports using different engines when querying different tables or
models. This requires specifying a mapping from a model, base class, or table to
an engine object. When using the extension, you can set this up generically
in `session_options` by mapping to names instead of engine objects. During
initialization, the extension will substitute each name for the configured
engine. You can also call `db.sessionmaker.configure(binds=...)` after the fact
and pass the engines using {meth}`~.SQLAlchemy.get_engine` yourself.

```python
db = SQLAlchemy(session_options={"binds": {
User: "auth",
Role: "auth",
ExternalBase: "external",
}})
```
Loading

0 comments on commit 163e43a

Please sign in to comment.