=========
Utilities
=========
Tet provides a small set of focused utility modules under ``tet.util`` to
handle common tasks in web applications.
Cryptographic Utilities
========================
The ``tet.util.crypt`` module provides password hashing built on top of
passlib's SHA-256 crypt scheme.
Password Hashing
----------------
Two functions are exposed: ``crypt`` to hash a password and ``verify`` to
check a plaintext password against an existing hash. Both accept either
``str`` or ``bytes`` for the password.
.. code-block:: python
from tet.util.crypt import crypt, verify
# Hash a password
hashed = crypt("my_secret_password")
# Verify a password
if verify("my_secret_password", hashed):
print("Password is correct!")
Hashing uses ``passlib.hash.sha256_crypt`` (exposed as the module-level
``password_hash``), which handles salting automatically. For SQLAlchemy
models, consider ``tet.sqlalchemy.password.UserPasswordMixin``, which
integrates this functionality directly into your model.
Base64 and Crockford Base32 Utilities
======================================
The ``tet.util.base64`` module provides two codec classes, ``Base64`` and
``CrockfordBase32``, both deriving from ``BaseCodec``. Each codec exposes
``encode``, ``decode`` and ``normalize`` classmethods, plus a
``generate_characters`` classmethod inherited from ``BaseCodec``.
Standard Base64
---------------
.. code-block:: python
from tet.util.base64 import Base64
encoded = Base64.encode(b"hello") # returns bytes
decoded = Base64.decode(encoded) # returns b"hello"
``Base64.encode`` wraps :func:`base64.b64encode` and returns ``bytes``;
``Base64.decode`` wraps :func:`base64.b64decode`. ``Base64.normalize`` is a
no-op that returns its argument unchanged. The class attributes are
``Base64.chars`` (a ``str`` of the 64-character alphabet,
``string.ascii_letters + string.digits + "+/"``), ``bits_per_char = 6`` and
``padding = True``.
Crockford Base32
----------------
Crockford's Base32 is a human-friendly encoding that avoids ambiguous
characters (``0``/``O`` and ``1``/``I``/``L``). It is case-insensitive on
decode and tolerates common transcription mistakes.
.. code-block:: python
from tet.util.base64 import CrockfordBase32
encoded = CrockfordBase32.encode(b"hello") # returns str
decoded = CrockfordBase32.decode(encoded) # returns bytes
# Ambiguous characters are normalized: O -> 0, I/L -> 1
CrockfordBase32.normalize("O1L") # "011"
``CrockfordBase32.encode`` accepts ``str`` or ``bytes`` and returns a ``str``
with any ``=`` padding stripped. ``CrockfordBase32.decode`` normalizes its
input by default (pass ``normalize=False`` to skip that), re-adds the
mandatory padding, and returns ``bytes``. ``CrockfordBase32.normalize``
translates the ambiguous characters and upper-cases the input. The class
attributes are ``CrockfordBase32.chars``
(``"0123456789ABCDEFGHJKMNPQRSTVWXYZ"``, a ``str``), ``bits_per_char = 5``
and ``padding = False``.
Generating Random Characters
----------------------------
``BaseCodec.generate_characters`` produces a random string of the requested
length using the codec's own alphabet, drawn from a cryptographically secure
source:
.. code-block:: python
from tet.util.base64 import Base64, CrockfordBase32
# 16 random Base64 characters
token = Base64.generate_characters(16)
# 26 random Crockford Base32 characters (good for IDs / tokens)
ident = CrockfordBase32.generate_characters(26)
Internally it generates ``ceil(length * bits_per_char / 8)`` random bytes
with :func:`secrets.token_bytes`, runs them through the codec's ``encode``,
and truncates the result to ``length`` characters. Because any padding only
trails the data, the truncated slice never contains padding. A non-positive
``length`` returns an empty string.
Collection Utilities
====================
The ``tet.util.collections`` module provides a single helper, ``flatten``.
Flattening Nested Iterables
---------------------------
``flatten`` is a generator that recursively flattens an arbitrarily nested
iterable. ``str`` and ``bytes`` are treated as atomic values and are never
exploded into their characters.
.. code-block:: python
from tet.util.collections import flatten
nested = [1, [2, 3, [4, 5]], 6]
list(flatten(nested)) # [1, 2, 3, 4, 5, 6]
with_strings = ["hello", ["world", ["!"]]]
list(flatten(with_strings)) # ["hello", "world", "!"]
Path Utilities
==============
The ``tet.util.path`` module provides ``caller_package``, used internally by
Tet's configuration system to determine which package called into the
framework.
Determining the Calling Package
-------------------------------
.. code-block:: python
from tet.util.path import caller_package
# The package module of the code that called the current function
pkg = caller_package()
# Skip additional modules when walking the stack
pkg = caller_package(ignored_modules=("myframework.helpers",))
``caller_package`` walks up the call stack (starting a few frames up, and
always ignoring ``tet.util.path`` itself), skipping any module whose name is
in ``ignored_modules``. When it reaches the first non-ignored module it
returns that module if it is itself a package (its ``__file__`` ends in
``__init__.py``), otherwise it returns the package that contains the module.
It builds on Pyramid's ``pyramid.path.caller_module``, which can also be
overridden via the ``caller_module`` keyword argument for testing.
Export Utilities
================
The ``tet.util.export`` module provides ``exporter``, a small helper for
maintaining a module's ``__all__`` via a decorator.
Maintaining ``__all__``
-----------------------
``exporter()`` returns a ``(decorator, list)`` tuple. Bind the list to your
module's ``__all__`` and apply the decorator to anything you want exported;
each decorated object's ``__name__`` is appended to ``__all__`` and the object
is returned unchanged.
.. code-block:: python
from tet.util.export import exporter
export, __all__ = exporter()
@export
def my_public_function():
pass
@export
class MyPublicClass:
pass
def _private_function():
pass
# __all__ == ["my_public_function", "MyPublicClass"]
Shell (pshell) Utilities
========================
The ``tet.util.pshell`` module provides snippet support for the Pyramid
``pshell`` interactive environment. A *snippet* is a ``.py`` file that defines
a ``run()`` function, which can then be invoked interactively.
Configuring Snippets
--------------------
Point the ``tet.snippets`` setting at a directory of snippet files in your INI
file:
.. code-block:: ini
[app:main]
tet.snippets = %(here)s/snippets
A snippet file ``snippets/create_user.py`` looks like:
.. code-block:: python
def run(username, email):
from myapp.models import User
session = env["request"].dbsession
user = User(username=username, email=email)
session.add(user)
return user
Using Snippets in pshell
------------------------
The ``Snippets`` factory builds a snippets-access object from an environment
mapping (the same ``env`` exposed in ``pshell``). Each ``.py`` file in the
configured directory becomes an attribute that, when called, executes that
file's ``run()`` function in the caller's globals:
.. code-block:: python
from tet.util.pshell import Snippets
snippets = Snippets(env)
# List available snippets
snippets()
# Invoke snippets/create_user.py's run() function
snippets.create_user("john", "john@example.com")
JSON Utilities
==============
The ``tet.util.json`` module provides ``js_safe_dumps`` for serializing data
to JSON that is safe to embed directly inside an HTML ``"}
js_safe_dumps(data)
# '{"name": "\\u003cscript\\u003ealert(\'xss\')\\u003c\\u002fscript\\u003e"}'
In a Tonnikala template, use ``$literal()`` so the already-escaped JSON is not
double-escaped:
.. code-block:: html
Best Practices
==============
**Use the secure codecs for tokens**
Prefer ``CrockfordBase32.generate_characters`` / ``Base64.generate_characters``
for identifiers and tokens; they draw from :mod:`secrets`.
**Hash passwords, never store them**
Use ``crypt`` and ``verify`` (or ``UserPasswordMixin``) for password storage.
**Escape JSON destined for HTML**
Use ``js_safe_dumps`` whenever JSON is embedded inside a ``