Utility Modules

This page lists utility modules that live directly in the ferrosoft package. They support non-functional requirements such as configuration. For most purposes, prefer modules from ferrobase.apps, where new modules should also be created.

Ferrosoft.config module

YAML-based Django settings loader with environment merging and variable substitution.

This module is the core of Ferrosoft’s configuration system. Rather than keeping settings in a Python file, settings are stored in one or more YAML files whose structure mirrors Django’s settings namespace. The loading pipeline has three stages:

  1. Merge files — one or more YAML files are read and merged with the | operator (right-hand file wins on key collision).

  2. Select environment — the merged YAML is expected to have a "default" top-level key and one key per environment (e.g. "development", "production"). The active environment’s block is merged on top of "default".

  3. Substitute variables — string values that start with "@ " are treated as str.format() templates and rendered with two namespaces:

    • envos.environ, so "@ {env[SECRET_KEY]}" reads the SECRET_KEY environment variable.

    • settings — the fully merged (but not yet substituted) settings dict, so "@ {settings[BASE_DIR]}/media" derives a path from another setting.

    If a referenced key is missing, a MissingOption sentinel is stored instead of raising immediately. The error is deferred until the setting value is actually used (e.g. converted to str), so that optional settings that are never accessed do not cause startup failures.

Environment variables

FERROSOFT_ENV

Selects the environment block merged on top of default. Defaults to "development".

FERROSOFT_CONFIG

Colon-separated list of YAML file paths to load and merge. Defaults to "settings.yml".

Typical usage

In a Django settings module:

from ferrosoft.config import load_django
load_django(globals())

This writes every resolved key from the YAML configuration directly into the calling module’s global namespace, making them available to Django as settings.

ferrosoft.config.CONFIG_FILE_DEFAULT = 'settings.yml'

Default config file path used when CONFIG_FILE_ENV is not set.

ferrosoft.config.CONFIG_FILE_ENV = 'FERROSOFT_CONFIG'

Name of the environment variable that lists YAML config file paths.

exception ferrosoft.config.ConfigError[source]

Bases: RuntimeError

Raised for unrecoverable configuration problems.

Examples include a YAML file that does not contain a mapping, or a required Django setting that is absent from the resolved configuration.

ferrosoft.config.DEFAULT_ENV_NAME = 'default'

Top-level YAML key whose block is used as the base for all environments.

ferrosoft.config.ENV_DEFAULT = 'development'

Default value for ENV_ENV when the variable is not set.

ferrosoft.config.ENV_ENV = 'FERROSOFT_ENV'

Name of the environment variable that selects the active config environment.

class ferrosoft.config.MissingOption(option_keys, originator)[source]

Bases: object

Deferred error sentinel for unresolvable variable references.

When a "@ {env[VAR]}" or "@ {settings[KEY]}" substitution fails because the referenced name is missing, a MissingOption instance is stored in its place rather than raising immediately. The ConfigError is only raised when the sentinel’s __str__ method is called — i.e. when the setting value is actually used as a string.

This design allows the full configuration to be loaded without aborting on optional or environment-specific settings that happen not to be set in the current environment.

Parameters:
  • option_keys (Iterable[str]) – The sequence of YAML key names that form the path to the setting that could not be resolved (used in the error message).

  • originator (Exception) – The KeyError raised during str.format() that triggered the deferred error.

ferrosoft.config.load_django(settings_module)[source]

Populate a Django settings module from the resolved YAML configuration.

Reads all YAML configuration files, selects and merges the active environment block, performs variable substitution, and writes every resulting key directly into settings_module.

Intended to be called from a Django settings file with globals() as the argument:

from ferrosoft.config import load_django
load_django(globals())
Parameters:

settings_module (dict) – The globals() dict of the calling Django settings module. Each resolved configuration key is written as a top-level entry.

Return type:

None

ferrosoft.config.merge_config_environment(unmerged_config)[source]

Select and merge the active environment block from the raw config dict.

Looks for a "default" top-level key and merges the block identified by the FERROSOFT_ENV environment variable on top of it (right-wins). If FERROSOFT_ENV is not set, falls back to "development".

Parameters:

unmerged_config (dict) – The raw configuration dictionary as returned by read_config_files_merged(), containing one key per environment plus "default".

Return type:

dict

Returns:

A flat settings dictionary with environment-specific overrides applied. If "default" is absent the entire unmerged_config is returned as-is with a warning. If the active environment key is absent, the "default" block is returned with a warning.

ferrosoft.config.read_config_file(file_name)[source]

Read and parse a single YAML configuration file.

Parameters:

file_name (str) – Path to the YAML file to read.

Return type:

dict

Returns:

The parsed YAML content as a Python dictionary, or an empty dict if the file does not exist.

Raises:

ConfigError – If the file exists but its content is not a YAML mapping.

ferrosoft.config.read_config_files()[source]

Execute the full configuration pipeline and return resolved settings.

Combines read_config_files_merged(), merge_config_environment(), and substitute() into a single call.

Return type:

dict

Returns:

A flat dictionary of Django settings with all variable references resolved. Values that could not be resolved are represented by MissingOption sentinels.

ferrosoft.config.read_config_files_merged()[source]

Read and merge all YAML configuration files listed in FERROSOFT_CONFIG.

Splits the FERROSOFT_CONFIG environment variable on ":" and loads each path in order. Files are merged with the | operator so that keys from later files override keys from earlier files. The resulting dict still contains the top-level environment keys ("default", "development", etc.) — environment selection happens in merge_config_environment().

Return type:

dict

Returns:

A merged dictionary of all YAML files, keyed by environment name at the top level.

Raises:

ConfigError – If any loaded file’s top-level value is not a mapping.

ferrosoft.config.require_settings(*keys)[source]

Fetch named settings from the active Django settings module.

Validates that each requested key is present and returns all values in a single dictionary. Intended for use by services that need a small, named subset of Django settings without importing the full settings module.

Parameters:

*keys (str) – One or more Django setting names to retrieve.

Return type:

dict

Returns:

A dictionary mapping each requested key to its current value.

Raises:

ConfigError – If any of the requested keys is absent from Django settings.

Example

from ferrosoft.config import require_settings

cfg = require_settings("VICTORIA_BASE_URL", "MEDIA_ROOT")
base_url = cfg["VICTORIA_BASE_URL"]
ferrosoft.config.substitute(settings, root_settings, key_trail=None)[source]

Recursively resolve variable references in a settings dictionary.

Iterates over every key in settings and calls substitute_value() on each value. Nested dicts are recursed; the key_trail is extended at each level so that error messages can report the full dotted path to the problematic setting.

Parameters:
  • settings (dict) – The dictionary of settings to process.

  • root_settings (dict) – The top-level (unsubstituted) settings dictionary, passed down unchanged so that {settings[KEY]} references can look up any top-level key.

  • key_trail (list[str] | None) – Accumulated list of key names representing the path from the root to the current settings dict. Used in MissingOption error messages. Pass None (or omit) at the top-level call.

Return type:

dict

Returns:

A new dictionary with the same keys as settings and all "@ ..." string values replaced by their resolved counterparts.

ferrosoft.config.substitute_value(value, root_settings, key_trail)[source]

Resolve a single configuration value, handling the substitution DSL.

String values that begin with "@ " are treated as str.format() templates rendered with two keyword namespaces:

  • envos.environ

  • settings — the top-level root_settings dictionary

Examples of valid substitution strings:

"@ {env[DATABASE_URL]}"
"@ {settings[BASE_DIR]}/media"
"@ {env[SECRET_KEY]}"

Dictionaries are recursed via substitute(). List elements are substituted individually. All other types (int, bool, None, etc.) are returned unchanged.

If a {env[...]} or {settings[...]} reference cannot be resolved because the key is absent, a MissingOption sentinel is returned instead of raising immediately.

Parameters:
  • value – The raw configuration value to process.

  • root_settings (dict) – The top-level settings dictionary used to resolve {settings[KEY]} references.

  • key_trail (list[str]) – Path of keys from the root to the current value, used to construct informative MissingOption error messages.

Returns:

The resolved value, a MissingOption sentinel, or the original value unchanged if no substitution applies.

ferrosoft.decimal module

ferrosoft.importutils module

Restricted dynamic import utility for loading objects from first-party app modules.

This module provides a single entry-point, import_object(), for dynamically importing Python objects (functions, classes, etc.) by their fully-qualified name. It enforces a namespace restriction so that only objects whose module path starts with "ferrosoft.apps." can be loaded, preventing an attacker-controlled or misconfigured string from importing arbitrary Python code.

Typical use case: the task queue and the global updater system store callable references as dotted strings in configuration or the database. import_object() resolves those strings at runtime without exposing a general-purpose code-execution surface.

exception ferrosoft.importutils.Error[source]

Bases: Exception

Base exception class for import utility errors.

exception ferrosoft.importutils.ForbiddenModule[source]

Bases: Error

Raised when the requested module path fails the namespace check.

This is a security guard: only modules whose fully-qualified name starts with _REQUIRED_MODULE ("ferrosoft.apps.") may be imported dynamically. Any attempt to import from the standard library, third-party packages, or other internal namespaces raises this exception.

Parameters:

qualified_name – The fully-qualified object name that was rejected.

ferrosoft.importutils.import_object(qualified_name)[source]

Import and return a named object from a first-party application module.

Splits qualified_name on the last "." to obtain the module path and the attribute name, imports the module via importlib.import_module(), and returns the named attribute.

The namespace guard enforces that qualified_name starts with "ferrosoft.apps." before any import is attempted, limiting the attack surface of dynamic import to first-party application code only.

Parameters:

qualified_name (str) – Fully-qualified name of the object to import, e.g. "ferrosoft.apps.emiflow.services.catalog.build_catalog". Must start with "ferrosoft.apps.".

Returns:

The imported object, or None if the module does not expose an attribute with the given name.

Raises:

ForbiddenModule – If qualified_name does not start with "ferrosoft.apps.".

Example

from ferrosoft.importutils import import_object

updater_fn = import_object(
    "ferrosoft.apps.ferrobase.services.update.run_global_update"
)
if updater_fn is not None:
    updater_fn()

ferrosoft.strparse module

Tolerant string-to-type parsers for processing external and user-supplied data.

All parsers in this module are intentionally lenient:

  • The parse_*_noexcept functions return None instead of raising on invalid input, making them safe to use with data from CSV files, query parameters, or any other source where the value may be absent or malformed.

  • parse_boolean() treats every unrecognised string as True, mirroring the JavaScript truthiness model and the convention used by HTML checkboxes (where a submitted field value always means “checked”).

The _UNTRUTHY_VALUES list deliberately covers representations from multiple languages and data formats (Python’s "none", JavaScript’s "null" and "undefined", SQL’s implicit empty string, numeric zeros) so that the same parser can be used across heterogeneous data sources.

ferrosoft.strparse.get_untruthy_values()[source]

Return the list of string values that parse_boolean() treats as False.

Callers can use this to display or validate the set of accepted falsy representations without hard-coding the list themselves.

Return type:

list[str]

Returns:

A list of lowercase strings considered untruthy by parse_boolean().

ferrosoft.strparse.parse_boolean(value)[source]

Parse a string as a boolean, treating all unrecognised values as True.

Converts value to lowercase and checks it against _UNTRUTHY_VALUES. Any string not in that list — including "yes", "on", "1", "true", or any arbitrary non-empty string — is considered True.

This permissive convention is intentional: it matches the behaviour of HTML form checkboxes (where the presence of any submitted value means “enabled”) and JavaScript’s general truthiness rules.

Parameters:

value (str) – The raw string to interpret as a boolean.

Return type:

bool

Returns:

False if value.lower() is in _UNTRUTHY_VALUES, True otherwise.

Example

parse_boolean("true")    # True
parse_boolean("yes")     # True
parse_boolean("1")       # True
parse_boolean("false")   # False
parse_boolean("0")       # False
parse_boolean("none")    # False
parse_boolean("")        # False
ferrosoft.strparse.parse_decimal_noexcept(value)[source]

Parse a string as a Decimal, returning None on failure.

Parameters:

value (str) – The raw string to parse.

Return type:

Decimal | None

Returns:

A Decimal on success, or None if value cannot be converted (e.g. it is empty, contains letters, or is otherwise not a valid decimal literal).

ferrosoft.strparse.parse_int_noexcept(value)[source]

Parse a string as an integer, returning None on failure or absent input.

Parameters:

value (str | None) – The raw string to parse, or None.

Return type:

int | None

Returns:

An int on success, or None if value is None or cannot be converted to an integer.

ferrosoft.strparse.parse_uuid_noexcept(value)[source]

Parse a string as a UUID, returning None on failure or absent input.

Accepts any UUID format recognised by uuid.UUID (with or without hyphens, upper or lower case).

Parameters:

value (str | None) – The raw string to parse, or None.

Return type:

UUID | None

Returns:

A UUID on success, or None if value is None or not a valid UUID representation.

ferrosoft.addresses module

Warning

Prefer ferrosoft.apps.ferromaps package over this module! It is only kept around for compatibility, though these days it should be dead code.

Address data types and geocoding/routing services.

Deprecated since version This: module is superseded by ferrosoft.apps.ferromaps.services.addresses, which should be used for all new code. The replacement offers several improvements:

class ferrosoft.addresses.Address(first_name: str, last_name: str, salutation: str, company: str, additional_lines: str, street: str, house_no: str, city: str, postal_code: str, country: str)[source]

Bases: NamedTuple

Structured postal address as an immutable value object.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.Address instead. The replacement is a mutable @dataclass and is accepted directly by the new service’s geocode method.

All fields default to an empty string when constructed via from_dict().

additional_lines: str

Alias for field number 4

city: str

Alias for field number 7

company: str

Alias for field number 3

country: str

Alias for field number 9

first_name: str

Alias for field number 0

format_lines(join=None)[source]

Format the structured address into display lines or a single string.

Omits empty fields. The output order is: salutation / first name / last name, company, additional lines (split on "\n"), street + house number, postal code + city, country.

Parameters:

join (str) – If a string, all non-empty lines are joined with this separator and returned as a single string. If None, a list of individual lines is returned.

Return type:

str | List[str]

Returns:

A joined string when join is a str, otherwise a list of address lines.

classmethod from_dict(data)[source]

Construct an Address from a dictionary, defaulting missing keys to "".

Parameters:

data (dict) – Dictionary that may contain any subset of the field names as keys.

Return type:

Address

Returns:

A fully populated Address with empty strings for absent fields.

geocode()[source]

Geocode this address using the default AddressRouterService.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.Address.geocode() on the replacement Address class instead.

Return type:

Location

Returns:

The Location resolved by AddressRouterGeoapify.

house_no: str

Alias for field number 6

last_name: str

Alias for field number 1

postal_code: str

Alias for field number 8

salutation: str

Alias for field number 2

street: str

Alias for field number 5

class ferrosoft.addresses.AddressRouterFake(distances)[source]

Bases: AddressRouterService

In-memory stub implementation for use in tests.

Geocoding always returns a fixed dummy location. Distances are consumed from an iterator supplied at construction time, returning None once the iterator is exhausted.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.AddressRouterFake instead.

Parameters:

distances (List[Tuple[int, str]]) – Sequence of (value, unit) tuples to return from successive distance() calls.

distance(route)[source]

Calculate the total distance of a route.

Parameters:

route (Route) – The Route to measure.

Return type:

Distance | None

Returns:

A Distance, or None if the service could not determine a distance.

geocode(address)[source]

Geocode a free-form address string to a Location.

Parameters:

address (str) – Free-form address string to resolve.

Return type:

Location

Returns:

The resolved Location.

class ferrosoft.addresses.AddressRouterGeoapify(config)[source]

Bases: AddressRouterService

Geocoding and routing backed by the Geoapify REST API.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.AddressRouterGeoapify instead. The replacement validates HTTP response status codes, falls back to city-level geocoding on missing results, uses Decimal for distances, and raises typed exceptions.

Configuration is read from settings.FERROBASE_MAP_CONFIG via Config.

class Config(base_url: <module 'string' from '/home/ap/.local/share/uv/python/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/string/__init__.py'>, api_key: <module 'string' from '/home/ap/.local/share/uv/python/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/string/__init__.py'>)[source]

Bases: NamedTuple

Geoapify connection configuration.

Parameters:
  • base_url (<module ‘string’ from ‘/home/ap/.local/share/uv/python/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/string/__init__.py’>) – Base URL of the Geoapify API (e.g. "https://api.geoapify.com").

  • api_key (<module ‘string’ from ‘/home/ap/.local/share/uv/python/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/string/__init__.py’>) – Geoapify API key.

api_key: <module 'string' from '/home/ap/.local/share/uv/python/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/string/__init__.py'>

Alias for field number 1

base_url: <module 'string' from '/home/ap/.local/share/uv/python/cpython-3.14.5-linux-x86_64-gnu/lib/python3.14/string/__init__.py'>

Alias for field number 0

classmethod from_django_settings()[source]

Read configuration from settings.FERROBASE_MAP_CONFIG.

Returns:

A populated Config instance.

classmethod default()[source]

Return a new instance configured from Django settings.

distance(route)[source]

Calculate the road distance of a route via the Geoapify routing API.

Uses heavy_truck routing mode. Returns None if the API response contains no feature results.

Parameters:

route (Route) – The Route whose legs to measure.

Return type:

Distance | None

Returns:

A Distance in metres, or None if no route was found.

Raises:
  • requests.HTTPError – On a non-2xx HTTP response.

  • ValueError – If the API returns a non-metric distance unit.

geocode(address)[source]

Resolve a free-form address string via the Geoapify geocoding API.

Parameters:

address (str) – Free-form address string to geocode.

Return type:

Location

Returns:

A Location built from the first Geoapify result.

Raises:
  • requests.HTTPError – On a non-2xx HTTP response.

  • RuntimeError – If the API response is malformed or returns no results.

class ferrosoft.addresses.AddressRouterService[source]

Bases: ABC

Abstract geocoding and routing service.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.AddressRouterService instead, which separates geocoding and routing into distinct abstract base classes and supports pluggable implementations via FERROBASE_ADDRESS_ROUTER_CLASS.

The default() class method always returns an AddressRouterGeoapify instance.

classmethod default()[source]

Return the default service instance (always AddressRouterGeoapify).

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.AddressRouterService.get_instance() instead, which selects the implementation from Django settings.

abstractmethod distance(route)[source]

Calculate the total distance of a route.

Parameters:

route (Route) – The Route to measure.

Return type:

Distance | None

Returns:

A Distance, or None if the service could not determine a distance.

abstractmethod geocode(address)[source]

Geocode a free-form address string to a Location.

Parameters:

address (str) – Free-form address string to resolve.

Return type:

Location

Returns:

The resolved Location.

class ferrosoft.addresses.Coordinates(lat: float, lon: float)[source]

Bases: NamedTuple

Geographic coordinates as a (latitude, longitude) pair using float.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.Coordinates instead, which stores values as Decimal to preserve precision in downstream calculations.

lat: float

Alias for field number 0

lon: float

Alias for field number 1

class ferrosoft.addresses.Distance(value: float, unit: str)[source]

Bases: NamedTuple

A measured distance with a unit string.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.Distance instead, which stores the value as Decimal.

unit: str

Alias for field number 1

value: float

Alias for field number 0

class ferrosoft.addresses.Location(coords: Coordinates, name: str, address: Address)[source]

Bases: NamedTuple

A resolved geographic location combining coordinates, a display name, and an address.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.Location instead.

address: Address

Alias for field number 2

coords: Coordinates

Alias for field number 0

name: str

Alias for field number 1

class ferrosoft.addresses.Route(legs: List[RouteLeg])[source]

Bases: NamedTuple

An ordered sequence of route legs.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.Route instead.

legs: List[RouteLeg]

Alias for field number 0

class ferrosoft.addresses.RouteLeg(start: Coordinates, destination: Coordinates)[source]

Bases: NamedTuple

A single leg of a route, defined by start and destination coordinates.

Deprecated since version Use: ferrosoft.apps.ferromaps.services.addresses.RouteLeg instead.

destination: Coordinates

Alias for field number 1

start: Coordinates

Alias for field number 0

ferrosoft.report module

PDF report generation via the WeasyPrint microservice.

This module provides a two-stage pipeline for producing reports:

  1. Template rendering — a Django template is rendered to HTML with a standard context that includes the current UTC timestamp, a title, and an embeddability flag.

  2. PDF conversion — the rendered HTML is POSTed to a running WeasyPrint HTTP microservice, which returns the PDF bytes.

The WeasyPrint service is configured via the WEASYPRINT_URL Django setting and is typically started alongside the application in the development environment (make local). In production it is declared as a dependency in compose-production.yml.

Callers can bypass PDF generation entirely by passing as_html=True (to get rendered HTML bytes back) or as_text=True (to get the raw HTML string before encoding), which is useful for preview modes or debugging.

Example:

from ferrosoft.report import generate_report

pdf_bytes = generate_report(
    "emiflow/report.html",
    context={"shipment": shipment},
    title="Emissions Report",
)

# HTML preview (embeddable in an iframe)
html_bytes = generate_report(
    "emiflow/report.html",
    context={"shipment": shipment},
    title="Emissions Report",
    as_html=True,
)
exception ferrosoft.report.ReportError[source]

Bases: RuntimeError

Raised when the WeasyPrint service returns a non-200 HTTP response.

Parameters:

message – Human-readable description including the HTTP status code.

ferrosoft.report.generate_report(template, context=None, title=None, as_html=None, as_text=None)[source]

Render a Django template and optionally convert it to PDF.

The template is rendered with a base context merged with any caller-supplied context. The base context always includes:

  • timestamp — current UTC datetime from now_utc().

  • title — passed through from the title argument.

  • report_embeddable — set to the value of as_html so templates can adjust their output (e.g. omit <html> / <body> tags when embedded in an iframe).

Parameters:
  • template (str | Template) – Either a template name string (resolved via django.template.loader.get_template()) or an already-loaded Template instance.

  • context (dict | None) – Additional template variables merged on top of the base context. Caller-supplied keys override the base context keys.

  • title (str | None) – Document title injected into the template as title.

  • as_html (bool | None) – If True, return the rendered HTML encoded as bytes without sending it to WeasyPrint. The report_embeddable template variable is also set to True.

  • as_text (bool | None) – If True, return the rendered HTML as a plain str without encoding or PDF conversion. Takes precedence over as_html for the encoding step.

Return type:

bytes | str

Returns:

PDF bytes when neither as_html nor as_text is set. Encoded HTML bytes when as_html=True. Raw HTML string when as_text=True.

ferrosoft.report.generate_report_pdf(html_content)[source]

Convert an HTML document to PDF by calling the WeasyPrint microservice.

The HTML is POSTed as text/html to the URL configured in settings.WEASYPRINT_URL. A SHA-256 digest of the content is sent as the X-Trace-Id header so that WeasyPrint logs can be correlated with application logs.

Parameters:

html_content (bytes) – The HTML document as UTF-8 encoded bytes.

Return type:

bytes

Returns:

The PDF document as raw bytes returned by WeasyPrint.

Raises:
  • ValueError – If WEASYPRINT_URL is not present in Django settings.

  • ReportError – If WeasyPrint returns any HTTP status other than 200.

ferrosoft.request module

Query string parsing with PHP/Rails/jQuery bracket-notation support.

Deprecated since version 2.0.0: Use of this module is strongly discouraged. New code should use Django’s built-in form machinery (Form, ModelForm) together with standard QueryDict access patterns. Django forms provide validation, error reporting, rendering, and CSRF protection that this module does not.

This module was written to handle query strings that use the bracket-notation convention popularised by PHP, Ruby on Rails, and jQuery’s $.param():

  • foo=bar{"foo": "bar"} (scalar)

  • items[]=1&items[]=2{"items": ["1", "2"]} (list)

  • addr[city]=Berlin&addr[zip]=10115{"addr": {"city": "Berlin", "zip": "10115"}} (dict)

It also provides is_htmx_request() for detecting HTMX driven requests, and sanitize_uuid() / sanitize_decimal() converters used by the Form class.

The Form class mimics the django.forms.Form API at a surface level (is_valid() / cleaned_data) but lacks Django’s validation pipeline, localisation support, and widget rendering.

Migration guide

Replace Form subclasses with Form subclasses and restructure_query() calls with direct request.GET/request.POST access or a DRF serializer. Replace is_htmx_request() with django_htmx or an inline header check.

class ferrosoft.request.DecimalField(**kwargs)[source]

Bases: FormField

A FormField that sanitizes values with sanitize_decimal().

Deprecated since version Use: DecimalField instead.

class ferrosoft.request.Form(query)[source]

Bases: object

Minimal Django-form-like class for parsing bracket-notation query strings.

Deprecated since version Use: Form instead.

At construction time, all FormField class attributes are discovered by reflection. Their sanitizer callables are collected into a map that is passed to restructure_query() when is_valid() is called.

Parameters:

query (QueryDict) – The QueryDict to parse (typically request.GET or request.POST).

is_valid()[source]

Parse and sanitize the query string.

Populates cleaned_data on success.

Return type:

bool

Returns:

True if restructure_query() succeeds; False if it raises ValueError (malformed key or type conflict).

class ferrosoft.request.FormField(**kwargs)[source]

Bases: object

Descriptor that declares a field on a Form subclass.

Stores arbitrary keyword arguments and exposes them via __getitem__. The special key "sanitizer" is allowed to be absent (returns None).

Deprecated since version Use: Field and its subclasses instead.

Parameters:

**kwargs – Field configuration, typically sanitizer.

class ferrosoft.request.IntegerField(**kwargs)[source]

Bases: DecimalField

A DecimalField alias for integer inputs.

Deprecated since version Use: IntegerField instead.

class ferrosoft.request.QueryFieldType(*values)[source]

Bases: Enum

Classifies a bracket-notation query string key by its expected value shape.

Determines how values are aggregated when restructure_query() processes a QueryDict.

SCALAR

A plain key with no brackets. Only a single value is stored. Even if the key contains brackets that make it look subscriptable the field type is still SCALAR when no brackets are present.

LIST

A key ending in []. All values for this key are collected into a list.

DICT

A key with a non-empty subscript, e.g. addr[city]. Values are stored in a dict keyed by the subscript string.

DICT = 3
LIST = 2
SCALAR = 1
class ferrosoft.request.UUIDField(**kwargs)[source]

Bases: FormField

A FormField that sanitizes values with sanitize_uuid().

Deprecated since version Use: UUIDField instead.

Parameters:

**kwargs – Passed through to FormField.

ferrosoft.request.is_htmx_request(request)[source]

Return True if the request was issued by the HTMX JavaScript library.

HTMX sets the HX-Request: true header on every request it makes. This can be used to return partial HTML fragments instead of full pages.

Parameters:

request (HttpRequest) – The incoming Django HTTP request.

Return type:

bool

Returns:

True if the HX-Request header is present and equals "true"; False otherwise.

ferrosoft.request.restructure_query(query, sanitizers=None)[source]

Convert a QueryDict to a nested Python structure.

Follows the PHP / Ruby on Rails / jQuery $.param() bracket-notation convention (lightly):

  • Plain keys (no brackets) are stored as scalars.

  • Keys ending with [] are collected into a list.

  • Keys with a named subscript (e.g. addr[city]) are stored in a dict.

If a sanitizer is provided for a root key, it is applied to every value for that key before storage.

Parameters:
  • query (QueryDict) – The QueryDict to parse (e.g. request.GET).

  • sanitizers (Mapping[str, Callable[[str], Any]]) – Optional mapping from root key name to a callable that converts a raw string value to the desired type. Keys without a sanitizer entry are stored as raw strings.

Return type:

Mapping[str, Any]

Returns:

A dict with scalar, list, or dict values depending on the key notation used.

Raises:

ValueError – If a key does not match the expected pattern, if a list field carries more than one value while typed as scalar, or if the same root key is used with conflicting types (e.g. first as a scalar then as a dict).

Example:

# Query string: foo=bar&items[]=1&items[]=2&addr[city]=Berlin
result = restructure_query(request.GET)
# {"foo": "bar", "items": ["1", "2"], "addr": {"city": "Berlin"}}
ferrosoft.request.sanitize_decimal(input)[source]

Parse a Decimal from a string, treating empty input as zero.

Parameters:

input (str) – A numeric string, or "" which is interpreted as "0".

Return type:

Decimal

Returns:

The parsed Decimal.

ferrosoft.request.sanitize_list(input, sanitizer)[source]

Apply a sanitizer function to every element of a list.

Parameters:
  • input (List[str]) – List of raw string values.

  • sanitizer – A callable that converts a single string to the desired type.

Returns:

A new list with sanitizer applied to each element.

ferrosoft.request.sanitize_uuid(input)[source]

Parse a UUID from a string, raising on invalid input.

Parameters:

input (str) – A string in any UUID format accepted by uuid.UUID.

Return type:

UUID

Returns:

The parsed uuid.UUID.

Raises:

ValueError – If input is not a valid UUID representation.