django-enum.IntFlag-field


Namedjango-enum.IntFlag-field JSON
Version 0.0.3 PyPI version JSON
download
home_pageNone
Summarydjango-enum.IntFlag-field — A model field that takes an enum.IntFlag class, stores flag combinations as an IntegerField, and allows convenient bitwise containment predicates through the Django ORM.
upload_time2024-10-02 21:34:31
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseLGPL-3.0+
keywords django enum.intflag bitfield
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # django-enum.IntFlag-field

This gives you a model field that takes a Python stdlib [enum.IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) class, stores flag combinations as an IntegerField, and allows convenient bitwise containment predicates through the Django ORM.

In other words: a bitfield in your DB, with all the conveniences of enum.IntFlag.

### What is a bitfield, even
What's a bitfield? A bitfield is a field of bits :-). Like `0110`.
You can use it to represent a bunch of booleans that in turn represent presence or absence of flags, or anything you want — perhaps sandwich toppings!

Let's say these 4 field positions represent Butter, Gouda, Rocket and Tomato. Then `0110` means we have a sandwich without Butter, but with Gouda and Rocket, but no Tomato.

A bitfield is thus a string of bits. And it just so happens that an integer is *also* a string of bits! `0110` is the number 6, look:

```pycon
>>> 0b0110
6
```

And it just so happens that both Python and databases have facilities to work with numbers-as-bitfields or bitfields-as-numbers.
A database 64-bit BigIntegerField can accommodate a Python [enum.IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) class
with up to 63 members. The first member gets the bit position for the number 1 (2⁰, thus bit position 0), and since the maximum positive value
of a 64-bit signed integer is 2⁶³-1, and since we need powers of 2 for our members... that leaves bits 0-62, and thus 63 members.

When you use this ModelField, a Django check will validate your associated enum.IntFlag class to make sure your values are within bounds.


## Installing
`pip install django-enum.IntFlag-field`. Or equivalent.
There's no need to add anything to your `settings.INSTALLED_APPS`.


## How do I even
Example model and parafernalia, inspired by the [test models.py](src/django_testapp_intflagfield/models.py):

```python
from enum import IntFlag, auto
from django.db import models
from enum_intflagfield import IntFlagField


class SandwichTopping(IntFlag):
    BUTTER = auto()  # becomes 1  (2**0)
    GOUDA  = auto()  # becomes 2  (2**1)
    ROCKET = auto()  # becomes 4  (2**2)
    TOMATO = auto()  # becomes 8  (2**3)
    HUMMUS = auto()  # becomes 16 (2**4)


class Sandwich(models.Model):
    class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(toppings__contains_noneof=IntFlagField.complement(SandwichTopping)),
                name='forbid_spurious_bits',
                violation_error_message='Field contains spurious bits (bits not representing any Topping member)',
            )
        ]
    name = models.CharField(primary_key=True)
    toppings = IntFlagField(choices=SandwichTopping, unique=True)
```

And then you'd use it like so — example adapted from the [tests](src/django_testapp_intflagfield/tests.py): 

```pycon
>>> Sandwich.objects.create(
    name='Healthy',
    toppings=SandwichTopping.ROCKET | SandwichTopping.TOMATO | SandwichTopping.HUMMUS,
)
<Sandwich: Sandwich object (Healthy)>

# gives you the sandwich you just saved — on readback, the integer value is neatly converted into an IntFlag combo.
# 28 is the numeric value that was actually stored in the database — as with all Enums, you can get at it with `.value`.
>>> Sandwich.objects.get(name='Healthy').toppings
<SandwichTopping.ROCKET|TOMATO|HUMMUS: 28>

# gives you all sandwiches that have *ONE OR MORE* of (Butter, Hummus) toppings
>>> Sandwich.objects.filter(toppings__contains_anyof=SandwichTopping.BUTTER | SandwichTopping.HUMMUS)
<QuerySet [<Sandwich: Sandwich object (Healthy)>]>

# gives you all sandwiches that have *AT LEAST ALL OF* (Rocket, Tomato) toppings
>>> Sandwich.objects.filter(toppings__contains=SandwichTopping.ROCKET | SandwichTopping.TOMATO)
<QuerySet [<Sandwich: Sandwich object (Healthy)>]>

# gives you all sandwiches that have *NONE OF* (Butter, Gouda) toppings
>>> Sandwich.objects.exclude(toppings__contains_anyof=SandwichTopping.BUTTER | SandwichTopping.GOUDA)
<QuerySet [<Sandwich: Sandwich object (Healthy)>]>
```

# This is wrong and not even in 1NF
True! It's a field with a set of values. You *can* set indices on it, but it's quite an art to match index expressions to query patterns. And you'll need to manage your Enum class wisely; deleting a member means changing over from `auto()` to hardcoded powers-of-2 as you don't want your semantics to shift, presumably! There's advantages too, in particular `CHECK` constraints are straightforward.

Alternatives:
- add a boolean column for every flag. Cons: Lots of columns, thus lots of schema changes when your set-of-booleans definition change often. Performance & efficiency suffer. Pros: Much easier to design indices for.
- use a many2many design. Cons: performance & efficiency suffer. Some queries are harder. `CHECK` constraints won't allow you to express all you would be able to express on a bitfield-containing row. Pros: You'll be doing it by the book.

Yet sometimes, you really just want a blessed bitfield. I couldn't find any ergonomic ones on PyPI (2024), so I made this one.


# Contributing
You may want to discuss your idea on the [mailinglist](https://lists.sr.ht/~nullenenenen/django-enum.IntFlag-field-discuss) first. Or just send a patch straight away, see https://git-send-email.io/ to learn how.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "django-enum.IntFlag-field",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "Django, enum.IntFlag, bitfield",
    "author": null,
    "author_email": "nullenenenen <nullenenenen@gavagai.eu>",
    "download_url": "https://files.pythonhosted.org/packages/d3/c2/5d9598752511b1030797ea5439345c9fdcbb195de7de31e7cb01c09ddfe0/django_enum_intflag_field-0.0.3.tar.gz",
    "platform": null,
    "description": "# django-enum.IntFlag-field\n\nThis gives you a model field that takes a Python stdlib [enum.IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) class, stores flag combinations as an IntegerField, and allows convenient bitwise containment predicates through the Django ORM.\n\nIn other words: a bitfield in your DB, with all the conveniences of enum.IntFlag.\n\n### What is a bitfield, even\nWhat's a bitfield? A bitfield is a field of bits :-). Like `0110`.\nYou can use it to represent a bunch of booleans that in turn represent presence or absence of flags, or anything you want \u2014 perhaps sandwich toppings!\n\nLet's say these 4 field positions represent Butter, Gouda, Rocket and Tomato. Then `0110` means we have a sandwich without Butter, but with Gouda and Rocket, but no Tomato.\n\nA bitfield is thus a string of bits. And it just so happens that an integer is *also* a string of bits! `0110` is the number 6, look:\n\n```pycon\n>>> 0b0110\n6\n```\n\nAnd it just so happens that both Python and databases have facilities to work with numbers-as-bitfields or bitfields-as-numbers.\nA database 64-bit BigIntegerField can accommodate a Python [enum.IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag) class\nwith up to 63 members. The first member gets the bit position for the number 1 (2\u2070, thus bit position 0), and since the maximum positive value\nof a 64-bit signed integer is 2\u2076\u00b3-1, and since we need powers of 2 for our members... that leaves bits 0-62, and thus 63 members.\n\nWhen you use this ModelField, a Django check will validate your associated enum.IntFlag class to make sure your values are within bounds.\n\n\n## Installing\n`pip install django-enum.IntFlag-field`. Or equivalent.\nThere's no need to add anything to your `settings.INSTALLED_APPS`.\n\n\n## How do I even\nExample model and parafernalia, inspired by the [test models.py](src/django_testapp_intflagfield/models.py):\n\n```python\nfrom enum import IntFlag, auto\nfrom django.db import models\nfrom enum_intflagfield import IntFlagField\n\n\nclass SandwichTopping(IntFlag):\n    BUTTER = auto()  # becomes 1  (2**0)\n    GOUDA  = auto()  # becomes 2  (2**1)\n    ROCKET = auto()  # becomes 4  (2**2)\n    TOMATO = auto()  # becomes 8  (2**3)\n    HUMMUS = auto()  # becomes 16 (2**4)\n\n\nclass Sandwich(models.Model):\n    class Meta:\n        constraints = [\n            models.CheckConstraint(\n                check=models.Q(toppings__contains_noneof=IntFlagField.complement(SandwichTopping)),\n                name='forbid_spurious_bits',\n                violation_error_message='Field contains spurious bits (bits not representing any Topping member)',\n            )\n        ]\n    name = models.CharField(primary_key=True)\n    toppings = IntFlagField(choices=SandwichTopping, unique=True)\n```\n\nAnd then you'd use it like so \u2014 example adapted from the [tests](src/django_testapp_intflagfield/tests.py): \n\n```pycon\n>>> Sandwich.objects.create(\n    name='Healthy',\n    toppings=SandwichTopping.ROCKET | SandwichTopping.TOMATO | SandwichTopping.HUMMUS,\n)\n<Sandwich: Sandwich object (Healthy)>\n\n# gives you the sandwich you just saved \u2014 on readback, the integer value is neatly converted into an IntFlag combo.\n# 28 is the numeric value that was actually stored in the database \u2014 as with all Enums, you can get at it with `.value`.\n>>> Sandwich.objects.get(name='Healthy').toppings\n<SandwichTopping.ROCKET|TOMATO|HUMMUS: 28>\n\n# gives you all sandwiches that have *ONE OR MORE* of (Butter, Hummus) toppings\n>>> Sandwich.objects.filter(toppings__contains_anyof=SandwichTopping.BUTTER | SandwichTopping.HUMMUS)\n<QuerySet [<Sandwich: Sandwich object (Healthy)>]>\n\n# gives you all sandwiches that have *AT LEAST ALL OF* (Rocket, Tomato) toppings\n>>> Sandwich.objects.filter(toppings__contains=SandwichTopping.ROCKET | SandwichTopping.TOMATO)\n<QuerySet [<Sandwich: Sandwich object (Healthy)>]>\n\n# gives you all sandwiches that have *NONE OF* (Butter, Gouda) toppings\n>>> Sandwich.objects.exclude(toppings__contains_anyof=SandwichTopping.BUTTER | SandwichTopping.GOUDA)\n<QuerySet [<Sandwich: Sandwich object (Healthy)>]>\n```\n\n# This is wrong and not even in 1NF\nTrue! It's a field with a set of values. You *can* set indices on it, but it's quite an art to match index expressions to query patterns. And you'll need to manage your Enum class wisely; deleting a member means changing over from `auto()` to hardcoded powers-of-2 as you don't want your semantics to shift, presumably! There's advantages too, in particular `CHECK` constraints are straightforward.\n\nAlternatives:\n- add a boolean column for every flag. Cons: Lots of columns, thus lots of schema changes when your set-of-booleans definition change often. Performance & efficiency suffer. Pros: Much easier to design indices for.\n- use a many2many design. Cons: performance & efficiency suffer. Some queries are harder. `CHECK` constraints won't allow you to express all you would be able to express on a bitfield-containing row. Pros: You'll be doing it by the book.\n\nYet sometimes, you really just want a blessed bitfield. I couldn't find any ergonomic ones on PyPI (2024), so I made this one.\n\n\n# Contributing\nYou may want to discuss your idea on the [mailinglist](https://lists.sr.ht/~nullenenenen/django-enum.IntFlag-field-discuss) first. Or just send a patch straight away, see https://git-send-email.io/ to learn how.\n",
    "bugtrack_url": null,
    "license": "LGPL-3.0+",
    "summary": "django-enum.IntFlag-field \u2014 A model field that takes an enum.IntFlag class, stores flag combinations as an IntegerField, and allows convenient bitwise containment predicates through the Django ORM.",
    "version": "0.0.3",
    "project_urls": {
        "Documentation": "https://git.sr.ht/~nullenenenen/django-enum.IntFlag-field/tree/master/item/README.md",
        "Homepage": "https://hub.sr.ht/~nullenenenen/django-enum.IntFlag-field/",
        "Source": "https://git.sr.ht/~nullenenenen/django-enum.IntFlag-field/"
    },
    "split_keywords": [
        "django",
        " enum.intflag",
        " bitfield"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0f4e18965fc86a327449940aac0c881379697759ed71ed601aedadca6369d2b8",
                "md5": "b649df86b7272cd666240321c73fca7e",
                "sha256": "431bed993fc9cdf51f7da524f783e28e5cd8f9844acc52c9d855a5e7f4909523"
            },
            "downloads": -1,
            "filename": "django_enum.IntFlag_field-0.0.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b649df86b7272cd666240321c73fca7e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 8666,
            "upload_time": "2024-10-02T21:34:29",
            "upload_time_iso_8601": "2024-10-02T21:34:29.739275Z",
            "url": "https://files.pythonhosted.org/packages/0f/4e/18965fc86a327449940aac0c881379697759ed71ed601aedadca6369d2b8/django_enum.IntFlag_field-0.0.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "d3c25d9598752511b1030797ea5439345c9fdcbb195de7de31e7cb01c09ddfe0",
                "md5": "f8c1c7ed490b469204a3d33af3637ee4",
                "sha256": "1677bfea09fe28b8433072ad6d2070e2c107d1bea7acf8771ec8cab900cf086c"
            },
            "downloads": -1,
            "filename": "django_enum_intflag_field-0.0.3.tar.gz",
            "has_sig": false,
            "md5_digest": "f8c1c7ed490b469204a3d33af3637ee4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 8162,
            "upload_time": "2024-10-02T21:34:31",
            "upload_time_iso_8601": "2024-10-02T21:34:31.143134Z",
            "url": "https://files.pythonhosted.org/packages/d3/c2/5d9598752511b1030797ea5439345c9fdcbb195de7de31e7cb01c09ddfe0/django_enum_intflag_field-0.0.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-10-02 21:34:31",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "django-enum.intflag-field"
}
        
Elapsed time: 3.05142s