Metadata-Version: 2.4
Name: wanting
Version: 0.6.1
Summary: A library for creating, and working with models that can represent incomplete information.
Author-email: Narvin Singh <Narvin.A.Singh@gmail.com>
License: Wanting is a library for working with incomplete models.
        Copyright (C) 2025  Narvin Singh
        
        This program is free software: you can redistribute it and/or modify
        it under the terms of the GNU General Public License as published by
        the Free Software Foundation, either version 3 of the License, or
        (at your option) any later version.
        
        This program is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
        GNU General Public License for more details.
        
        You should have received a copy of the GNU General Public License
        along with this program.  If not, see <https://www.gnu.org/licenses/>.
        
Project-URL: Homepage, https://gitlab.com/narvin/wanting
Project-URL: Repository, https://gitlab.com/narvin/wanting
Project-URL: Bug Tracker, https://gitlab.com/narvin/wanting/-/issues
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: pydantic~=2.11
Provides-Extra: dev
Requires-Dist: mypy~=1.18; extra == "dev"
Requires-Dist: pre-commit~=4.3; extra == "dev"
Requires-Dist: pytest~=8.4; extra == "dev"
Requires-Dist: ruff~=0.13.0; extra == "dev"
Provides-Extra: doc
Requires-Dist: python-docs-theme~=2025.9; extra == "doc"
Requires-Dist: sphinx~=8.2; extra == "doc"
Provides-Extra: deploy
Requires-Dist: build~=1.3; extra == "deploy"
Requires-Dist: twine~=6.2; extra == "deploy"
Dynamic: license-file

Wanting
#######

Wanting is a library for creating, and working with models that can represent
incomplete information.

Motivation
**********

Instances of domain models don't always spring into existence fully formed.
They may be partially constructed intially, then filled in over time. Making a
model field optional that is not intially available, but eventually required is
inaccurate because an optional field may *always* be optional, so it never has
to be filled in. It would be better to make the field a required union of the
type it wants, and a placholder type. The wanting types are such placeholders.
They can include metadata, such as the source of the update with missing data,
and even partial data from that source.

Usage
*****

A domain model may look like this:

.. code-block:: python

    from typing import Literal

    import pydantic
    import wanting


    class User(pydantic.BaseModel):
        """A model that can have incomplete information."""

        name: str
        employee_id: str | wanting.Unavailable
        department_code: Literal["TECH", "FO", "BO", "HR"] | wanting.Unmapped
       
Then there is an onboarding system that creates a ``User``. However, the
``employee_id`` is unavailable at this time because it will be generated later.
The onboarding system sources the department code from some other system, which
uses different values than those in the ``User`` model. The onboarding system
knows how to map some of the codes from the other system to the ``User``
department codes, but not all of them. However, because ``employee_id``, and
``department_code`` may also be wanting fields in the ``User`` model, the
onboarding system can still create a fully valid model, while also indicating
that some information is missing:

.. code-block:: python

    user = User(
        name="Charlotte",
        employee_id=wanting.Unavailable(source="onboarding"),
        department_code=wanting.Unmapped(source="onboarding", value="art"),
    )

The model validates, and all the wanting fields serialize to valid JSON:

.. code-block:: python

    assert user.model_dump() == {
        "name": "Charlotte",
        "employee_id": {
            "kind": "unavailable",
            "source": "onboarding",
            "value": {"serialized": b"null"},
        },
        "department_code": {
            "kind": "unmapped",
            "source": "onboarding",
            "value": {"serialized": b'"art"'},
        },
    }

This user can now be persisted, then queried, and updated later by other
systems.

A model class can be queried for its potentially wanting fields:

.. code-block:: python

    class Child(pydantic.BaseModel):
        """A model that can have incomplete information."""

        non_wanting: int
        wanting: int | wanting.Unavailable


    class Parent(pydantic.BaseModel):
        """A model that can have top-level, and nested incomplete information."""

        non_wanting: int
        wanting: int | wanting.Unavailable
        nested: Child


    def reduce_path(path: list[wanting.FieldInfoEx]) -> str:
        """Reduce the FieldInfoEx objects that comprise a path to a readable string."""
        return "->".join(f"{fi.cls.__name__}.{fi.name}" for fi in path)


    paths = wanting.wanting_fields(Parent)
    summary = [reduce_path(path) for path in paths]
    assert summary == ["Parent.wanting", "Parent.nested->Child.wanting"]

A model instance can be queried for its wanting values:

.. code-block:: python

    p = Parent(
        non_wanting=1,
        wanting=2,
        nested=Child(non_wanting=3, wanting=wanting.Unavailable(source="doc")),
    )
    assert wanting.wanting_values(p) == {
        "nested": {"wanting": wanting.Unavailable(source="doc")}
    }

A model instance can also be serialized, either including or excluding its
wanting values:

.. code-block:: python

    incex = wanting.wanting_incex(p)
    assert p.model_dump(include=incex) == {
        "nested": {
            "wanting": {
                "kind": "unavailable",
                "source": "doc",
                "value": {"serialized": b"null"},
            }
        }
    }
    assert p.model_dump(exclude=incex) == {
        "non_wanting": 1,
        "wanting": 2,
        "nested": {"non_wanting": 3},
    }

Model serialization with respect to wanting fields is invertible. A model can
be serialized, then the result can be deserialized back into an equivalent
model.

.. code-block:: python

    p2 = Parent.model_validate(p.model_dump())
    assert p == p2
