Name | django-enum.IntFlag-field JSON |
Version |
0.0.3
JSON |
| download |
home_page | None |
Summary | django-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_time | 2024-10-02 21:34:31 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.8 |
license | LGPL-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"
}