Metadata-Version: 2.3
Name: flasklet
Version: 1.3.0
Summary: A few nice things for Flask
Author: Marcel Kröker
Author-email: Marcel Kröker <kroeker.marcel@gmail.com>
License: MIT License
         
         Copyright (c) 2026 mkrd
         
         Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
         associated documentation files (the "Software"), to deal in the Software without restriction, including
         without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
         following conditions:
         
         The above copyright notice and this permission notice shall be included in all copies or substantial
         portions of the Software.
         
         THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
         LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
         EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
         IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
         USE OR OTHER DEALINGS IN THE SOFTWARE.
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Framework :: Flask
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: flask>=3.1
Requires-Dist: jinja2>=3.1
Requires-Dist: markupsafe>=2.1
Requires-Python: >=3.13, <3.15
Description-Content-Type: text/markdown

# flasklet

A few nice things for Flask: typed configuration, flash messages with delivery
types, convention-based blueprint discovery, route imports, and template
rendering helpers.

## Quick start

Install with `uv add flasklet`. Create these files:

```text
myapp/
  __init__.py
  admin/
    __init__.py
    routes.py
  templates/
    index.html
    components.html
    partial.html
```

`myapp/__init__.py`:

```python
from enum import StrEnum

from flask import Flask
from flasklet import config, init_template_helpers, register_blueprints


class Mode(StrEnum):
	DEVELOPMENT = "development"
	PRODUCTION = "production"


class Settings(config.Config, prefix="MYAPP_"):
	secret_key: str = config.from_env()
	mode: Mode = config.from_env(default=Mode.DEVELOPMENT)


def create_app() -> Flask:
	app = Flask(__name__)
	settings = config.load(Settings)
	config.init_app(app, settings, flask_keys={"SECRET_KEY": "secret_key"})
	init_template_helpers(app)
	register_blueprints(app, auto_import_routes=True)
	return app
```

`myapp/admin/__init__.py`:

```python
from flask import Blueprint

bp = Blueprint("admin", __name__)
```

`myapp/admin/routes.py`:

```python
from flask import Response, redirect, render_template, url_for
from flasklet import config, flash_message, parse_flashed_messages

from myapp import Settings
from . import bp


@bp.get("/")
def index() -> str:
	return render_template(
		"index.html",
		mode=config.read_config(Settings).mode.value,
		messages=parse_flashed_messages(),
	)


@bp.post("/notice")
def notice() -> Response:
	flash_message("Saved", type="toast", category="success")
	return redirect(url_for("admin.index"))
```

`myapp/templates/index.html`:

```jinja
{{ render_macro("components.html", "badge", value=mode) }}
{{ render_block("components.html", "notice", text="Welcome") }}
{{ render_partial("partial.html", text="Rendered separately") }}
{% for message in messages %}
  <p data-type="{{ message.type }}" class="{{ message.category }}">{{ message.text }}</p>
{% endfor %}
<form action="{{ url_for('admin.notice') }}" method="post">
  <button type="submit">Flash a message</button>
</form>
```

`myapp/templates/components.html`:

```jinja
{% macro badge(value) %}<strong>{{ value }}</strong>{% endmacro %}
{% block notice %}<p>{{ text }}</p>{% endblock %}
```

`myapp/templates/partial.html`:

```jinja
<aside>{{ text }}</aside>
```

Run `MYAPP_SECRET_KEY=local-example-only uv run flask --app myapp:create_app run`,
then open `http://127.0.0.1:5000/`.

## Typed configuration

`config.load(Settings)` reads environment variables when called. A field's name
defaults to its uppercased attribute name, preceded by the class prefix. Pass a
name to `from_env` to override it. `load(Settings, prefix=...)` adds a prefix in
front of the class prefix.

Supported types are `str`, `int`, `float`, `bool`, `Path`, `Enum` subclasses,
`Optional[T]`, `list[T]`, and `NewType` wrappers. Enum variables match the
string representation of a member's value exactly. For example,
`MYAPP_MODE=production` selects `Mode.PRODUCTION`. An invalid value reports the
environment variable and allowed values. Booleans accept only `true` and
`false`. Lists are comma-separated; an empty variable gives an empty list.
For `Optional[T]`, an empty variable gives `None`.

A field without a default is required. Use `default_factory` for values that
must be created per instance. Mutable `list`, `dict`, and `set` defaults are
deep-copied. Derived fields are computed once, in declaration order:

```python
from pathlib import Path


class Storage(config.Config, prefix="MYAPP_"):
	data_dir: Path | None = config.from_env(default=None)
	base_dir: Path = config.derived(lambda s: (s.data_dir or Path.cwd()).resolve())
```

`config.init_app` stores the settings on the Flask app. `flask_keys` copies
selected attributes into Flask's own config, which is needed for values such as
`SECRET_KEY`. In an app context, `config.read_config(Settings)` returns the
registered instance. Outside an app context, it loads a new instance from the
current environment on every call.

## Blueprint and route discovery

`register_blueprints(app)` scans subpackages of the app package for a module
attribute named `bp`. It registers each discovered blueprint on its parent
blueprint or on the app. A subpackage without `bp` and its children are skipped.
Call it during app creation, before handling requests. The same imported
blueprints can be registered on separate app instances.

Pass `auto_import_routes=True` to import modules next to each discovered blueprint
and in its `routes/` directory before registration. This is opt-in because
importing a module runs its top-level code. The quick start uses this option.

`import_routes(__file__, __name__)` imports every Python module in the current
package directory and its `routes/` directory, in sorted order. Route
decorators run as those modules are imported. It returns `None`. Use it in a
package's `__init__.py` when you want explicit route imports instead.

## Flash messages

`flash_message(text, type="toast", category="success")` adds a delivery type to
Flask's ordinary flash category. Read messages with `parse_flashed_messages()`;
each result has `text`, `type`, and `category`. Messages from Flask's own
`flash()` are returned with `type="message"`. Use `parse_flashed_messages()`
when mixing the two functions.

## Template helpers

`init_template_helpers(app)` registers `render_block`, `render_macro`,
`render_macro_unsafe`, and `render_partial` as Jinja globals. Call it in the
app factory before rendering templates. You can also register individual
functions with `app.add_template_global`.

`render_partial` renders a complete template. `render_block` renders a block
defined in the named template; it does not resolve a block that exists only in
a parent template, and the selected block cannot use `super()`.

`render_macro` renders only the selected macro. Other top-level expressions in
the source template are not evaluated. Its keyword arguments serve two roles:
arguments with names declared by the macro are passed to it, and all arguments
are available as template context. A misspelled or unused keyword argument
has no effect, so check argument names when output is missing.

`render_macro` follows the app's Jinja autoescaping. `render_macro_unsafe` disables
autoescaping and returns an ordinary string. Use it only when the macro values
are trusted HTML. When calling it from a Jinja template, the result needs
`|safe` to avoid escaping the rendered HTML.

To include a file's source without Jinja processing, import
`IncludeRawExtension` from `flasklet`, register it with
`app.jinja_env.add_extension(IncludeRawExtension)`, and use
`{% include_raw "path/to/file" %}` in a template.
