django-range-merge


Namedjango-range-merge JSON
Version 2024.10.1 PyPI version JSON
download
home_pageNone
SummaryEnables the range_merge Aggregate for Django on Postgres. range_merge 'Computes the smallest range that includes ... the given ranges'.
upload_time2024-10-26 00:02:55
maintainerNone
docs_urlNone
authorNone
requires_python<4.0,>=3.10
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            # django-range-merge

A Django package that enables the PostgreSQL `range_merge` aggregate function for use with Django’s ORM.

`django-range-merge` provides access to PostgreSQL's `range_merge` aggregate function, which computes the smallest range that includes all input ranges. This is particularly useful when working with Django's range fields like `DateTimeRangeField`, `DateRangeField`, or `IntegerRangeField`.

![Visualization of what range_merge does, returning smallest range that includes input ranges in the QuerySet](https://raw.githubusercontent.com/omenapps/django-range-merge/main/media/range_merge.png)

This package should only be used with Django projects using the Postgres database. See [Postgres docs on Range Functions](https://www.postgresql.org/docs/14/functions-range.html#RANGE-FUNCTIONS-TABLE).

Note: This app is still a work-in-progress, but currently works. Tests have not yet been implemented.


## Installation

```bash
pip install django-range-merge
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...
    "django_range_merge",
    ...
]
```

Migrate to apply the aggregation to your database:

```bash
> python manage.py migrate
```

## Getting Started

Here is a quick example. We have an `Event` model with two different range fields: `period`, which contains the datetime range period during which the Event occurs; and `potential_visitors`, which is an approximation of the minimum and maximum number of people attending the Event.

We want two different views to help Event organizers understand some aggregate details about Events.

- **range_of_visitors_this_month**: Show the overall lowest and greatest number of people we expect for all events this month
- **overall_dates_of_funded_events**: Shows the overall range of dates for Events which are funded (the `is_funded` BooleanField is set to True)

models.py

```python
class Event(models.Model):
    name = models.TextField()
    period = models.DateTimeRangeField()
    potential_visitors = models.IntegerRangeField()
    is_funded = BooleanField(default=False)

    class Meta:
        verbose_name = "Event"
        verbose_name_plural = "Events"

    def __str__(self):
        return self.name

```

date_utils.py (get a range covering the entire current month)

```python
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from psycopg2.extras import DateTimeTZRange

def get_month_range():
    """Return a DateTimeRange range covering this entire month"""
    today = timezone.now().date()
    if today.day > 25:
        today += timezone.timedelta(7)
    this_month_start = today.replace(day=1)
    next_month_start = this_month_start + relativedelta(months=1)
    return DateTimeTZRange(this_month_start, next_month_start)
```

views.py

```python
from django.db.models import F, Aggregate
from django.template.response import TemplateResponse

from .date_utils import get_month_range

def range_of_visitors_this_month(request):
    """
    e.g., given the following instances:
        {"id" : 1, "name" : "Birthday",     "potential_visitors" : "[2, 3)", ...}
        {"id" : 2, "name" : "Bake Sale",    "potential_visitors" : "[30, 50)", ...}
        {"id" : 3, "name" : "Band Camp",    "potential_visitors" : "[22, 28)", ...}
        {"id" : 4, "name" : "Cooking Show", "potential_visitors" : "[7, 20)", ...}
        {"id" : 5, "name" : "Pajama Day",   "potential_visitors" : "[15, 30)", ...}

    The result would be:
        {'output': NumericRange(2, 50, '[)')}
    """
    template = "base.html"

    context = Event.objects.filter(period__overlap=get_month_range()).aggregate(
        output=Aggregate(F("potential_visitors"), function="range_merge")
    )

    return TemplateResponse(request, template, context)

def overall_dates_of_funded_events(request):
    template = "base.html"

    context = Event.objects.filter(is_funded=True).aggregate(
        output=Aggregate(F("period"), function="range_merge")
    )
    # Example result: {'output': DateTimeRange("2022-10-01 02:00:00", "2022-12-07 12:00:00", '[)')}

    return TemplateResponse(request, template, context)

```

base.html

```html
<html>
    <head></head>
    <body>
        {{ output }}
    </body>
</html>
```

## Performance Considerations

- The `range_merge` aggregate operates efficiently on server side
- Indexes on range fields can improve query performance
- Consider using `values()` to limit data transfer when only ranges are needed


## Development and Testing Setup

This project uses Docker Compose for development and testing. Follow these steps to get started:


### Prerequisites

1. Make sure you have Docker and Docker Compose installed
2. Install `uv` tool: `pip install uv`


### Setting Up the Development Environment

1. Clone the repository:
   ```bash
   git clone https://github.com/OmenApps/django-range-merge.git
   cd django-range-merge
   ```

2. Create a virtual environment and install dependencies:
   ```bash
   uv venv
   source .venv/bin/activate  # On Windows: .venv\Scripts\activate
   uv sync --prerelease=allow --extra=dev
   ```

3. Build and start the Docker containers:
   ```bash
   docker-compose up -d --build postgres
   ```

4. Run migrations:
   ```bash
    python manage.py migrate
    ```

### Running Tests

Using `nox`:

   ```bash
   nox -s tests
   ```
   This will run tests across multiple Django versions.


### Development Database Setup

The project uses PostgreSQL for testing. The Docker Compose setup includes a PostgreSQL instance with the following configuration:

- Host: localhost
- Port: 5436  # To avoid conflicts with local PostgreSQL installations
- Database: postgres
- Username: postgres
- Password: postgres

The database is automatically configured when running tests through Docker Compose.


## License

The code in this repository is licensed under The MIT License. See LICENSE.md in the repository for more details.


## Contributing

Contributions are very welcome.

This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features.  However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "django-range-merge",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": "Jack Linke <jacklinke@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/74/28/553e05e69ad51e3011f07eb9f492cfab7f52fd4f1181a55b6485505394a3/django_range_merge-2024.10.1.tar.gz",
    "platform": null,
    "description": "# django-range-merge\n\nA Django package that enables the PostgreSQL `range_merge` aggregate function for use with Django\u2019s ORM.\n\n`django-range-merge` provides access to PostgreSQL's `range_merge` aggregate function, which computes the smallest range that includes all input ranges. This is particularly useful when working with Django's range fields like `DateTimeRangeField`, `DateRangeField`, or `IntegerRangeField`.\n\n![Visualization of what range_merge does, returning smallest range that includes input ranges in the QuerySet](https://raw.githubusercontent.com/omenapps/django-range-merge/main/media/range_merge.png)\n\nThis package should only be used with Django projects using the Postgres database. See [Postgres docs on Range Functions](https://www.postgresql.org/docs/14/functions-range.html#RANGE-FUNCTIONS-TABLE).\n\nNote: This app is still a work-in-progress, but currently works. Tests have not yet been implemented.\n\n\n## Installation\n\n```bash\npip install django-range-merge\n```\n\nAdd to `INSTALLED_APPS`:\n\n```python\nINSTALLED_APPS = [\n    ...\n    \"django_range_merge\",\n    ...\n]\n```\n\nMigrate to apply the aggregation to your database:\n\n```bash\n> python manage.py migrate\n```\n\n## Getting Started\n\nHere is a quick example. We have an `Event` model with two different range fields: `period`, which contains the datetime range period during which the Event occurs; and `potential_visitors`, which is an approximation of the minimum and maximum number of people attending the Event.\n\nWe want two different views to help Event organizers understand some aggregate details about Events.\n\n- **range_of_visitors_this_month**: Show the overall lowest and greatest number of people we expect for all events this month\n- **overall_dates_of_funded_events**: Shows the overall range of dates for Events which are funded (the `is_funded` BooleanField is set to True)\n\nmodels.py\n\n```python\nclass Event(models.Model):\n    name = models.TextField()\n    period = models.DateTimeRangeField()\n    potential_visitors = models.IntegerRangeField()\n    is_funded = BooleanField(default=False)\n\n    class Meta:\n        verbose_name = \"Event\"\n        verbose_name_plural = \"Events\"\n\n    def __str__(self):\n        return self.name\n\n```\n\ndate_utils.py (get a range covering the entire current month)\n\n```python\nfrom django.utils import timezone\nfrom dateutil.relativedelta import relativedelta\nfrom psycopg2.extras import DateTimeTZRange\n\ndef get_month_range():\n    \"\"\"Return a DateTimeRange range covering this entire month\"\"\"\n    today = timezone.now().date()\n    if today.day > 25:\n        today += timezone.timedelta(7)\n    this_month_start = today.replace(day=1)\n    next_month_start = this_month_start + relativedelta(months=1)\n    return DateTimeTZRange(this_month_start, next_month_start)\n```\n\nviews.py\n\n```python\nfrom django.db.models import F, Aggregate\nfrom django.template.response import TemplateResponse\n\nfrom .date_utils import get_month_range\n\ndef range_of_visitors_this_month(request):\n    \"\"\"\n    e.g., given the following instances:\n        {\"id\" : 1, \"name\" : \"Birthday\",     \"potential_visitors\" : \"[2, 3)\", ...}\n        {\"id\" : 2, \"name\" : \"Bake Sale\",    \"potential_visitors\" : \"[30, 50)\", ...}\n        {\"id\" : 3, \"name\" : \"Band Camp\",    \"potential_visitors\" : \"[22, 28)\", ...}\n        {\"id\" : 4, \"name\" : \"Cooking Show\", \"potential_visitors\" : \"[7, 20)\", ...}\n        {\"id\" : 5, \"name\" : \"Pajama Day\",   \"potential_visitors\" : \"[15, 30)\", ...}\n\n    The result would be:\n        {'output': NumericRange(2, 50, '[)')}\n    \"\"\"\n    template = \"base.html\"\n\n    context = Event.objects.filter(period__overlap=get_month_range()).aggregate(\n        output=Aggregate(F(\"potential_visitors\"), function=\"range_merge\")\n    )\n\n    return TemplateResponse(request, template, context)\n\ndef overall_dates_of_funded_events(request):\n    template = \"base.html\"\n\n    context = Event.objects.filter(is_funded=True).aggregate(\n        output=Aggregate(F(\"period\"), function=\"range_merge\")\n    )\n    # Example result: {'output': DateTimeRange(\"2022-10-01 02:00:00\", \"2022-12-07 12:00:00\", '[)')}\n\n    return TemplateResponse(request, template, context)\n\n```\n\nbase.html\n\n```html\n<html>\n    <head></head>\n    <body>\n        {{ output }}\n    </body>\n</html>\n```\n\n## Performance Considerations\n\n- The `range_merge` aggregate operates efficiently on server side\n- Indexes on range fields can improve query performance\n- Consider using `values()` to limit data transfer when only ranges are needed\n\n\n## Development and Testing Setup\n\nThis project uses Docker Compose for development and testing. Follow these steps to get started:\n\n\n### Prerequisites\n\n1. Make sure you have Docker and Docker Compose installed\n2. Install `uv` tool: `pip install uv`\n\n\n### Setting Up the Development Environment\n\n1. Clone the repository:\n   ```bash\n   git clone https://github.com/OmenApps/django-range-merge.git\n   cd django-range-merge\n   ```\n\n2. Create a virtual environment and install dependencies:\n   ```bash\n   uv venv\n   source .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\n   uv sync --prerelease=allow --extra=dev\n   ```\n\n3. Build and start the Docker containers:\n   ```bash\n   docker-compose up -d --build postgres\n   ```\n\n4. Run migrations:\n   ```bash\n    python manage.py migrate\n    ```\n\n### Running Tests\n\nUsing `nox`:\n\n   ```bash\n   nox -s tests\n   ```\n   This will run tests across multiple Django versions.\n\n\n### Development Database Setup\n\nThe project uses PostgreSQL for testing. The Docker Compose setup includes a PostgreSQL instance with the following configuration:\n\n- Host: localhost\n- Port: 5436  # To avoid conflicts with local PostgreSQL installations\n- Database: postgres\n- Username: postgres\n- Password: postgres\n\nThe database is automatically configured when running tests through Docker Compose.\n\n\n## License\n\nThe code in this repository is licensed under The MIT License. See LICENSE.md in the repository for more details.\n\n\n## Contributing\n\nContributions are very welcome.\n\nThis project is currently accepting all types of contributions, bug fixes,\nsecurity fixes, maintenance work, or new features.  However, please make sure\nto have a discussion about your new feature idea with the maintainers prior to\nbeginning development to maximize the chances of your change being accepted.\nYou can start a conversation by creating a new issue on this repo summarizing\nyour idea.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Enables the range_merge Aggregate for Django on Postgres. range_merge 'Computes the smallest range that includes ... the given ranges'.",
    "version": "2024.10.1",
    "project_urls": {
        "Changelog": "https://github.com/OmenApps/django-range-merge/releases",
        "Homepage": "https://github.com/OmenApps/django-range-merge",
        "Repository": "https://github.com/OmenApps/django-range-merge"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "29e6877f21add0ee51a58c5aa9b0f0b673b4402576f6c4bbf70d5b523e34c2a7",
                "md5": "f69e150a7c193f74287cbf3072a73247",
                "sha256": "038047dfb3fa28f0ccc713dacdaadcefb23b6624cb5f5711c8bae433f751594a"
            },
            "downloads": -1,
            "filename": "django_range_merge-2024.10.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "f69e150a7c193f74287cbf3072a73247",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 6710,
            "upload_time": "2024-10-26T00:02:54",
            "upload_time_iso_8601": "2024-10-26T00:02:54.214185Z",
            "url": "https://files.pythonhosted.org/packages/29/e6/877f21add0ee51a58c5aa9b0f0b673b4402576f6c4bbf70d5b523e34c2a7/django_range_merge-2024.10.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "7428553e05e69ad51e3011f07eb9f492cfab7f52fd4f1181a55b6485505394a3",
                "md5": "4532c82a125164f077ea6a5296fd463b",
                "sha256": "94c88d4f6b462a557db76514c387bd94e35586eb69db3c79ce6e2f9ed426262b"
            },
            "downloads": -1,
            "filename": "django_range_merge-2024.10.1.tar.gz",
            "has_sig": false,
            "md5_digest": "4532c82a125164f077ea6a5296fd463b",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 9057,
            "upload_time": "2024-10-26T00:02:55",
            "upload_time_iso_8601": "2024-10-26T00:02:55.778293Z",
            "url": "https://files.pythonhosted.org/packages/74/28/553e05e69ad51e3011f07eb9f492cfab7f52fd4f1181a55b6485505394a3/django_range_merge-2024.10.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-10-26 00:02:55",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "OmenApps",
    "github_project": "django-range-merge",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "requirements": [],
    "lcname": "django-range-merge"
}
        
Elapsed time: 0.38044s