pax_global_header00006660000000000000000000000064143704433110014512gustar00rootroot0000000000000052 comment=25b800bb134257e24dc73c9fd9cfd2a11ba2a3f5 django-timescaledb-0.2.13/000077500000000000000000000000001437044331100153115ustar00rootroot00000000000000django-timescaledb-0.2.13/.github/000077500000000000000000000000001437044331100166515ustar00rootroot00000000000000django-timescaledb-0.2.13/.github/workflows/000077500000000000000000000000001437044331100207065ustar00rootroot00000000000000django-timescaledb-0.2.13/.github/workflows/ci-tests.yml000066400000000000000000000022551437044331100231700ustar00rootroot00000000000000name: Django timescaledb - Test basic project setup on: push: branches: [main] jobs: setup-example-project: runs-on: ubuntu-latest env: DB_NAME: test DB_USER: postgres DB_PORT: 5433 steps: - uses: actions/checkout@v1 - name: Set up Python 3.7 uses: actions/setup-python@v1 with: python-version: 3.7 - name: Checkout code uses: actions/checkout@v2 - name: Install dependencies run: |- cd example pip install -r requirements.txt # Start timescaledb docker run -d --name timescaledb -p 5433:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=test timescale/timescaledb:2.5.1-pg14 # Wait for db to be ready sleep 4 # Migrate PYTHONPATH=../ python3 manage.py migrate - name: Run tests run: |- cd example PYTHONPATH=../ python3 manage.py test - name: Test alter field run: |- cd example sed -i -e 's/1 day/2 days/g' metrics/models.py PYTHONPATH=../ python3 manage.py makemigrations PYTHONPATH=../ python3 manage.py migrate django-timescaledb-0.2.13/.github/workflows/python-publish.yml000066400000000000000000000016521437044331100244220ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.x" - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* -u "$TWINE_USERNAME" -p "$TWINE_PASSWORD" django-timescaledb-0.2.13/.gitignore000066400000000000000000000001311437044331100172740ustar00rootroot00000000000000*.pyc dist/ build/ django_timescaledb.egg-info/ .venv *ipynb example/timescale .DS_STORE django-timescaledb-0.2.13/LICENSE000066400000000000000000000261351437044331100163250ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. django-timescaledb-0.2.13/MANIFEST.in000066400000000000000000000001021437044331100170400ustar00rootroot00000000000000include LICENSE include README.rst recursive-include timescale/ * django-timescaledb-0.2.13/README.md000066400000000000000000000111301437044331100165640ustar00rootroot00000000000000# Django timescaledb [![PyPI version fury.io](https://badge.fury.io/py/django-timescaledb.svg)](https://pypi.python.org/pypi/django-timescaledb/) ![Workflow](https://github.com/schlunsen/django-timescaledb/actions/workflows/ci-tests.yml/badge.svg) A database backend and tooling for Timescaledb. Based on [gist](https://gist.github.com/dedsm/fc74f04eb70d78459ff0847ef16f2e7a) from WeRiot. ## Quick start 1. Install via pip ```bash pip install django-timescaledb ``` 2. Use as DATABASE engine in settings.py: Standard PostgreSQL ```python DATABASES = { 'default': { 'ENGINE': 'timescale.db.backends.postgresql', ... }, } ``` PostGIS ```python DATABASES = { 'default': { 'ENGINE': 'timescale.db.backends.postgis', ... }, } ``` If you already make use of a custom PostgreSQL db backend you can set the path in settings.py. ```python TIMESCALE_DB_BACKEND_BASE = "django.contrib.gis.db.backends.postgis" ``` 3. Inherit from the TimescaleModel. A [hypertable](https://docs.timescale.com/latest/using-timescaledb/hypertables#react-docs) will automatically be created. ```python class TimescaleModel(models.Model): """ A helper class for using Timescale within Django, has the TimescaleManager and TimescaleDateTimeField already present. This is an abstract class it should be inheritted by another class for use. """ time = TimescaleDateTimeField(interval="1 day") objects = TimescaleManager() class Meta: abstract = True ``` Implementation would look like this ```python from timescale.db.models.models import TimescaleModel class Metric(TimescaleModel): temperature = models.FloatField() ``` If you already have a table, you can either add `time` field of type `TimescaleDateTimeField` to your model or rename (if not already named `time`) and change type of existing `DateTimeField` (rename first then run `makemigrations` and then change the type, so that `makemigrations` considers it as change in same field instead of removing and adding new field). This also triggers the creation of a hypertable. ```python from timescale.db.models.fields import TimescaleDateTimeField from timescale.db.models.managers import TimescaleManager class Metric(models.Model): time = TimescaleDateTimeField(interval="1 day") objects = models.Manager() timescale = TimescaleManager() ``` The name of the field is important as Timescale specific feratures require this as a property of their functions. ### Reading Data "TimescaleDB hypertables are designed to behave in the same manner as PostgreSQL database tables for reading data, using standard SQL commands." As such the use of the Django's ORM is perfectally suited to this type of data. By leveraging a custom model manager and queryset we can extend the queryset methods to include Timescale functions. #### Time Bucket [More Info](https://docs.timescale.com/latest/using-timescaledb/reading-data#time-bucket) ```python Metric.timescale.filter(time__range=date_range).time_bucket('time', '1 hour') # expected output )}, ... ]> ``` #### Time Bucket Gap Fill [More Info](https://docs.timescale.com/latest/using-timescaledb/reading-data#gap-filling) ```python from metrics.models import * from django.db.models import Count, Avg from django.utils import timezone from datetime import timedelta ranges = (timezone.now() - timedelta(days=2), timezone.now()) (Metric.timescale .filter(time__range=ranges) .time_bucket_gapfill('time', '1 day', ranges[0], ranges[1], datapoints=240) .annotate(Avg('temperature'))) # expected output ), 'temperature__avg': None}, ...]> ``` #### Histogram [More Info](https://docs.timescale.com/latest/using-timescaledb/reading-data#histogram) ```python from metrics.models import * from django.db.models import Count from django.utils import timezone from datetime import timedelta ranges = (timezone.now() - timedelta(days=3), timezone.now()) (Metric.timescale .filter(time__range=ranges) .values('device') .histogram(field='temperature', min_value=50.0, max_value=55.0, num_of_buckets=10) .annotate(Count('device'))) # expected output ``` ## Contributors - [Rasmus Schlünsen](https://github.com/schlunsen) - [Ben Cleary](https://github.com/bencleary) - [Jonathan Sundqvist](https://github.com/jonathan-s) - [Harsh Bhikadia](https://github.com/daadu) django-timescaledb-0.2.13/README.rst000066400000000000000000000114411437044331100170010ustar00rootroot00000000000000Django timescaledb ================== A database backend and tooling for Timescaledb. Based on `gist `__ from WeRiot. Quick start ----------- 1. Install via pip .. code:: bash pip install django-timescaledb 2. Use as DATABASE engine in settings.py: Standard PostgreSQL .. code:: python DATABASES = { 'default': { 'ENGINE': 'timescale.db.backends.postgresql', ... }, } PostGIS .. code:: python DATABASES = { 'default': { 'ENGINE': 'timescale.db.backends.postgis', ... }, } If you already make use of a custom PostgreSQL db backend you can set the path in settings.py. .. code:: python TIMESCALE_DB_BACKEND_BASE = "django.contrib.gis.db.backends.postgis" 3. Inherit from the TimescaleModel. A `hypertable `__ will automatically be created. .. code:: python class TimescaleModel(models.Model): """ A helper class for using Timescale within Django, has the TimescaleManager and TimescaleDateTimeField already present. This is an abstract class it should be inheritted by another class for use. """ time = TimescaleDateTimeField(interval="1 day") objects = TimescaleManager() class Meta: abstract = True Implementation would look like this .. code:: python from timescale.db.models.models import TimescaleModel class Metric(TimescaleModel): temperature = models.FloatField() If you already have a table, you can either add `time` field of type `TimescaleDateTimeField` to your model or rename (if not already named `time`) and change type of existing `DateTimeField` (rename first then run `makemigrations` and then change the type, so that `makemigrations` considers it as change in same field instead of removing and adding new field). This also triggers the creation of a hypertable. .. code:: python from timescale.db.models.fields import TimescaleDateTimeField from timescale.db.models.managers import TimescaleManager class Metric(models.Model): time = TimescaleDateTimeField(interval="1 day") objects = models.Manager() timescale = TimescaleManager() The name of the field is important as Timescale specific feratures require this as a property of their functions. ### Reading Data "TimescaleDB hypertables are designed to behave in the same manner as PostgreSQL database tables for reading data, using standard SQL commands." As such the use of the Django's ORM is perfectally suited to this type of data. By leveraging a custom model manager and queryset we can extend the queryset methods to include Timescale functions. Time Bucket `More Info `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: python Metric.timescale.filter(time__range=date_range).time_bucket('time', '1 hour') # expected output )}, ... ]> Time Bucket Gap Fill `More Info `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: python from metrics.models import * from django.db.models import Count, Avg from django.utils import timezone from datetime import timedelta ranges = (timezone.now() - timedelta(days=2), timezone.now()) (Metric.timescale .filter(time__range=ranges) .time_bucket_gapfill('time', '1 day', ranges[0], ranges[1], datapoints=240) .annotate(Avg('temperature'))) # expected output ), 'temperature__avg': None}, ...]> Histogram `More Info `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: python from metrics.models import * from django.db.models import Count from django.utils import timezone from datetime import timedelta ranges = (timezone.now() - timedelta(days=3), timezone.now()) (Metric.timescale .filter(time__range=ranges) .values('device') .histogram(field='temperature', min_value=50.0, max_value=55.0, num_of_buckets=10) .annotate(Count('device'))) # expected output django-timescaledb-0.2.13/example/000077500000000000000000000000001437044331100167445ustar00rootroot00000000000000django-timescaledb-0.2.13/example/iot_example_app/000077500000000000000000000000001437044331100221125ustar00rootroot00000000000000django-timescaledb-0.2.13/example/iot_example_app/.env000066400000000000000000000001321437044331100226770ustar00rootroot00000000000000DB_DATABASE=test DB_USERNAME=postgres DB_PASSWORD=password DB_HOST=127.0.0.1 DB_PORT=5433 django-timescaledb-0.2.13/example/iot_example_app/__init__.py000066400000000000000000000000001437044331100242110ustar00rootroot00000000000000django-timescaledb-0.2.13/example/iot_example_app/asgi.py000066400000000000000000000006271437044331100234140ustar00rootroot00000000000000""" ASGI config for iot_example_app project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'iot_example_app.settings') application = get_asgi_application() django-timescaledb-0.2.13/example/iot_example_app/settings.py000066400000000000000000000064551437044331100243360ustar00rootroot00000000000000""" Django settings for iot_example_app project. Generated by 'django-admin startproject' using Django 3.1.4. For more information on this file, see https://docs.djangoproject.com/en/3.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.1/ref/settings/ """ from pathlib import Path from dotenv import load_dotenv import os load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'm#r0kagf+fk2r8e$8=2*g3v2(%e3*sb2a)l*_s+g0bg$zgpkr)' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'timescale', 'django_extensions', "metrics", ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'iot_example_app.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'iot_example_app.wsgi.application' # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'timescale.db.backends.postgresql', 'NAME': os.getenv('DB_DATABASE'), 'USER': os.getenv('DB_USERNAME'), 'PASSWORD': os.getenv('DB_PASSWORD'), 'HOST': os.getenv('DB_HOST'), 'PORT': os.getenv('DB_PORT'), } } # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ STATIC_URL = '/static/' django-timescaledb-0.2.13/example/iot_example_app/urls.py000066400000000000000000000013651437044331100234560ustar00rootroot00000000000000"""iot_example_app URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), ] django-timescaledb-0.2.13/example/iot_example_app/wsgi.py000066400000000000000000000006271437044331100234420ustar00rootroot00000000000000""" WSGI config for iot_example_app project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'iot_example_app.settings') application = get_wsgi_application() django-timescaledb-0.2.13/example/manage.py000077500000000000000000000012371437044331100205540ustar00rootroot00000000000000#!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'iot_example_app.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() django-timescaledb-0.2.13/example/metrics/000077500000000000000000000000001437044331100204125ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/__init__.py000066400000000000000000000000001437044331100225110ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/admin.py000066400000000000000000000000771437044331100220600ustar00rootroot00000000000000from django.contrib import admin # Register your models here. django-timescaledb-0.2.13/example/metrics/apps.py000066400000000000000000000001311437044331100217220ustar00rootroot00000000000000from django.apps import AppConfig class MetricsConfig(AppConfig): name = 'metrics' django-timescaledb-0.2.13/example/metrics/management/000077500000000000000000000000001437044331100225265ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/management/__init__.py000066400000000000000000000000001437044331100246250ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/management/commands/000077500000000000000000000000001437044331100243275ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/management/commands/__init__.py000066400000000000000000000000001437044331100264260ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/management/commands/load_temperature.py000066400000000000000000000011221437044331100302310ustar00rootroot00000000000000from django.core.management.base import BaseCommand, CommandError from metrics.models import Metric from django.utils import timezone from random import uniform, choice from datetime import timedelta class Command(BaseCommand): help = 'Uses PSUTILS to read any temperature sensor and adds a record' DEVICES = [1234, 1245, 1236] def handle(self, *args, **options): for i in range(1000): timestamp = timezone.now() - timedelta(minutes=i * 5) Metric.objects.create(time=timestamp, temperature=uniform(51.1, 53.3), device=choice(self.DEVICES)) django-timescaledb-0.2.13/example/metrics/migrations/000077500000000000000000000000001437044331100225665ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/migrations/0001_initial.py000066400000000000000000000011541437044331100252320ustar00rootroot00000000000000# Generated by Django 3.1.4 on 2020-12-21 15:32 from django.db import migrations, models import timescale.db.models.fields class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Metric', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('time', timescale.db.models.fields.TimescaleDateTimeField(interval='1 day')), ('temperature', models.FloatField(default=0.0)), ], ), ] django-timescaledb-0.2.13/example/metrics/migrations/0002_auto_20201221_2052.py000066400000000000000000000006541437044331100261770ustar00rootroot00000000000000# Generated by Django 3.1.4 on 2020-12-21 20:52 from django.db import migrations import django.db.models.manager class Migration(migrations.Migration): dependencies = [ ('metrics', '0001_initial'), ] operations = [ migrations.AlterModelManagers( name='metric', managers=[ ('timescale', django.db.models.manager.Manager()), ], ), ] django-timescaledb-0.2.13/example/metrics/migrations/0003_auto_20201222_1121.py000066400000000000000000000015011437044331100261650ustar00rootroot00000000000000# Generated by Django 3.1.4 on 2020-12-22 11:21 from django.db import migrations, models import timescale.db.models.fields class Migration(migrations.Migration): dependencies = [ ('metrics', '0002_auto_20201221_2052'), ] operations = [ migrations.CreateModel( name='SampleTest', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('time', timescale.db.models.fields.TimescaleDateTimeField(interval='1 day')), ('value', models.FloatField(default=0.0)), ], options={ 'abstract': False, }, ), migrations.AlterModelManagers( name='metric', managers=[ ], ), ] django-timescaledb-0.2.13/example/metrics/migrations/0004_metric_device.py000066400000000000000000000005771437044331100264160ustar00rootroot00000000000000# Generated by Django 3.1.4 on 2020-12-22 11:33 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('metrics', '0003_auto_20201222_1121'), ] operations = [ migrations.AddField( model_name='metric', name='device', field=models.IntegerField(default=0), ), ] 0005_rename_sampletest_anothermetricfromtimescalemodel.py000066400000000000000000000005401437044331100357540ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/migrations# Generated by Django 3.2 on 2021-04-20 19:57 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('metrics', '0004_metric_device'), ] operations = [ migrations.RenameModel( old_name='SampleTest', new_name='AnotherMetricFromTimeScaleModel', ), ] django-timescaledb-0.2.13/example/metrics/migrations/__init__.py000066400000000000000000000000001437044331100246650ustar00rootroot00000000000000django-timescaledb-0.2.13/example/metrics/models.py000066400000000000000000000011271437044331100222500ustar00rootroot00000000000000from django.db import models from timescale.db.models.fields import TimescaleDateTimeField from typing import Dict from timescale.db.models.models import TimescaleModel from timescale.db.models.managers import TimescaleManager # Create your models here. class Metric(models.Model): time = TimescaleDateTimeField(interval="1 day") temperature = models.FloatField(default=0.0) device = models.IntegerField(default=0) objects = models.Manager() timescale = TimescaleManager() class AnotherMetricFromTimeScaleModel(TimescaleModel): value = models.FloatField(default=0.0) django-timescaledb-0.2.13/example/metrics/tests.py000066400000000000000000000033121437044331100221250ustar00rootroot00000000000000from timescale.db.models.fields import TimescaleDateTimeField from timescale.db.models.expressions import TimeBucketNG from metrics.models import Metric from django.utils import timezone from dateutil.relativedelta import relativedelta from django.test import TestCase from django.db.models import Avg from timescale.db.models.aggregates import First class TimescaleDBTests(TestCase): def setUp(self): super().setUp() def test_time_bucket_ng(self): timestamp = timezone.now().replace(day=1) # datapoints for current month Metric.objects.create(time=timestamp - relativedelta(days=15), temperature=8) Metric.objects.create(time=timestamp - relativedelta(days=10), temperature=10) # datapoints for last month Metric.objects.create(time=timestamp - relativedelta(months=1, days=15), temperature=14) Metric.objects.create(time=timestamp - relativedelta(months=1, days=10), temperature=12) # get all metrics, monthly aggregated metrics = Metric.timescale.time_bucket_ng('time', '1 month').annotate(Avg('temperature')) # verify self.assertEqual(metrics[0]["temperature__avg"], 9.0) self.assertEqual(metrics[1]["temperature__avg"], 13.0) # get first entry of the monthly aggregated datapoints metrics = (Metric.timescale .values(interval_end=TimeBucketNG('time', f'1 month', output_field=TimescaleDateTimeField(interval='1 month'))) .annotate(temperature__first=First('temperature', 'time'))) # verify # XXX: Remove self.assertEqual(metrics[0]["temperature__first"], 14.0) self.assertEqual(metrics[1]["temperature__first"], 8.0) django-timescaledb-0.2.13/example/metrics/views.py000066400000000000000000000000771437044331100221250ustar00rootroot00000000000000from django.shortcuts import render # Create your views here. django-timescaledb-0.2.13/example/requirements.txt000066400000000000000000000000721437044331100222270ustar00rootroot00000000000000Django psycopg2 django-extensions python-dotenv dateutils django-timescaledb-0.2.13/setup.cfg000066400000000000000000000016341437044331100171360ustar00rootroot00000000000000[metadata] name = django-timescaledb version = 0.2.13 description = A Django database backend for integration with TimescaleDB long_description = file: README.rst url = https://github.com/schlunsen/django-timescaledb author = Rasmus Schlünsen author_email = raller84@gmail.com license = Apache-2.0 License classifiers = Environment :: Web Environment Framework :: Django Framework :: Django :: 3.0 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP :: Dynamic Content [options] include_package_data = true packages = find: django-timescaledb-0.2.13/setup.py000066400000000000000000000000461437044331100170230ustar00rootroot00000000000000from setuptools import setup setup() django-timescaledb-0.2.13/timescale/000077500000000000000000000000001437044331100172575ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/.DS_Store000066400000000000000000000140041437044331100207410ustar00rootroot00000000000000Bud1%dbdsclboolbool  @€ @€ @€ @ E%DSDB`€ @€ @€ @django-timescaledb-0.2.13/timescale/__init__.py000066400000000000000000000000001437044331100213560ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/000077500000000000000000000000001437044331100176445ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/.DS_Store000066400000000000000000000140041437044331100213260ustar00rootroot00000000000000Bud1 endsbwspbackendsbwspblobÉbplist00×  ]ShowStatusBar[ShowPathbar[ShowToolbar[ShowTabView_ContainerShowSidebar\WindowBounds[ShowSidebar  _{{455, 306}, {770, 436}} %1=I`myz{|}~™šbackendsvSrnlong  @€ @€ @€ @ E DSDB `€ @€ @€ @django-timescaledb-0.2.13/timescale/db/__init__.py000066400000000000000000000000001437044331100217430ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/000077500000000000000000000000001437044331100214165ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/.DS_Store000066400000000000000000000140041437044331100231000ustar00rootroot00000000000000Bud1 gisbwspblpostgisbwspblobÉbplist00×  ]ShowStatusBar[ShowPathbar[ShowToolbar[ShowTabView_ContainerShowSidebar\WindowBounds[ShowSidebar  _{{475, 436}, {770, 436}} %1=I`myz{|}~™špostgisvSrnlong postgresqlbwspblobÉbplist00×  ]ShowStatusBar[ShowPathbar[ShowToolbar[ShowTabView_ContainerShowSidebar\WindowBounds[ShowSidebar  _{{475, 436}, {770, 436}} %1=I`myz{|}~™š postgresqlvSrnlong  @€ @€ @€ @ E DSDB `€ @€ @€ @django-timescaledb-0.2.13/timescale/db/backends/__init__.py000066400000000000000000000000001437044331100235150ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/postgis/000077500000000000000000000000001437044331100231065ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/postgis/__init__.py000066400000000000000000000000001437044331100252050ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/postgis/base.py000066400000000000000000000022121437044331100243670ustar00rootroot00000000000000import logging from django.db import ProgrammingError from django.core.exceptions import ImproperlyConfigured from timescale.db.backends.postgis import base_impl from timescale.db.backends.postgis.schema import TimescaleSchemaEditor logger = logging.getLogger(__name__) class DatabaseWrapper(base_impl.backend()): SchemaEditorClass = TimescaleSchemaEditor def prepare_database(self): """Prepare the configured database. This is where we enable the `timescaledb` extension if it isn't enabled yet.""" super().prepare_database() with self.cursor() as cursor: try: cursor.execute('CREATE EXTENSION IF NOT EXISTS timescaledb') except ProgrammingError: # permission denied logger.warning( 'Failed to create "timescaledb" extension. ' 'Usage of timescale capabilities might fail' 'If timescale is needed, make sure you are connected ' 'to the database as a superuser ' 'or add the extension manually.', exc_info=True ) django-timescaledb-0.2.13/timescale/db/backends/postgis/base_impl.py000066400000000000000000000044361437044331100254220ustar00rootroot00000000000000import importlib import logging from django.db.backends.postgresql.base import ( # isort:skip DatabaseWrapper as Psycopg2DatabaseWrapper, ) from django.conf import settings from django.core.exceptions import ImproperlyConfigured logger = logging.getLogger(__name__) def backend(): """Gets the base class for the custom database back-end. This should be the Django PostgreSQL back-end. However, some people are already using a custom back-end from another package. We are nice people and expose an option that allows them to configure the back-end we base upon. As long as the specified base eventually also has the PostgreSQL back-end as a base, then everything should work as intended. """ base_class_name = getattr( settings, "TIMESCALE_DB_BACKEND_BASE", "django.contrib.gis.db.backends.postgis", ) base_class_module = importlib.import_module(base_class_name + ".base") base_class = getattr(base_class_module, "DatabaseWrapper", None) if not base_class: raise ImproperlyConfigured( ( "'%s' is not a valid database back-end." " The module does not define a DatabaseWrapper class." " Check the value of TIMESCALE_EXTRA_DB_BACKEND_BASE." ) % base_class_name ) if isinstance(base_class, Psycopg2DatabaseWrapper): raise ImproperlyConfigured( ( "'%s' is not a valid database back-end." " It does inherit from the PostgreSQL back-end." " Check the value of TIMESCALE_EXTRA_DB_BACKEND_BASE." ) % base_class_name ) return base_class def schema_editor(): """Gets the base class for the schema editor. We have to use the configured base back-end's schema editor for this. """ return backend().SchemaEditorClass def introspection(): """Gets the base class for the introspection class. We have to use the configured base back-end's introspection class for this. """ return backend().introspection_class def operations(): """Gets the base class for the operations class. We have to use the configured base back-end's operations class for this. """ return backend().ops_class django-timescaledb-0.2.13/timescale/db/backends/postgis/schema.py000066400000000000000000000140551437044331100247250ustar00rootroot00000000000000from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor from timescale.db.models.fields import TimescaleDateTimeField class TimescaleSchemaEditor(PostGISSchemaEditor): sql_is_hypertable = '''SELECT * FROM timescaledb_information.hypertables WHERE hypertable_name = {table}{extra_condition}''' sql_assert_is_hypertable = ( 'DO $do$ BEGIN ' 'IF EXISTS ( ' + sql_is_hypertable + ') ' 'THEN NULL; ' 'ELSE RAISE EXCEPTION {error_message}; ' 'END IF;' 'END; $do$' ) sql_assert_is_not_hypertable = ( 'DO $do$ BEGIN ' 'IF EXISTS ( ' + sql_is_hypertable + ') ' 'THEN RAISE EXCEPTION {error_message}; ' 'ELSE NULL; ' 'END IF;' 'END; $do$' ) sql_drop_primary_key = 'ALTER TABLE {table} DROP CONSTRAINT {pkey}' sql_add_hypertable = ( "SELECT create_hypertable(" "{table}, {partition_column}, " "chunk_time_interval => interval {interval}, " "migrate_data => {migrate})" ) sql_set_chunk_time_interval = 'SELECT set_chunk_time_interval({table}, interval {interval})' sql_hypertable_is_in_schema = '''hypertable_schema = {schema_name}''' def _assert_is_hypertable(self, model): """ Assert if the table is a hyper table """ table = self.quote_value(model._meta.db_table) error_message = self.quote_value("assert failed - " + table + " should be a hyper table") extra_condition = self._get_extra_condition() sql = self.sql_assert_is_hypertable.format(table=table, error_message=error_message, extra_condition=extra_condition) self.execute(sql) def _assert_is_not_hypertable(self, model): """ Assert if the table is not a hyper table """ table = self.quote_value(model._meta.db_table) error_message = self.quote_value("assert failed - " + table + " should not be a hyper table") extra_condition = self._get_extra_condition() sql = self.sql_assert_is_not_hypertable.format(table=table, error_message=error_message, extra_condition=extra_condition) self.execute(sql) def _drop_primary_key(self, model): """ Hypertables can't partition if the primary key is not the partition column. So we drop the mandatory primary key django creates. """ db_table = model._meta.db_table table = self.quote_name(db_table) pkey = self.quote_name(f'{db_table}_pkey') sql = self.sql_drop_primary_key.format(table=table, pkey=pkey) self.execute(sql) def _create_hypertable(self, model, field, should_migrate=False): """ Create the hypertable with the partition column being the field. """ # assert that the table is not already a hypertable self._assert_is_not_hypertable(model) # drop primary key of the table self._drop_primary_key(model) partition_column = self.quote_value(field.column) interval = self.quote_value(field.interval) table = self.quote_value(model._meta.db_table) migrate = "true" if should_migrate else "false" if should_migrate and getattr(settings, "TIMESCALE_MIGRATE_HYPERTABLE_WITH_FRESH_TABLE", False): # TODO migrate with fresh table [https://github.com/schlunsen/django-timescaledb/issues/16] raise NotImplementedError() else: sql = self.sql_add_hypertable.format( table=table, partition_column=partition_column, interval=interval, migrate=migrate ) self.execute(sql) def _set_chunk_time_interval(self, model, field): """ Change time interval for hypertable """ # assert if already a hypertable self._assert_is_hypertable(model) table = self.quote_value(model._meta.db_table) interval = self.quote_value(field.interval) sql = self.sql_set_chunk_time_interval.format(table=table, interval=interval) self.execute(sql) def create_model(self, model): super().create_model(model) # scan if any field is of instance `TimescaleDateTimeField` for field in model._meta.local_fields: if isinstance(field, TimescaleDateTimeField): # create hypertable, with the field as partition column self._create_hypertable(model, field) break def add_field(self, model, field): super().add_field(model, field) # check if this field is type `TimescaleDateTimeField` if isinstance(field, TimescaleDateTimeField): # migrate existing table to hypertable self._create_hypertable(model, field, True) def alter_field(self, model, old_field, new_field, strict=False): super().alter_field(model, old_field, new_field, strict) # check if old_field is not type `TimescaleDateTimeField` and new_field is if not isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField): # migrate existing table to hypertable self._create_hypertable(model, new_field, True) # check if old_field and new_field is type `TimescaleDateTimeField` and `interval` is changed elif isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField) \ and old_field.interval != new_field.interval: # change chunk-size self._set_chunk_time_interval(model, new_field) def _get_extra_condition(self): extra_condition = '' try: if self.connection.schema_name: schema_name = self.quote_value(self.connection.schema_name) extra_condition = ' AND ' + self.sql_hypertable_is_in_schema.format(schema_name=schema_name) except: pass return extra_condition django-timescaledb-0.2.13/timescale/db/backends/postgresql/000077500000000000000000000000001437044331100236215ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/postgresql/__init__.py000066400000000000000000000000001437044331100257200ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/backends/postgresql/base.py000066400000000000000000000022201437044331100251010ustar00rootroot00000000000000import logging from django.db import ProgrammingError from django.core.exceptions import ImproperlyConfigured from timescale.db.backends.postgresql import base_impl from timescale.db.backends.postgresql.schema import TimescaleSchemaEditor logger = logging.getLogger(__name__) class DatabaseWrapper(base_impl.backend()): SchemaEditorClass = TimescaleSchemaEditor def prepare_database(self): """Prepare the configured database. This is where we enable the `timescaledb` extension if it isn't enabled yet.""" super().prepare_database() with self.cursor() as cursor: try: cursor.execute('CREATE EXTENSION IF NOT EXISTS timescaledb') except ProgrammingError: # permission denied logger.warning( 'Failed to create "timescaledb" extension. ' 'Usage of timescale capabilities might fail' 'If timescale is needed, make sure you are connected ' 'to the database as a superuser ' 'or add the extension manually.', exc_info=True ) django-timescaledb-0.2.13/timescale/db/backends/postgresql/base_impl.py000066400000000000000000000044251437044331100261330ustar00rootroot00000000000000import importlib import logging from django.db.backends.postgresql.base import ( # isort:skip DatabaseWrapper as Psycopg2DatabaseWrapper, ) from django.conf import settings from django.core.exceptions import ImproperlyConfigured logger = logging.getLogger(__name__) def backend(): """Gets the base class for the custom database back-end. This should be the Django PostgreSQL back-end. However, some people are already using a custom back-end from another package. We are nice people and expose an option that allows them to configure the back-end we base upon. As long as the specified base eventually also has the PostgreSQL back-end as a base, then everything should work as intended. """ base_class_name = getattr( settings, "TIMESCALE_DB_BACKEND_BASE", "django.db.backends.postgresql", ) base_class_module = importlib.import_module(base_class_name + ".base") base_class = getattr(base_class_module, "DatabaseWrapper", None) if not base_class: raise ImproperlyConfigured( ( "'%s' is not a valid database back-end." " The module does not define a DatabaseWrapper class." " Check the value of TIMESCALE_EXTRA_DB_BACKEND_BASE." ) % base_class_name ) if isinstance(base_class, Psycopg2DatabaseWrapper): raise ImproperlyConfigured( ( "'%s' is not a valid database back-end." " It does inherit from the PostgreSQL back-end." " Check the value of TIMESCALE_EXTRA_DB_BACKEND_BASE." ) % base_class_name ) return base_class def schema_editor(): """Gets the base class for the schema editor. We have to use the configured base back-end's schema editor for this. """ return backend().SchemaEditorClass def introspection(): """Gets the base class for the introspection class. We have to use the configured base back-end's introspection class for this. """ return backend().introspection_class def operations(): """Gets the base class for the operations class. We have to use the configured base back-end's operations class for this. """ return backend().ops_class django-timescaledb-0.2.13/timescale/db/backends/postgresql/schema.py000066400000000000000000000141071437044331100254360ustar00rootroot00000000000000from django.conf import settings from django.db.backends.postgresql.schema import DatabaseSchemaEditor from timescale.db.models.fields import TimescaleDateTimeField class TimescaleSchemaEditor(DatabaseSchemaEditor): sql_is_hypertable = '''SELECT * FROM timescaledb_information.hypertables WHERE hypertable_name = {table}{extra_condition}''' sql_assert_is_hypertable = ( 'DO $do$ BEGIN ' 'IF EXISTS ( ' + sql_is_hypertable + ') ' 'THEN NULL; ' 'ELSE RAISE EXCEPTION {error_message}; ' 'END IF;' 'END; $do$' ) sql_assert_is_not_hypertable = ( 'DO $do$ BEGIN ' 'IF EXISTS ( ' + sql_is_hypertable + ') ' 'THEN RAISE EXCEPTION {error_message}; ' 'ELSE NULL; ' 'END IF;' 'END; $do$' ) sql_drop_primary_key = 'ALTER TABLE {table} DROP CONSTRAINT {pkey}' sql_add_hypertable = ( "SELECT create_hypertable(" "{table}, {partition_column}, " "chunk_time_interval => interval {interval}, " "migrate_data => {migrate})" ) sql_set_chunk_time_interval = 'SELECT set_chunk_time_interval({table}, interval {interval})' sql_hypertable_is_in_schema = '''hypertable_schema = {schema_name}''' def _assert_is_hypertable(self, model): """ Assert if the table is a hyper table """ table = self.quote_value(model._meta.db_table) error_message = self.quote_value("assert failed - " + table + " should be a hyper table") extra_condition = self._get_extra_condition() sql = self.sql_assert_is_hypertable.format(table=table, error_message=error_message, extra_condition=extra_condition) self.execute(sql) def _assert_is_not_hypertable(self, model): """ Assert if the table is not a hyper table """ table = self.quote_value(model._meta.db_table) error_message = self.quote_value("assert failed - " + table + " should not be a hyper table") extra_condition = self._get_extra_condition() sql = self.sql_assert_is_not_hypertable.format(table=table, error_message=error_message, extra_condition=extra_condition) self.execute(sql) def _drop_primary_key(self, model): """ Hypertables can't partition if the primary key is not the partition column. So we drop the mandatory primary key django creates. """ db_table = model._meta.db_table table = self.quote_name(db_table) pkey = self.quote_name(f'{db_table}_pkey') sql = self.sql_drop_primary_key.format(table=table, pkey=pkey) self.execute(sql) def _create_hypertable(self, model, field, should_migrate=False): """ Create the hypertable with the partition column being the field. """ # assert that the table is not already a hypertable self._assert_is_not_hypertable(model) # drop primary key of the table self._drop_primary_key(model) partition_column = self.quote_value(field.column) interval = self.quote_value(field.interval) table = self.quote_value(model._meta.db_table) migrate = "true" if should_migrate else "false" if should_migrate and getattr(settings, "TIMESCALE_MIGRATE_HYPERTABLE_WITH_FRESH_TABLE", False): # TODO migrate with fresh table [https://github.com/schlunsen/django-timescaledb/issues/16] raise NotImplementedError() else: sql = self.sql_add_hypertable.format( table=table, partition_column=partition_column, interval=interval, migrate=migrate ) self.execute(sql) def _set_chunk_time_interval(self, model, field): """ Change time interval for hypertable """ # assert if already a hypertable self._assert_is_hypertable(model) table = self.quote_value(model._meta.db_table) interval = self.quote_value(field.interval) sql = self.sql_set_chunk_time_interval.format(table=table, interval=interval) self.execute(sql) def create_model(self, model): super().create_model(model) # scan if any field is of instance `TimescaleDateTimeField` for field in model._meta.local_fields: if isinstance(field, TimescaleDateTimeField): # create hypertable, with the field as partition column self._create_hypertable(model, field) break def add_field(self, model, field): super().add_field(model, field) # check if this field is type `TimescaleDateTimeField` if isinstance(field, TimescaleDateTimeField): # migrate existing table to hypertable self._create_hypertable(model, field, True) def alter_field(self, model, old_field, new_field, strict=False): super().alter_field(model, old_field, new_field, strict) # check if old_field is not type `TimescaleDateTimeField` and new_field is if not isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField): # migrate existing table to hypertable self._create_hypertable(model, new_field, True) # check if old_field and new_field is type `TimescaleDateTimeField` and `interval` is changed elif isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField) \ and old_field.interval != new_field.interval: # change chunk-size self._set_chunk_time_interval(model, new_field) def _get_extra_condition(self): extra_condition = '' try: if self.connection.schema_name: schema_name = self.quote_value(self.connection.schema_name) extra_condition = ' AND ' + self.sql_hypertable_is_in_schema.format(schema_name=schema_name) except: pass return extra_condition django-timescaledb-0.2.13/timescale/db/models/000077500000000000000000000000001437044331100211275ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/models/__init__.py000066400000000000000000000000001437044331100232260ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/db/models/aggregates.py000066400000000000000000000021131437044331100236070ustar00rootroot00000000000000from django.db import models from django.contrib.postgres.fields import ArrayField from django.db.models.fields import FloatField class Histogram(models.Aggregate): """ Implementation of the histogram function from Timescale. Read more about it here - https://docs.timescale.com/latest/using-timescaledb/reading-data#histogram Response: """ function = 'histogram' name = 'histogram' output_field = ArrayField(models.FloatField()) def __init__(self, expression, min_value, max_value, bucket): super().__init__(expression, min_value, max_value, bucket) class Last(models.Aggregate): function = 'last' name = 'last' output_field = FloatField() def __init__(self, expression, bucket): super().__init__(expression, bucket) class First(models.Aggregate): function = 'first' name = 'first' output_field = FloatField() def __init__(self, expression, bucket): super().__init__(expression, bucket)django-timescaledb-0.2.13/timescale/db/models/expressions.py000066400000000000000000000067571437044331100241020ustar00rootroot00000000000000from django.db import models from django.contrib.postgres.fields import ArrayField from django.db.models.functions.mixins import ( FixDurationInputMixin, NumericOutputFieldMixin, ) from django.utils import timezone from datetime import timedelta from timescale.db.models.fields import TimescaleDateTimeField class Interval(models.Func): """ A helper class to format the interval used by the time_bucket_gapfill function to generate correct timestamps. Accepts an interval e.g '1 day', '5 days', '1 hour' """ function = "INTERVAL" template = "%(function)s %(expressions)s" def __init__(self, interval, *args, **kwargs): if not isinstance(interval, models.Value): interval = models.Value(interval) super().__init__(interval, *args, **kwargs) class TimeBucket(models.Func): """ Implementation of the time_bucket function from Timescale. Read more about it here - https://docs.timescale.com/latest/using-timescaledb/reading-data#time-bucket Response: [ {'bucket': '2020-12-22T10:00:00+00:00', 'devices': 12}, {'bucket': '2020-12-22T09:00:00+00:00', 'devices': 12}, {'bucket': '2020-12-22T08:00:00+00:00', 'devices': 12}, {'bucket': '2020-12-22T07:00:00+00:00', 'devices': 12}, ] """ function = "time_bucket" name = "time_bucket" def __init__(self, expression, interval, *args, **kwargs): if not isinstance(interval, models.Value): interval = models.Value(interval) output_field = TimescaleDateTimeField(interval=interval) super().__init__(interval, expression, output_field=output_field) class TimeBucketNG(models.Func): """ Implementation of the time_bucket_ng function from Timescale. Read more about it here - https://docs.timescale.com/api/latest/hyperfunctions/time_bucket_ng/#timescaledb-experimental-time-bucket-ng Response: [ {'bucket': '2020-12-01T00:00:00+00:00', 'devices': 12}, {'bucket': '2020-11-01T00:00:00+00:00', 'devices': 12}, {'bucket': '2020-10-01T00:00:00+00:00', 'devices': 12}, {'bucket': '2020-09-01T00:00:00+00:00', 'devices': 12}, ] """ function = "timescaledb_experimental.time_bucket_ng" name = "timescaledb_experimental.time_bucket_ng" def __init__(self, expression, interval, *args, **kwargs): if not isinstance(interval, models.Value): interval = models.Value(interval) output_field = TimescaleDateTimeField(interval=interval) super().__init__(interval, expression, output_field=output_field) class TimeBucketGapFill(models.Func): """ IMplementation of the time_bucket_gapfill function from Timescale Read more about it here - https://docs.timescale.com/latest/using-timescaledb/reading-data#gap-filling Response: [ ... {'bucket': '2020-12-22T11:36:00+00:00', 'temperature__avg': 52.7127405105567}, {'bucket': '2020-12-22T11:42:00+00:00', 'temperature__avg': None}, ... ] """ function = "time_bucket_gapfill" name = "time_bucket_gapfill" def __init__( self, expression, interval, start, end, datapoints=None, *args, **kwargs ): if not isinstance(interval, models.Value): interval = Interval(interval) if datapoints: interval = interval / datapoints output_field = TimescaleDateTimeField(interval=interval) super().__init__(interval, expression, start, end, output_field=output_field) django-timescaledb-0.2.13/timescale/db/models/fields.py000066400000000000000000000006001437044331100227430ustar00rootroot00000000000000from django.db.models import DateTimeField class TimescaleDateTimeField(DateTimeField): def __init__(self, *args, interval, **kwargs): self.interval = interval super().__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() kwargs['interval'] = self.interval return name, path, args, kwargsdjango-timescaledb-0.2.13/timescale/db/models/managers.py000066400000000000000000000017751437044331100233100ustar00rootroot00000000000000from django.db import models from timescale.db.models.querysets import * from typing import Optional class TimescaleManager(models.Manager): """ A custom model manager specifically designed around the Timescale functions and tooling that has been ported to Django's ORM. """ def get_queryset(self): return TimescaleQuerySet(self.model, using=self._db) def time_bucket(self, field, interval): return self.get_queryset().time_bucket(field, interval) def time_bucket_ng(self, field, interval): return self.get_queryset().time_bucket_ng(field, interval) def time_bucket_gapfill(self, field: str, interval: str, start: datetime, end: datetime, datapoints: Optional[int] = None): return self.get_queryset().time_bucket_gapfill(field, interval, start, end, datapoints) def histogram(self, field: str, min_value: float, max_value: float, num_of_buckets: int = 5): return self.get_queryset().histogram(field, min_value, max_value, num_of_buckets) django-timescaledb-0.2.13/timescale/db/models/models.py000066400000000000000000000011001437044331100227540ustar00rootroot00000000000000from django.db import models from timescale.db.models.fields import TimescaleDateTimeField from timescale.db.models.managers import TimescaleManager class TimescaleModel(models.Model): """ A helper class for using Timescale within Django, has the TimescaleManager and TimescaleDateTimeField already present. This is an abstract class it should be inheritted by another class for use. """ time = TimescaleDateTimeField(interval="1 day") objects = models.Manager() timescale = TimescaleManager() class Meta: abstract = Truedjango-timescaledb-0.2.13/timescale/db/models/querysets.py000066400000000000000000000037221437044331100235510ustar00rootroot00000000000000from django.db import models from timescale.db.models.expressions import TimeBucket, TimeBucketGapFill, TimeBucketNG from timescale.db.models.aggregates import Histogram from typing import Dict, Optional from datetime import datetime class TimescaleQuerySet(models.QuerySet): def time_bucket(self, field: str, interval: str, annotations: Dict = None): """ Wraps the TimescaleDB time_bucket function into a queryset method. """ if annotations: return self.values(bucket=TimeBucket(field, interval)).order_by('-bucket').annotate(**annotations) return self.values(bucket=TimeBucket(field, interval)).order_by('-bucket') def time_bucket_ng(self, field: str, interval: str, annotations: Dict = None): """ Wraps the TimescaleDB time_bucket_ng function into a queryset method. """ if annotations: return self.values(bucket=TimeBucketNG(field, interval)).order_by('-bucket').annotate(**annotations) return self.values(bucket=TimeBucketNG(field, interval)).order_by('-bucket') def time_bucket_gapfill(self, field: str, interval: str, start: datetime, end: datetime, datapoints: Optional[int] = None): """ Wraps the TimescaleDB time_bucket_gapfill function into a queryset method. """ return self.values(bucket=TimeBucketGapFill(field, interval, start, end, datapoints)) def histogram(self, field: str, min_value: float, max_value: float, num_of_buckets: int = 5): """ Wraps the TimescaleDB histogram function into a queryset method. """ return self.values(histogram=Histogram(field, min_value, max_value, num_of_buckets)) def to_list(self, normalise_datetimes: bool = False): if normalise_datetimes: normalised = [] for b in list(self): b["bucket"] = b["bucket"].isoformat() normalised.append(b) return normalised return list(self) django-timescaledb-0.2.13/timescale/tests/000077500000000000000000000000001437044331100204215ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/tests/__init__.py000066400000000000000000000000001437044331100225200ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/tests/conftest.py000066400000000000000000000000001437044331100226060ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/tests/factories.py000066400000000000000000000000001437044331100227400ustar00rootroot00000000000000django-timescaledb-0.2.13/timescale/tests/test_models.py000066400000000000000000000000001437044331100233030ustar00rootroot00000000000000