Repository: chaitin/django-pg-timepart
Branch: master
Commit: 129f5ddd11c6
Files: 39
Total size: 64.0 KB
Directory structure:
gitextract_of6gshgr/
├── .gitignore
├── .readthedocs.yml
├── .travis.yml
├── CONTRIBUTING.rst
├── CONTRIBUTORS
├── LICENSE
├── README.rst
├── codecov.yml
├── dev/
│ ├── docker-compose.yml
│ └── postgres_init.sh
├── docs/
│ ├── Makefile
│ ├── readthedocs.txt
│ └── source/
│ ├── _templates/
│ │ └── sidebarlogo.html
│ ├── api.rst
│ ├── conf.py
│ ├── decorators.rst
│ ├── design.rst
│ ├── index.rst
│ ├── installation.rst
│ └── signals.rst
├── pg_partitioning/
│ ├── __init__.py
│ ├── apps.py
│ ├── constants.py
│ ├── decorators.py
│ ├── manager.py
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── patch/
│ │ ├── __init__.py
│ │ └── schema.py
│ ├── shortcuts.py
│ └── signals.py
├── run_test.py
├── setup.cfg
├── setup.py
├── tests/
│ ├── __init__.py
│ ├── models.py
│ └── tests.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.egg-info
dist/
build/
docs/_build
docs/source/.doctrees/
__pycache__/
.cache
*.sqlite3
.tox/
*.swp
*.pyc
.coverage*
.pytest_cache
node_modules
venv/
# IDE and Tooling files
.idea/*
*~
# macOS
.DS_Store
================================================
FILE: .readthedocs.yml
================================================
version: 2
build:
image: latest
sphinx:
configuration: docs/source/conf.py
python:
version: 3.6
install:
- requirements: docs/readthedocs.txt
================================================
FILE: .travis.yml
================================================
dist: xenial
language: python
cache: pip
sudo: required
matrix:
fast_finish: true
include:
- { python: "3.6", env: DJANGO=2.0 }
- { python: "3.6", env: DJANGO=2.1 }
- { python: "3.6", env: DJANGO=2.2 }
- { python: "3.7", env: DJANGO=2.0 }
- { python: "3.7", env: DJANGO=2.1 }
- { python: "3.7", env: DJANGO=2.2 }
- { python: "3.7", env: DJANGO=master }
- { python: "3.6", env: TOXENV=lint }
- { python: "3.6", env: TOXENV=docs }
allow_failures:
- env: TOXENV=docs
- env: DJANGO=master
services:
- docker
before_install:
- sudo /etc/init.d/postgresql stop
install:
- pip install '.[dev]' tox-travis
before_script:
- docker-compose -f dev/docker-compose.yml up -d
- sleep 5
script:
- tox
after_success:
- pip install codecov
- codecov -e TOXENV,DJANGO
notifications:
email: false
================================================
FILE: CONTRIBUTING.rst
================================================
Contributing Guidelines
=======================
Issue tracker
-------------
您可以通过 `issue tracker <https://github.com/chaitin/django-pg-partitioning/issues>`__ 提交改进建议、缺陷报告或功能需求,但 **必须** 遵守以下规范:
* **请勿** 重复提交相似的主题或内容。
* **请勿** 讨论任何与本项目无关的内容。
* 我们非常欢迎您提交程序缺陷报告,但在此之前,请确保您已经完整阅读过相关文档,并已经做了一些必要的调查,确定错误并非您自身造成的。在您编写程序缺陷报告时,
请详细描述您所出现的问题和复现步骤,并附带详细的信息,以便我们能尽快定位问题。
----
You can submit improvement suggestions, bug reports, or feature requests through the `issue tracker <https://github.com/chaitin/django-pg-partitioning/issues>`_,
but you **MUST** adhere to the following specifications:
* **Do not** submit similar topics or content repeatedly.
* **Do not** discuss any content not related to this project.
* We welcome you to submit a bug report, but before doing so, please make sure that you have read the documentation in its entirety and
have done some necessary investigations to determine that the error is not yours. When you write a bug report, Please describe in detail
the problem and recurring steps that you have with detailed information so that we can locate the problem as quickly as possible.
Code guidelines
---------------
* 本项目采用 `语义化版本 2.0.0 <https://semver.org/spec/v2.0.0.html>`_
* 本项目使用了 `flask8` `isort` `black` 等代码静态检查工具。提交的代码 **必须** 通过 `lint` 工具检查。某些特殊情况不符合规范的部分,需要按照检查工具要求的方式具体标记出来。
* 公开的 API **必须** 使用 Type Hint 并编写 Docstrings,其他部分 **建议** 使用并在必要的地方为代码编写注释,增强代码的可读性。
* **必须** 限定非 Development 的外部依赖的模块版本为某一个完全兼容的系列。
相关文档:
| `Google Python Style Guide <https://github.com/google/styleguide/blob/gh-pages/pyguide.md>`_
| `PEP 8 Style Guide for Python Code <https://www.python.org/dev/peps/pep-0008/>`_
----
* This project uses a `Semantic Version 2.0.0 <https://semver.org/spec/v2.0.0.html>`_
* This project uses a code static check tool such as `flask8` `isort` `black`. The submitted code **MUST** be checked by the `lint` tool.
Some special cases that do not meet the specifications need to be specifically marked in the way required by the inspection tool.
* The public API **MUST** use Type Hint and write Docstrings, other parts **SHOULD** use it and write comments to the code where necessary
to enhance the readability of code.
* External dependencies published with the project **MUST** at least define a fully compatible version family.
Related documents:
| `Google Python Style Guide <https://github.com/google/styleguide/blob/gh-pages/pyguide.md>`_
| `PEP 8 Style Guide for Python Code <https://www.python.org/dev/peps/pep-0008/>`_
================================================
FILE: CONTRIBUTORS
================================================
virusdefender <virusdefender@outlook.com>
monouno <xingji2163@163.com>
================================================
FILE: LICENSE
================================================
Copyright (c) 2019 Chaitin Tech Co., Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.rst
================================================
django-pg-partitioning
======================
.. image:: https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square
:target: https://raw.githubusercontent.com/chaitin/django-pg-partitioning/master/LICENSE
.. image:: https://img.shields.io/badge/Django-2.x-green.svg?style=flat-square&logo=django
:target: https://www.djangoproject.com/
.. image:: https://img.shields.io/badge/PostgreSQL-11-lightgrey.svg?style=flat-square&logo=postgresql
:target: https://www.postgresql.org/
.. image:: https://readthedocs.org/projects/django-pg-partitioning/badge/?version=latest&style=flat-square
:target: https://django-pg-partitioning.readthedocs.io
.. image:: https://img.shields.io/pypi/v/django-pg-partitioning.svg?style=flat-square
:target: https://pypi.org/project/django-pg-partitioning/
.. image:: https://api.travis-ci.org/chaitin/django-pg-partitioning.svg?branch=master
:target: https://travis-ci.org/chaitin/django-pg-partitioning
.. image:: https://api.codacy.com/project/badge/Grade/c872699c1b254e90b540b053343d1e81
:target: https://www.codacy.com/app/xingji2163/django-pg-partitioning?utm_source=github.com&utm_medium=referral&utm_content=chaitin/django-pg-partitioning&utm_campaign=Badge_Grade
.. image:: https://codecov.io/gh/chaitin/django-pg-partitioning/branch/master/graph/badge.svg
:target: https://codecov.io/gh/chaitin/django-pg-partitioning
⚠️
----
目前已经有了更好用的 Django 插件(比如 django-postgres-extra)使得基于 Django 开发的项目能够方便地使用 PostgreSQL 数据库的高级功能,因此本项目不再维护。你依然可以 fork 本项目并进行二次开发,祝你生活愉快 :)
There are already better Django plugins (such as django-postgres-extra) that make it easy for Django-based projects to use the advanced features of PostgreSQL databases, so this project is no longer maintained. You can still fork this project and do secondary development, have a nice life :)
----
一个支持 PostgreSQL 11 原生表分区的 Django 扩展,使您可以在 Django 中创建分区表并管理它们。目前它支持两种分区类型:
- 时间范围分区(Time Range Partitioning):将时序数据分开存储到不同的时间范围分区表中,支持创建连续且不重叠的时间范围分区并进行归档管理。
- 列表分区(List Partitioning):根据分区字段的确定值将数据分开存储到不同的分区表中。
A Django extension that supports PostgreSQL 11 native table partitioning, allowing you to create partitioned tables in Django
and manage them. Currently it supports the following two partition types:
- **Time Range Partitioning**: Separate time series data into different time range partition tables,
support the creation of continuous and non-overlapping time range partitions and archival management.
- **List Partitioning**: Store data separately into different partition tables based on the determined values of the partition key.
Documentation
https://django-pg-partitioning.readthedocs.io
.. image:: https://raw.githubusercontent.com/chaitin/django-pg-partitioning/master/docs/source/_static/carbon.png
:align: center
TODO
----
- Improve the details of the function.
- Improve documentation and testing.
- Optimization implementation.
maybe more...
Contributing
------------
If you want to contribute to a project and make it better, you help is very welcome!
Please read through `Contributing Guidelines <https://github.com/chaitin/django-pg-partitioning/blob/master/CONTRIBUTING.rst>`__.
License
-------
This project is licensed under the MIT. Please see `LICENSE <https://raw.githubusercontent.com/chaitin/django-pg-partitioning/master/LICENSE>`_.
Project Practice
----------------
.. image:: https://raw.githubusercontent.com/chaitin/django-pg-timepart/master/docs/source/_static/safeline.svg?sanitize=true
:target: https://www.chaitin.cn/en/safeline
================================================
FILE: codecov.yml
================================================
coverage:
precision: 2
round: down
range: "80...100"
status:
project: yes
patch: no
changes: no
comment: off
================================================
FILE: dev/docker-compose.yml
================================================
version: "3"
services:
postgres:
image: postgres:${POSTGRES_VERSION:-11.1-alpine}
container_name: postgres
restart: always
volumes:
- ./postgres_init.sh:/docker-entrypoint-initdb.d/postgres_init.sh
environment:
- POSTGRES_DB=test
- POSTGRES_USER=test
- POSTGRES_PASSWORD=test
ports:
- "127.0.0.1:5432:5432"
================================================
FILE: dev/postgres_init.sh
================================================
#!/usr/bin/env bash
mkdir /tmp/data1 /tmp/data2
psql -U test -c "CREATE TABLESPACE data1 LOCATION '/tmp/data1'"
psql -U test -c "CREATE TABLESPACE data2 LOCATION '/tmp/data2'"
================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = django-partitioning
SOURCEDIR = source
BUILDDIR = _build
.PHONY: all
all:
@$(SPHINXBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/"
.PHONY: help
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
================================================
FILE: docs/readthedocs.txt
================================================
alabaster==0.7.12
sphinx==1.8.3
sphinxcontrib-napoleon==0.7
Pygments==2.3.1
psycopg2-binary==2.7.6.1
Django>=2.0,<2.2
python-dateutil~=2.7
================================================
FILE: docs/source/_templates/sidebarlogo.html
================================================
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/logo.png', 1) }}" width="165px" style="margin-left: -20px; position: relative" />
</a>
<br/>
</p>
================================================
FILE: docs/source/api.rst
================================================
API Reference
=============
.. py:currentmodule:: pg_partitioning.manager
Time Range Partitioning
-----------------------
.. autoclass:: TimeRangePartitionManager
:members:
.. autoclass:: PartitionConfig
:members: period, interval, attach_tablespace, detach_tablespace, save
.. autoclass:: PartitionLog
:members: is_attached, detach_time, save, delete
List Partitioning
-----------------
.. autoclass:: ListPartitionManager
:members:
.. py:currentmodule:: pg_partitioning.shortcuts
Shortcuts
---------
.. automodule:: pg_partitioning.shortcuts
:members: truncate_table, set_tablespace, drop_table
================================================
FILE: docs/source/conf.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import django
from django.conf import settings
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath('./'))
settings.configure(
INSTALLED_APPS=(
"pg_partitioning",
),
)
django.setup()
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'alabaster',
]
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = 'django-pg-partitioning'
copyright = '2019, Chaitin Tech'
author = 'Boyce Li'
language = 'en'
exclude_patterns = ['_build']
pygments_style = 'sphinx'
html_theme = 'alabaster'
html_theme_options = {
'github_user': 'chaitin',
'github_repo': 'django-pg-partitioning',
'github_type': 'star',
'github_banner': 'true',
'show_powered_by': 'false',
'code_font_size': '14px',
}
html_static_path = ['_static']
html_sidebars = {
'**': [
'sidebarlogo.html',
'navigation.html',
'searchbox.html',
]
}
htmlhelp_basename = 'django-pg-partitioning-doc'
man_pages = [
(master_doc, 'django-pg-partitioning', 'django-pg-partitioning Documentation',
[author], 1)
]
texinfo_documents = [
(master_doc, 'django-pg-partitioning', 'django-pg-partitioning Documentation',
author, 'django-pg-partitioning', 'A Django extension that supports PostgreSQL 11 time ranges and list partitioning.',
'Miscellaneous'),
]
intersphinx_mapping = {
'https://docs.python.org/3/': None,
'https://pika.readthedocs.io/en/0.10.0/': None,
}
add_module_names = False
================================================
FILE: docs/source/decorators.rst
================================================
Decorators
==========
Now the decorator must be used on a non-abstract model class that has not yet built a table in the database.
If you must use the decorator on a model class that has previously performed a migrate operation, you need
to back up the model's data, then drop the table, and then import the data after you have created a
partitioned table.
.. py:currentmodule:: pg_partitioning.decorators
.. autodata:: TimeRangePartitioning
:annotation:
.. autodata:: ListPartitioning
:annotation:
Post-Decoration
---------------
You can run ``makemigrations`` and ``migrate`` commands to create and apply new migrations.
Once the table has been created, it is not possible to turn a regular table into a partitioned table or vice versa.
================================================
FILE: docs/source/design.rst
================================================
Design
======
A brief description of the architecture of django-pg-partitioning.
Partitioned Table Support
-------------------------
Currently Django does not support creating partitioned tables, so django-partitioning monkey patch ``create_model`` method in
``DatabaseSchemaEditor`` to make it generate SQL statements that create partitioned tables.
Some of the operations that Django applies to regular database tables may not be supported or even conflicted on the partitioned
table, eventually throwing a database exception. Therefore, it is recommended that you read the section on the partition table
in the official database documentation and refer to the relevant implementation inside Django.
Constraint Limitations
----------------------
It is important to note that PostgreSQL table partitioning has some restrictions on field constraints.
In order for the extension to work, we turned off Django's automatically generated primary key constraint, but did not do other legality checks.
For example, if you mistakenly used a unique or foreign key constraint, it will throw an exception directly, which is what you are coding and
it needs to be manually circumvented during use.
Tablespace
----------
``pg_partitioning`` will silently set the tablespace of all local partitioned indexes under one partition to be consistent with
the partition.
Partition Information
---------------------
``pg_partitioning`` saves partition configuration and state information in ``PartitionConfig`` and ``PartitionLog``.
The problem with this is that once this information is inconsistent with the actual situation, ``pg_partitioning``
will not work properly, so you can only fix it manually.
Management
----------
You can use ``Model.partitioning.create_partition`` and ``Model.partitioning.detach_partition`` to automatically create and
archive partitions. In addition setting ``default_detach_tablespace`` and ``default_attach_tablespace``, you can also use the
``set_tablespace`` method of the PartitionLog object to move the partition. See :doc:`api` for details.
================================================
FILE: docs/source/index.rst
================================================
.. include:: ../../README.rst
.. toctree::
:hidden:
:maxdepth: 2
installation
design
decorators
api
signals
Contributing <https://github.com/chaitin/django-pg-partitioning/blob/master/CONTRIBUTING.rst>
Project License <https://raw.githubusercontent.com/chaitin/django-pg-partitioning/master/LICENSE>
================================================
FILE: docs/source/installation.rst
================================================
Installation
============
PyPI
----
.. code-block:: bash
$ pip install django-pg-partitioning
Or you can install from GitHub
.. code-block:: bash
$ pip install git+https://github.com/chaitin/django-pg-partitioning.git@master
Integrate with Django
---------------------
Add ``pg_partitioning`` to ``INSTALLED_APPS`` in settings.py.
Important - Please note 'pg_partitioning' should be loaded earlier than other apps that depend on it::
INSTALLED_APPS = [
'pg_partitioning',
...
]
PARTITION_TIMEZONE = "Asia/Shanghai"
You can specify the time zone referenced by the time range partitioned table via ``PARTITION_TIMEZONE``,
and if it is not specified, ``TIME_ZONE`` value is used.
Post-Installation
-----------------
In your Django root execute the command below to create 'pg_partitioning' database tables::
./manage.py migrate pg_partitioning
================================================
FILE: docs/source/signals.rst
================================================
Signals
=======
Note that these signals are only triggered when the save methods of ``PartitionConfig`` and ``PartitionLog``
You can hook to them for your own needs (for example to create corresponding table index).
.. py:currentmodule:: pg_partitioning.signals
.. autodata:: post_create_partition(sender, partition_log)
:annotation:
.. autodata:: post_attach_partition(sender, partition_log)
:annotation:
.. autodata:: post_detach_partition(sender, partition_log)
:annotation:
================================================
FILE: pg_partitioning/__init__.py
================================================
"""
A Django extension that supports PostgreSQL 11 time ranges and list partitioning.
"""
REQUIRED_DJANGO_VERSION = [(2, 0), (3, 0)]
DJANGO_VERSION_ERROR = "django-pg-partitioning isn't available on the currently installed version of Django."
try:
import django
except ImportError:
raise ImportError(DJANGO_VERSION_ERROR)
if REQUIRED_DJANGO_VERSION[0] > tuple(django.VERSION[:2]) or tuple(django.VERSION[:2]) > REQUIRED_DJANGO_VERSION[1]:
raise ImportError(DJANGO_VERSION_ERROR)
__version__ = "0.11"
default_app_config = "pg_partitioning.apps.AppConfig"
================================================
FILE: pg_partitioning/apps.py
================================================
from django.apps import AppConfig as DefaultAppConfig
class AppConfig(DefaultAppConfig):
name = "pg_partitioning"
def ready(self):
from .patch import schema # noqa
================================================
FILE: pg_partitioning/constants.py
================================================
SQL_CREATE_TIME_RANGE_PARTITION = """\
CREATE TABLE IF NOT EXISTS %(child)s PARTITION OF %(parent)s FOR VALUES FROM (%(date_start)s) TO (%(date_end)s)"""
SQL_CREATE_LIST_PARTITION = """\
CREATE TABLE IF NOT EXISTS %(child)s PARTITION OF %(parent)s FOR VALUES IN (%(value)s)"""
SQL_SET_TABLE_TABLESPACE = """\
ALTER TABLE IF EXISTS %(name)s SET TABLESPACE %(tablespace)s"""
SQL_APPEND_TABLESPACE = " TABLESPACE %(tablespace)s"
SQL_ATTACH_TIME_RANGE_PARTITION = """\
ALTER TABLE IF EXISTS %(parent)s ATTACH PARTITION %(child)s FOR VALUES FROM (%(date_start)s) TO (%(date_end)s)"""
SQL_ATTACH_LIST_PARTITION = """\
ALTER TABLE IF EXISTS %(parent)s ATTACH PARTITION %(child)s FOR VALUES IN (%(value)s)"""
SQL_DETACH_PARTITION = "ALTER TABLE IF EXISTS %(parent)s DETACH PARTITION %(child)s"
SQL_DROP_TABLE = "DROP TABLE IF EXISTS %(name)s"
SQL_TRUNCATE_TABLE = "TRUNCATE TABLE %(name)s"
SQL_DROP_INDEX = "DROP INDEX IF EXISTS %(name)s"
SQL_CREATE_INDEX = "CREATE INDEX IF NOT EXISTS %(name)s ON %(table_name)s USING %(method)s (%(column_name)s)"
SQL_SET_INDEX_TABLESPACE = "ALTER INDEX %(name)s SET TABLESPACE %(tablespace)s"
SQL_GET_TABLE_INDEXES = "SELECT indexname FROM pg_indexes WHERE tablename = %(table_name)s"
DT_FORMAT = "%Y-%m-%d"
class PartitioningType:
Range = "RANGE"
List = "LIST"
class PeriodType:
Day = "Day"
Week = "Week"
Month = "Month"
Year = "Year"
================================================
FILE: pg_partitioning/decorators.py
================================================
import logging
from typing import Type
from django.db import models
from pg_partitioning.manager import ListPartitionManager, TimeRangePartitionManager
logger = logging.getLogger(__name__)
class _PartitioningBase:
def __init__(self, partition_key: str, **options):
self.partition_key = partition_key
self.options = options
def __call__(self, model: Type[models.Model]):
if model._meta.abstract:
raise NotImplementedError("Decorative abstract model classes are not supported.")
class TimeRangePartitioning(_PartitioningBase):
"""Use this decorator to declare the database table corresponding to the model to be partitioned by time range.
Parameters:
partition_key(str): Partition field name of DateTimeField.
options: Currently supports the following keyword parameters:
- default_period(PeriodType): Default partition period.
- default_interval(int): Default detach partition interval.
- default_attach_tablespace(str): Default tablespace for attached tables.
- default_detach_tablespace(str): Default tablespace for attached tables.
Example:
.. code-block:: python
from django.db import models
from django.utils import timezone
from pg_partitioning.decorators import TimeRangePartitioning
@TimeRangePartitioning(partition_key="timestamp")
class MyLog(models.Model):
name = models.TextField(default="Hello World!")
timestamp = models.DateTimeField(default=timezone.now, primary_key=True)
"""
def __call__(self, model: Type[models.Model]):
super().__call__(model)
if model._meta.get_field(self.partition_key).get_internal_type() != models.DateTimeField().get_internal_type():
raise ValueError("The partition_key must be DateTimeField type.")
model.partitioning = TimeRangePartitionManager(model, self.partition_key, self.options)
return model
class ListPartitioning(_PartitioningBase):
"""Use this decorator to declare the database table corresponding to the model to be partitioned by list.
Parameters:
partition_key(str): Partition key name, the type of the key must be one of boolean, text or integer.
Example:
.. code-block:: python
from django.db import models
from django.utils import timezone
from pg_partitioning.decorators import ListPartitioning
@ListPartitioning(partition_key="category")
class MyLog(models.Model):
category = models.TextField(default="A")
timestamp = models.DateTimeField(default=timezone.now, primary_key=True)
"""
def __call__(self, model: Type[models.Model]):
super().__call__(model)
model.partitioning = ListPartitionManager(model, self.partition_key, self.options)
return model
================================================
FILE: pg_partitioning/manager.py
================================================
import datetime
from collections import Iterable
from typing import Optional, Type, Union
import pytz
from dateutil.relativedelta import MO, relativedelta
from django.conf import settings
from django.db import IntegrityError, models
from django.db.models import Q
from django.utils import timezone
from pg_partitioning.shortcuts import double_quote, execute_sql, generate_set_indexes_tablespace_sql, single_quote
from .constants import (
DT_FORMAT,
SQL_APPEND_TABLESPACE,
SQL_ATTACH_LIST_PARTITION,
SQL_CREATE_LIST_PARTITION,
SQL_DETACH_PARTITION,
SQL_SET_TABLE_TABLESPACE,
PartitioningType,
PeriodType,
)
from .models import PartitionConfig, PartitionLog
class _PartitionManagerBase:
type = None
def __init__(self, model: Type[models.Model], partition_key: str, options: dict):
self.model = model
self.partition_key = partition_key
self.options = options
class TimeRangePartitionManager(_PartitionManagerBase):
"""Manage time-based partition APIs."""
type = PartitioningType.Range
@property
def config(self) -> PartitionConfig:
"""Get the latest PartitionConfig instance of this model.
In order to avoid the race condition, we used **select_for_update** when querying.
Returns:
PartitionConfig: The latest PartitionConfig instance of this model.
"""
try:
return PartitionConfig.objects.select_for_update().get(model_label=self.model._meta.label_lower)
except PartitionConfig.DoesNotExist:
try:
return PartitionConfig.objects.create(
model_label=self.model._meta.label_lower,
period=self.options.get("default_period", PeriodType.Month),
interval=self.options.get("default_interval"),
attach_tablespace=self.options.get("default_attach_tablespace"),
detach_tablespace=self.options.get("default_detach_tablespace"),
)
except IntegrityError:
return PartitionConfig.objects.select_for_update().get(model_label=self.model._meta.label_lower)
@property
def latest(self) -> Optional[PartitionLog]:
"""Get the latest PartitionLog instance of this model.
Returns:
Optional[PartitionLog]: The latest PartitionLog instance of this model or none.
"""
return self.config.logs.order_by("-id").first()
@classmethod
def _get_period_bound(cls, date_start, initial, addition_zeros=None, is_week=False, **kwargs):
zeros = {"hour": 0, "minute": 0, "second": 0, "microsecond": 0}
if addition_zeros:
zeros.update(addition_zeros)
def func(): # lazy evaluation
if initial:
start = date_start.replace(**zeros)
if is_week:
start -= relativedelta(days=start.weekday())
else:
start = date_start
end = start + relativedelta(**kwargs, **zeros)
return start, end
return func
def create_partition(self, max_days_to_next_partition: int = 1) -> None:
"""The partition of the next cycle is created according to the configuration.
After modifying the period field, the new period will take effect the next time.
The start time of the new partition is the end time of the previous partition table,
or the start time of the current archive period when no partition exists.
For example:
the current time is June 5, 2018, and the archiving period is one year,
then the start time of the first partition is 00:00:00 on January 1, 2018.
Parameters:
max_days_to_next_partition(int):
If numbers of days remained in current partition is greater than ``max_days_to_next_partition``, no new partitions will be created.
"""
while True:
if max_days_to_next_partition > 0 and self.latest and timezone.now() < (self.latest.end - relativedelta(days=max_days_to_next_partition)):
return
partition_timezone = getattr(settings, "PARTITION_TIMEZONE", None)
if partition_timezone:
partition_timezone = pytz.timezone(partition_timezone)
date_start = timezone.localtime(self.latest.end if self.latest else None, timezone=partition_timezone)
initial = not bool(self.latest)
date_start, date_end = {
PeriodType.Day: self._get_period_bound(date_start, initial, days=+1),
PeriodType.Week: self._get_period_bound(date_start, initial, is_week=True, days=+1, weekday=MO),
PeriodType.Month: self._get_period_bound(date_start, initial, addition_zeros=dict(day=1), months=+1),
PeriodType.Year: self._get_period_bound(date_start, initial, addition_zeros=dict(month=1, day=1), years=+1),
}[self.config.period]()
partition_table_name = "_".join((self.model._meta.db_table, date_start.strftime(DT_FORMAT), date_end.strftime(DT_FORMAT)))
PartitionLog.objects.create(config=self.config, table_name=partition_table_name, start=date_start, end=date_end)
if not max_days_to_next_partition > 0:
return
def attach_partition(self, partition_log: Optional[Iterable] = None, detach_time: Optional[datetime.datetime] = None) -> None:
"""Attach partitions.
Parameters:
partition_log(Optional[Iterable]):
All partitions are attached when you don't specify partitions to attach.
detach_time(Optional[datetime.datetime]):
When the partition specifies the archive time, it will **not** be automatically archived until that time.
"""
if not partition_log:
partition_log = PartitionLog.objects.filter(config=self.config, is_attached=False)
for log in partition_log:
log.is_attached = True
log.detach_time = detach_time
log.save()
def detach_partition(self, partition_log: Optional[Iterable] = None) -> None:
"""Detach partitions.
Parameters:
partition_log(Optional[Iterable]):
Specify a partition to archive. When you don't specify a partition to archive, all partitions that meet the configuration rule are archived.
"""
if not partition_log:
if self.config.interval:
# fmt: off
period = {PeriodType.Day: {"days": 1},
PeriodType.Week: {"weeks": 1},
PeriodType.Month: {"months": 1},
PeriodType.Year: {"years": 1}}[self.config.period]
# fmt: on
now = timezone.now()
detach_timeline = now - self.config.interval * relativedelta(**period)
partition_log = PartitionLog.objects.filter(config=self.config, end__lt=detach_timeline, is_attached=True)
partition_log = partition_log.filter(Q(detach_time=None) | Q(detach_time__lt=now))
else:
return
for log in partition_log:
log.is_attached = False
log.detach_time = None
log.save()
def delete_partition(self, partition_log: Iterable) -> None:
"""Delete partitions.
Parameters:
partition_log(Iterable): The partitions to be deleted.
"""
for log in partition_log:
if log.config == self.config:
log.delete()
def _db_value(value: Union[str, int, bool, None]) -> str:
if value is None:
return "null"
return single_quote(value) if isinstance(value, str) else str(value)
class ListPartitionManager(_PartitionManagerBase):
"""Manage list-based partition APIs."""
type = PartitioningType.List
def create_partition(self, partition_name: str, value: Union[str, int, bool, None], tablespace: str = None) -> None:
"""Create partitions.
Parameters:
partition_name(str): Partition name.
value(Union[str, int, bool, None]): Partition key value.
tablespace(str): Partition tablespace name.
"""
create_partition_sql = SQL_CREATE_LIST_PARTITION % {
"parent": double_quote(self.model._meta.db_table),
"child": double_quote(partition_name),
"value": _db_value(value),
}
if tablespace:
create_partition_sql += SQL_APPEND_TABLESPACE % {"tablespace": tablespace}
execute_sql(create_partition_sql)
execute_sql(generate_set_indexes_tablespace_sql(partition_name, tablespace))
def attach_partition(self, partition_name: str, value: Union[str, int, bool, None], tablespace: str = None) -> None:
"""Attach partitions.
Parameters:
partition_name(str): Partition name.
value(Union[str, int, bool, None]): Partition key value.
tablespace(str): Partition tablespace name.
"""
sql_sequence = list()
if tablespace:
sql_sequence.append(SQL_SET_TABLE_TABLESPACE % {"name": double_quote(partition_name), "tablespace": tablespace})
sql_sequence.extend(generate_set_indexes_tablespace_sql(partition_name, tablespace))
sql_sequence.append(
SQL_ATTACH_LIST_PARTITION % {"parent": double_quote(self.model._meta.db_table), "child": double_quote(partition_name), "value": _db_value(value)}
)
execute_sql(sql_sequence)
def detach_partition(self, partition_name: str, tablespace: str = None) -> None:
"""Detach partitions.
Parameters:
partition_name(str): Partition name.
tablespace(str): Partition tablespace name.
"""
sql_sequence = [SQL_DETACH_PARTITION % {"parent": double_quote(self.model._meta.db_table), "child": double_quote(partition_name)}]
if tablespace:
sql_sequence.append(SQL_SET_TABLE_TABLESPACE % {"name": double_quote(partition_name), "tablespace": tablespace})
sql_sequence.extend(generate_set_indexes_tablespace_sql(partition_name, tablespace))
execute_sql(sql_sequence)
================================================
FILE: pg_partitioning/migrations/0001_initial.py
================================================
# Generated by Django 2.1.7 on 2019-02-17 12:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='PartitionConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('model_label', models.TextField(unique=True)),
('period', models.TextField(default='Month')),
('interval', models.PositiveIntegerField(null=True)),
('attach_tablespace', models.TextField(null=True)),
('detach_tablespace', models.TextField(null=True)),
],
),
migrations.CreateModel(
name='PartitionLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('table_name', models.TextField(unique=True)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('is_attached', models.BooleanField(default=True)),
('detach_time', models.DateTimeField(null=True)),
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='pg_partitioning.PartitionConfig')),
],
options={
'ordering': ('-id',),
},
),
]
================================================
FILE: pg_partitioning/migrations/__init__.py
================================================
================================================
FILE: pg_partitioning/models.py
================================================
from django.apps import apps
from django.db import models, transaction
from pg_partitioning.signals import post_attach_partition, post_create_partition, post_detach_partition
from .constants import (
SQL_APPEND_TABLESPACE,
SQL_ATTACH_TIME_RANGE_PARTITION,
SQL_CREATE_TIME_RANGE_PARTITION,
SQL_DETACH_PARTITION,
SQL_SET_TABLE_TABLESPACE,
PeriodType,
)
from .shortcuts import double_quote, drop_table, execute_sql, generate_set_indexes_tablespace_sql, single_quote
class PartitionConfig(models.Model):
"""You can get the configuration object of the partition table through ``Model.partitioning.config``,
You can only edit the following fields via the object's ``save`` method:
"""
model_label = models.TextField(unique=True)
period = models.TextField(default=PeriodType.Month)
"""Partition period. you can only set options in the `PeriodType`.
The default value is ``PeriodType.Month``. Changing this value will trigger the ``detach_partition`` method."""
interval = models.PositiveIntegerField(null=True)
"""Detaching period. The ``detach_partition`` method defaults to detach partitions before the interval * period.
The default is None, ie no partition will be detached. Changing this value will trigger the ``detach_partition`` method."""
attach_tablespace = models.TextField(null=True)
"""The name of the tablespace specified when creating or attaching a partition. Modifying this field will only affect subsequent operations.
A table migration may occur at this time."""
detach_tablespace = models.TextField(null=True)
"""The name of the tablespace specified when detaching a partition. Modifying this field will only affect subsequent operations.
A table migration may occur at this time."""
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
"""This setting will take effect immediately when you modify the value of
``interval`` in the configuration.
"""
adding = self._state.adding
model = apps.get_model(self.model_label)
if not adding:
prev = self.__class__.objects.get(pk=self.pk)
with transaction.atomic():
super().save(force_insert, force_update, using, update_fields)
if adding:
# Creating first partition.
model.partitioning.create_partition(0)
if not adding:
# Period or interval changed.
if prev.period != self.period or (prev.interval != self.interval):
model.partitioning.detach_partition()
class PartitionLog(models.Model):
"""You can only edit the following fields via the object's ``save`` method:"""
config = models.ForeignKey(PartitionConfig, on_delete=models.CASCADE, related_name="logs")
table_name = models.TextField(unique=True)
# range bound: [start, end)
start = models.DateTimeField()
end = models.DateTimeField()
is_attached = models.BooleanField(default=True)
"""Whether the partition is a attached partition. changing the value will trigger an attaching or detaching operation."""
detach_time = models.DateTimeField(null=True)
"""When the value is not `None`, the partition will not be automatically detached before this time. The default is `None`."""
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
"""This setting will take effect immediately when you modify the value of
``is_attached`` in the configuration.
"""
model = apps.get_model(self.config.model_label)
if self._state.adding:
create_partition_sql = SQL_CREATE_TIME_RANGE_PARTITION % {
"parent": double_quote(model._meta.db_table),
"child": double_quote(self.table_name),
"date_start": single_quote(self.start.isoformat()),
"date_end": single_quote(self.end.isoformat()),
}
if self.config.attach_tablespace:
create_partition_sql += SQL_APPEND_TABLESPACE % {"tablespace": self.config.attach_tablespace}
with transaction.atomic():
super().save(force_insert, force_update, using, update_fields)
execute_sql(create_partition_sql)
execute_sql(generate_set_indexes_tablespace_sql(self.table_name, self.config.attach_tablespace))
post_create_partition.send(sender=model, partition_log=self)
else:
with transaction.atomic():
prev = self.__class__.objects.select_for_update().get(pk=self.pk)
# Detach partition.
if prev.is_attached and (not self.is_attached):
sql_sequence = [SQL_DETACH_PARTITION % {"parent": double_quote(model._meta.db_table), "child": double_quote(self.table_name)}]
if self.config.detach_tablespace:
sql_sequence.append(SQL_SET_TABLE_TABLESPACE % {"name": double_quote(self.table_name), "tablespace": self.config.detach_tablespace})
sql_sequence.extend(generate_set_indexes_tablespace_sql(self.table_name, self.config.detach_tablespace))
super().save(force_insert, force_update, using, update_fields)
execute_sql(sql_sequence)
post_detach_partition.send(sender=model, partition_log=self)
# Attach partition.
elif (not prev.is_attached) and self.is_attached:
sql_sequence = list()
if self.config.attach_tablespace:
sql_sequence.append(SQL_SET_TABLE_TABLESPACE % {"name": double_quote(self.table_name), "tablespace": self.config.attach_tablespace})
sql_sequence.extend(generate_set_indexes_tablespace_sql(self.table_name, self.config.attach_tablespace))
sql_sequence.append(
SQL_ATTACH_TIME_RANGE_PARTITION
% {
"parent": double_quote(model._meta.db_table),
"child": double_quote(self.table_name),
"date_start": single_quote(self.start.isoformat()),
"date_end": single_quote(self.end.isoformat()),
}
)
super().save(force_insert, force_update, using, update_fields)
execute_sql(sql_sequence)
post_attach_partition.send(sender=model, partition_log=self)
# State has not changed.
else:
super().save(force_insert, force_update, using, update_fields)
@transaction.atomic
def delete(self, using=None, keep_parents=False):
"""When the instance is deleted, the partition corresponding to it will also be deleted."""
drop_table(self.table_name)
super().delete(using, keep_parents)
class Meta:
ordering = ("-id",)
================================================
FILE: pg_partitioning/patch/__init__.py
================================================
================================================
FILE: pg_partitioning/patch/schema.py
================================================
import logging
from importlib import import_module
from django.apps.config import MODELS_MODULE_NAME
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
from pg_partitioning.manager import _PartitionManagerBase
logger = logging.getLogger(__name__)
default_create_model_method = DatabaseSchemaEditor.create_model
default_sql_create_table = DatabaseSchemaEditor.sql_create_table
def create_model(self, model):
meta = model._meta
try:
cls_module = import_module(f"{meta.app_label}.{MODELS_MODULE_NAME}")
except ModuleNotFoundError:
cls_module = None
cls = getattr(cls_module, meta.object_name, None)
partitioning = getattr(cls, "partitioning", None)
if isinstance(partitioning, _PartitionManagerBase):
# XXX: Monkeypatch create_model.
logger.debug("Partitioned model detected: %s", meta.label)
_type = partitioning.type
key = partitioning.partition_key
DatabaseSchemaEditor.sql_create_table = f"CREATE TABLE %(table)s (%(definition)s) PARTITION BY {_type} ({key})"
if meta.pk.name != key:
"""The partition key must be part of the primary key,
and currently Django does not support setting a composite primary key,
so its properties are turned off."""
meta.pk.primary_key = False
logger.info("Note that PK constraints for %s has been temporarily closed.", meta.label)
else:
DatabaseSchemaEditor.sql_create_table = default_sql_create_table
default_create_model_method(self, model)
meta.pk.primary_key = True
DatabaseSchemaEditor.create_model = create_model
================================================
FILE: pg_partitioning/shortcuts.py
================================================
import logging
from typing import List, Optional, Tuple, Union
from django.db import connection
from pg_partitioning.constants import SQL_DROP_TABLE, SQL_GET_TABLE_INDEXES, SQL_SET_INDEX_TABLESPACE, SQL_SET_TABLE_TABLESPACE, SQL_TRUNCATE_TABLE
logger = logging.getLogger(__name__)
def single_quote(name: str) -> str:
"""Represent a string constants in SQL."""
if name.startswith("'") and name.endswith("'"):
return name
return "'%s'" % name
def double_quote(name: str) -> str:
"""Represent a identify in SQL."""
if name.startswith('"') and name.endswith('"'):
return name
return '"%s"' % name
def execute_sql(sql_sequence: Union[str, List[str], Tuple[str]], fetch: bool = False) -> Optional[List]:
"""Execute SQL sequence and returning result."""
if not sql_sequence:
if fetch:
return []
return
sql_str = ""
for statement in sql_sequence if isinstance(sql_sequence, (list, tuple)) else [sql_sequence]:
sql_str += ";\n" + statement if sql_str else statement
logger.debug("The sequence of SQL statements to be executed:\n %s", sql_str)
with connection.cursor() as cursor:
cursor.execute(sql_str)
if fetch:
return cursor.fetchall()
def generate_set_indexes_tablespace_sql(table_name: str, tablespace: str) -> List[str]:
"""Generate set indexes tablespace SQL sequence.
Parameters:
table_name(str): Table name.
tablespace(str): Partition tablespace.
"""
sql_sequence = []
result = execute_sql(SQL_GET_TABLE_INDEXES % {"table_name": single_quote(table_name)}, fetch=True)
for item in result:
sql_sequence.append(SQL_SET_INDEX_TABLESPACE % {"name": double_quote(item[0]), "tablespace": tablespace})
return sql_sequence
def set_tablespace(table_name: str, tablespace: str) -> None:
"""Set the tablespace for a table and indexes.
Parameters:
table_name(str): Table name.
tablespace(str): Tablespace name.
"""
sql_sequence = [SQL_SET_TABLE_TABLESPACE % {"name": double_quote(table_name), "tablespace": tablespace}]
sql_sequence.extend(generate_set_indexes_tablespace_sql(table_name, tablespace))
execute_sql(sql_sequence)
def truncate_table(table_name: str) -> None:
"""Truncate table.
Parameters:
table_name(str): Table name.
"""
execute_sql(SQL_TRUNCATE_TABLE % {"name": double_quote(table_name)})
def drop_table(table_name: str) -> None:
"""Drop table.
Parameters:
table_name(str): Table name.
"""
execute_sql(SQL_DROP_TABLE % {"name": double_quote(table_name)})
================================================
FILE: pg_partitioning/signals.py
================================================
from django.dispatch import Signal
post_create_partition = Signal(providing_args=["partition_log"])
"""Sent when a partition is created.
"""
post_attach_partition = Signal(providing_args=["partition_log"])
"""Sent when a partition is attached.
"""
post_detach_partition = Signal(providing_args=["partition_log"])
"""Sent when a partition is detached.
"""
================================================
FILE: run_test.py
================================================
import argparse
import sys
import dj_database_url
import django
from django.core.management import call_command
def setup_django_environment():
from django.conf import settings
settings.configure(
DEBUG_PROPAGATE_EXCEPTIONS=True,
DATABASES={
"default": dj_database_url.config(env="DATABASE_URL",
default="postgres://test:test@localhost/test",
conn_max_age=20)
},
SECRET_KEY="not very secret in tests",
USE_I18N=True,
USE_L10N=True,
USE_TZ=True,
TIME_ZONE="Asia/Shanghai",
INSTALLED_APPS=(
"pg_partitioning",
"tests",
),
LOGGING={
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "[%(asctime)s] %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "standard"
}
},
"loggers": {
"pg_partitioning.shortcuts": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
},
"pg_partitioning.patch.schema": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
},
},
}
)
django.setup()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the pg_partitioning test suite.")
parser.add_argument("-c", "--coverage", dest="use_coverage", action="store_true", help="Run coverage to collect code coverage and generate report.")
options = parser.parse_args()
if options.use_coverage:
try:
from coverage import coverage
except ImportError:
options.use_coverage = False
if options.use_coverage:
cov = coverage()
cov.start()
setup_django_environment()
call_command("test", verbosity=2, interactive=False, stdout=sys.stdout)
if options.use_coverage:
print("\nRunning Code Coverage...\n")
cov.stop()
cov.report()
cov.xml_report()
================================================
FILE: setup.cfg
================================================
[flake8]
max-complexity = 20
max-line-length = 157
inline-quotes = "
multiline-quotes = """
[isort]
not_skip = __init__.py
line_length = 157
include_trailing_comma = true
combine_as_imports = true
multi_line_output = 3
order_by_type = true
[bdist_wheel]
universal=1
[coverage:run]
include = pg_partitioning/*,test/*
omit =
pg_partitioning/migrations/*
branch = True
[coverage:report]
exclude_lines =
pragma: no cover
raise NotImplementedError
ignore_errors = True
================================================
FILE: setup.py
================================================
import os
from setuptools import setup
def rel(*xs):
return os.path.join(os.path.abspath(os.path.dirname(__file__)), *xs)
with open(rel("README.rst")) as f:
long_description = f.read()
with open(rel("pg_partitioning", "__init__.py"), "r") as f:
version_marker = "__version__ = "
for line in f:
if line.startswith(version_marker):
_, version = line.split(version_marker)
version = version.strip().strip('"')
break
else:
raise RuntimeError("Version marker not found.")
dependencies = [
"python-dateutil~=2.7",
]
extra_dependencies = {
"django": [
"Django>=2.0,<3.0"
],
}
extra_dependencies["all"] = list(set(sum(extra_dependencies.values(), [])))
extra_dependencies["dev"] = extra_dependencies["all"] + [
# Pinned due to https://bitbucket.org/ned/coveragepy/issues/578/incomplete-file-path-in-xml-report
"coverage>=4.0,<4.4",
# Docs
"alabaster==0.7.12",
"sphinx==1.8.3",
"sphinxcontrib-napoleon==0.7",
# Linting
"flake8~=3.6.0",
"isort~=4.3.4",
"black~=18.9b0",
"flake8-bugbear~=18.8.0",
"flake8-quotes~=1.0.0",
# Misc
"dj-database-url==0.5.0",
"psycopg2-binary==2.7.6.1",
"twine==1.12.1",
# Testing
"tox==3.9.0",
"tox-venv==0.4.0"
]
setup(
name="django-pg-partitioning",
version=version,
author="Boyce Li",
author_email="monobiao@gmail.com",
description="A Django extension that supports PostgreSQL 11 time ranges and list partitioning.",
long_description=long_description,
long_description_content_type="text/x-rst",
url="https://github.com/chaitin/django-pg-partitioning",
packages=["pg_partitioning", "pg_partitioning.migrations", "pg_partitioning.patch"],
include_package_data=True,
install_requires=dependencies,
extras_require=extra_dependencies,
python_requires=">=3.6",
classifiers=[
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Framework :: Django :: 2.0",
"Framework :: Django :: 2.1",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules"
],
)
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/models.py
================================================
from django.contrib.postgres.indexes import BrinIndex
from django.db import models
from django.utils import timezone
from pg_partitioning.constants import PeriodType
from pg_partitioning.decorators import ListPartitioning, TimeRangePartitioning
@TimeRangePartitioning(partition_key="timestamp", default_period=PeriodType.Month, default_attach_tablespace="data1", default_detach_tablespace="data2")
class TimeRangeTableA(models.Model):
text = models.TextField()
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
db_tablespace = "pg_default"
indexes = [BrinIndex(fields=["timestamp"])]
unique_together = ("text", "timestamp")
ordering = ["text"]
@TimeRangePartitioning(partition_key="timestamp", default_period=PeriodType.Day, default_attach_tablespace="data2", default_detach_tablespace="data1")
class TimeRangeTableB(models.Model):
text = models.TextField()
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
indexes = [BrinIndex(fields=["timestamp"])]
unique_together = ("text", "timestamp")
ordering = ["text"]
@ListPartitioning(partition_key="category")
class ListTableText(models.Model):
category = models.TextField(default="A", null=True, blank=True)
timestamp = models.DateTimeField(default=timezone.now)
@ListPartitioning(partition_key="category")
class ListTableInt(models.Model):
category = models.IntegerField(default=0, null=True)
timestamp = models.DateTimeField(default=timezone.now)
@ListPartitioning(partition_key="category")
class ListTableBool(models.Model):
category = models.NullBooleanField(default=False, null=True)
timestamp = models.DateTimeField(default=timezone.now)
================================================
FILE: tests/tests.py
================================================
import datetime
from unittest.mock import patch
from dateutil.relativedelta import MO, relativedelta
from django.db import connection
from django.test import TestCase
from django.utils import timezone
from django.utils.crypto import get_random_string
from pg_partitioning.constants import SQL_GET_TABLE_INDEXES, PeriodType
from pg_partitioning.models import PartitionConfig, PartitionLog
from pg_partitioning.shortcuts import single_quote
from .models import ListTableBool, ListTableInt, ListTableText, TimeRangeTableA, TimeRangeTableB
def t(year=2018, month=8, day=25, hour=7, minute=15, second=15, millisecond=0):
"""A point in time."""
return timezone.get_current_timezone().localize(datetime.datetime(year, month, day, hour, minute, second, millisecond))
def tz(time):
return timezone.localtime(time)
class GeneralTestCase(TestCase):
def assertTablespace(self, table_name, tablespace):
with connection.cursor() as cursor:
cursor.execute(f"SELECT tablespace FROM pg_tables WHERE tablename = {single_quote(table_name)};")
rows = cursor.fetchall()
self.assertEqual(tablespace, rows[0][0])
cursor.execute(SQL_GET_TABLE_INDEXES % {"table_name": single_quote(table_name)})
rows = cursor.fetchall()
for row in rows:
cursor.execute(f"SELECT tablespace FROM pg_indexes WHERE indexname = {single_quote(row[0])};")
rows = cursor.fetchall()
self.assertEqual(tablespace, rows[0][0])
class TimeRangePartitioningTestCase(GeneralTestCase):
def assertTimeRangeEqual(self, model, time_start, time_end):
self.assertListEqual([time_start, time_end], [tz(model.partitioning.latest.start), tz(model.partitioning.latest.end)])
# Verify that the partition has been created by inserting data.
model.objects.create(text=get_random_string(length=32), timestamp=time_start)
model.objects.create(text=get_random_string(length=32), timestamp=time_end - relativedelta(microseconds=1))
def _create_partition(self, period, start_date, delta):
TimeRangeTableB.partitioning.options["default_period"] = period
for i in range(0, 3):
if i == 0:
TimeRangeTableB.partitioning.config # Create first partition by side effect.
else:
TimeRangeTableB.partitioning.create_partition(0)
end_date = start_date + delta
self.assertTimeRangeEqual(TimeRangeTableB, start_date, end_date)
start_date = end_date
@patch("django.utils.timezone.now", new=t)
def test_create_partition_week(self):
self._create_partition(PeriodType.Week, t(2018, 8, 20, 0, 0, 0), relativedelta(days=1, weekday=MO))
@patch("django.utils.timezone.now", new=t)
def test_create_partition_day(self):
self._create_partition(PeriodType.Day, t(2018, 8, 25, 0, 0, 0), relativedelta(days=1))
@patch("django.utils.timezone.now", new=t)
def test_create_partition_month(self):
self._create_partition(PeriodType.Month, t(2018, 8, 1, 0, 0, 0), relativedelta(months=1))
@patch("django.utils.timezone.now", new=t)
def test_create_partition_year(self):
self._create_partition(PeriodType.Year, t(2018, 1, 1, 0, 0, 0), relativedelta(years=1))
@classmethod
def _update_config_period(cls, config: PartitionConfig, period: str):
config.period = period
config.save()
def test_create_partition(self):
with patch("django.utils.timezone.now", new=t):
config_a: PartitionConfig = TimeRangeTableA.partitioning.config
self.assertEqual(1, config_a.logs.count())
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 8, 1, 0, 0, 0), t(2018, 9, 1, 0, 0, 0))
# Repeated calls will not produce wrong results (idempotence).
for _ in range(3):
TimeRangeTableA.partitioning.create_partition()
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 8, 1, 0, 0, 0), t(2018, 9, 1, 0, 0, 0))
# Perform a series of partition creation operations.
self._update_config_period(config_a, PeriodType.Week)
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 9, 1, 0, 0, 0), t(2018, 9, 3, 0, 0, 0))
self._update_config_period(config_a, PeriodType.Day)
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 9, 3, 0, 0, 0), t(2018, 9, 4, 0, 0, 0))
self._update_config_period(config_a, PeriodType.Month)
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 9, 4, 0, 0, 0), t(2018, 10, 1, 0, 0, 0))
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 10, 1, 0, 0, 0), t(2018, 11, 1, 0, 0, 0))
self._update_config_period(config_a, PeriodType.Year)
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 11, 1, 0, 0, 0), t(2019, 1, 1, 0, 0, 0))
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2019, 1, 1, 0, 0, 0), t(2020, 1, 1, 0, 0, 0))
def test_max_days_to_next_partition(self):
with patch("django.utils.timezone.now", new=t):
TimeRangeTableA.partitioning.create_partition()
TimeRangeTableA.partitioning.create_partition(0)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 9, 1, 0, 0, 0), t(2018, 10, 1, 0, 0, 0))
with patch("django.utils.timezone.now", return_value=t(2018, 8, 2, 0, 0, 0)):
TimeRangeTableA.partitioning.create_partition(59)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 9, 1, 0, 0, 0), t(2018, 10, 1, 0, 0, 0))
TimeRangeTableA.partitioning.create_partition(60)
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 10, 1, 0, 0, 0), t(2018, 11, 1, 0, 0, 0))
with patch("django.utils.timezone.now", return_value=t(2019, 3, 3, 0, 0, 0)):
TimeRangeTableA.partitioning.create_partition(5)
self.assertTimeRangeEqual(TimeRangeTableA, t(2019, 3, 1, 0, 0, 0), t(2019, 4, 1, 0, 0, 0))
def test_attach_or_detach_partition(self):
self.test_create_partition()
config_a: PartitionConfig = TimeRangeTableA.partitioning.config
self.assertEqual(0, config_a.logs.filter(is_attached=False).count())
TimeRangeTableA.partitioning.detach_partition(config_a.logs.all())
self.assertEqual(0, config_a.logs.filter(is_attached=True).count())
TimeRangeTableA.partitioning.attach_partition(config_a.logs.all())
self.assertEqual(0, config_a.logs.filter(is_attached=False).count())
with patch("django.utils.timezone.now", return_value=t(2018, 10, 15, 12, 1, 4)):
config_a.period = PeriodType.Day
config_a.interval = 15
config_a.save()
self.assertEqual(t(2018, 10, 1, 0, 0, 0), tz(config_a.logs.filter(is_attached=True).order_by("start").first().end))
TimeRangeTableA.partitioning.attach_partition(config_a.logs.all())
config_a.period = PeriodType.Week
config_a.interval = 2
config_a.save()
self.assertEqual(t(2018, 11, 1, 0, 0, 0), tz(config_a.logs.filter(is_attached=True).order_by("start").first().end))
TimeRangeTableA.partitioning.attach_partition(config_a.logs.all())
log = config_a.logs.filter(is_attached=True).order_by("start").first()
self.assertEqual(t(2018, 9, 1, 0, 0, 0), log.end)
log.detach_time = t(2018, 10, 15, 12, 1, 5)
log.save()
config_a.period = PeriodType.Month
config_a.interval = 1
config_a.save()
self.assertEqual(t(2018, 9, 1, 0, 0, 0), tz(config_a.logs.filter(is_attached=True).order_by("start").first().end))
log.refresh_from_db()
self.assertEqual(True, log.is_attached)
log.detach_time = None
log.save()
TimeRangeTableA.partitioning.detach_partition()
self.assertEqual(t(2018, 10, 1, 0, 0, 0), tz(config_a.logs.filter(is_attached=True).order_by("start").first().end))
log.refresh_from_db()
self.assertEqual(False, log.is_attached)
def test_delete_partition(self):
for _ in range(4):
TimeRangeTableA.partitioning.create_partition(0)
config_a: PartitionConfig = TimeRangeTableA.partitioning.config
TimeRangeTableA.partitioning.delete_partition(config_a.logs.all())
self.assertEqual(0, config_a.logs.count())
with patch("django.utils.timezone.now", new=t):
self._update_config_period(config_a, PeriodType.Day)
TimeRangeTableA.partitioning.create_partition()
self.assertEqual(2, config_a.logs.count())
self.assertTimeRangeEqual(TimeRangeTableA, t(2018, 8, 26, 0, 0, 0), t(2018, 8, 27, 0, 0, 0))
def test_attach_detach_tablespace(self):
TimeRangeTableA.partitioning.create_partition()
log: PartitionLog = TimeRangeTableA.partitioning.latest
self.assertEqual(True, log.is_attached)
self.assertTablespace(log.table_name, log.config.attach_tablespace)
TimeRangeTableA.partitioning.detach_partition([log])
log.refresh_from_db()
self.assertEqual(False, log.is_attached)
self.assertTablespace(log.table_name, log.config.detach_tablespace)
TimeRangeTableA.partitioning.attach_partition()
log.refresh_from_db()
self.assertEqual(True, log.is_attached)
self.assertTablespace(log.table_name, log.config.attach_tablespace)
class ListPartitioningTestCase(GeneralTestCase):
@classmethod
def assertCreated(cls, model, category):
# Verify that the partition has been created by inserting data.
model.objects.create(category=category)
def test_create_partition(self):
ListTableText.partitioning.create_partition("list_table_text_a", "A", "data1")
self.assertCreated(ListTableText, "A")
ListTableText.partitioning.create_partition("list_table_text_b", "B")
self.assertCreated(ListTableText, "B")
ListTableText.partitioning.create_partition("list_table_text_blank", "")
self.assertCreated(ListTableText, "")
ListTableText.partitioning.create_partition("list_table_text_none", None, "data2")
self.assertCreated(ListTableText, None)
ListTableInt.partitioning.create_partition("list_table_int_1", 1, "data1")
self.assertCreated(ListTableInt, 1)
ListTableInt.partitioning.create_partition("list_table_int_2", 2)
self.assertCreated(ListTableInt, 2)
ListTableInt.partitioning.create_partition("list_table_int_none", None, "data2")
self.assertCreated(ListTableInt, None)
ListTableBool.partitioning.create_partition("list_table_bool_true", True, "data1")
self.assertCreated(ListTableBool, True)
ListTableBool.partitioning.create_partition("list_table_bool_false", False)
self.assertCreated(ListTableBool, False)
ListTableBool.partitioning.create_partition("list_table_bool_none", None, "data2")
self.assertCreated(ListTableBool, None)
def assertTablespace(self, table_name, tablespace):
with connection.cursor() as cursor:
cursor.execute(f"SELECT tablespace FROM pg_tables WHERE tablename = '{table_name}';")
rows = cursor.fetchall()
self.assertEqual(tablespace, rows[0][0])
def test_attach_or_detach_partition(self):
self.test_create_partition()
ListTableText.partitioning.detach_partition("list_table_text_none", "data1")
self.assertTablespace("list_table_text_none", "data1")
ListTableText.partitioning.attach_partition("list_table_text_none", None, "data2")
self.assertTablespace("list_table_text_none", "data2")
================================================
FILE: tox.ini
================================================
[tox]
envlist =
{py36,py37}-django{20,21,22}
docs
lint
[travis:env]
DJANGO =
2.0: django20
2.1: django21
2.2: django22
[testenv]
setenv =
PYTHONDONTWRITEBYTECODE=1
PYTHONWARNINGS=once
envdir = {toxworkdir}/venvs/{envname}
extras =
dev
deps =
lint: isort
black
flake8
flake8-bugbear
flake8-quotes
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2a1,<3.0
commands =
python3 ./run_test.py -c {posargs}
[testenv:docs]
basepython = python3.6
whitelist_externals = make
changedir = docs
commands =
make html
[testenv:lint]
basepython = python3.6
commands =
isort -rc pg_partitioning tests
black pg_partitioning tests -l 157 --exclude pg_partitioning/migrations/*
flake8 {toxinidir}/pg_partitioning {toxinidir}/tests {toxinidir}/*.py --exclude */migrations
gitextract_of6gshgr/ ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── CONTRIBUTING.rst ├── CONTRIBUTORS ├── LICENSE ├── README.rst ├── codecov.yml ├── dev/ │ ├── docker-compose.yml │ └── postgres_init.sh ├── docs/ │ ├── Makefile │ ├── readthedocs.txt │ └── source/ │ ├── _templates/ │ │ └── sidebarlogo.html │ ├── api.rst │ ├── conf.py │ ├── decorators.rst │ ├── design.rst │ ├── index.rst │ ├── installation.rst │ └── signals.rst ├── pg_partitioning/ │ ├── __init__.py │ ├── apps.py │ ├── constants.py │ ├── decorators.py │ ├── manager.py │ ├── migrations/ │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── patch/ │ │ ├── __init__.py │ │ └── schema.py │ ├── shortcuts.py │ └── signals.py ├── run_test.py ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── models.py │ └── tests.py └── tox.ini
SYMBOL INDEX (72 symbols across 12 files)
FILE: pg_partitioning/apps.py
class AppConfig (line 4) | class AppConfig(DefaultAppConfig):
method ready (line 7) | def ready(self):
FILE: pg_partitioning/constants.py
class PartitioningType (line 23) | class PartitioningType:
class PeriodType (line 28) | class PeriodType:
FILE: pg_partitioning/decorators.py
class _PartitioningBase (line 11) | class _PartitioningBase:
method __init__ (line 12) | def __init__(self, partition_key: str, **options):
method __call__ (line 16) | def __call__(self, model: Type[models.Model]):
class TimeRangePartitioning (line 21) | class TimeRangePartitioning(_PartitioningBase):
method __call__ (line 48) | def __call__(self, model: Type[models.Model]):
class ListPartitioning (line 56) | class ListPartitioning(_PartitioningBase):
method __call__ (line 77) | def __call__(self, model: Type[models.Model]):
FILE: pg_partitioning/manager.py
class _PartitionManagerBase (line 27) | class _PartitionManagerBase:
method __init__ (line 30) | def __init__(self, model: Type[models.Model], partition_key: str, opti...
class TimeRangePartitionManager (line 36) | class TimeRangePartitionManager(_PartitionManagerBase):
method config (line 42) | def config(self) -> PartitionConfig:
method latest (line 64) | def latest(self) -> Optional[PartitionLog]:
method _get_period_bound (line 73) | def _get_period_bound(cls, date_start, initial, addition_zeros=None, i...
method create_partition (line 90) | def create_partition(self, max_days_to_next_partition: int = 1) -> None:
method attach_partition (line 126) | def attach_partition(self, partition_log: Optional[Iterable] = None, d...
method detach_partition (line 143) | def detach_partition(self, partition_log: Optional[Iterable] = None) -...
method delete_partition (line 170) | def delete_partition(self, partition_log: Iterable) -> None:
function _db_value (line 181) | def _db_value(value: Union[str, int, bool, None]) -> str:
class ListPartitionManager (line 187) | class ListPartitionManager(_PartitionManagerBase):
method create_partition (line 192) | def create_partition(self, partition_name: str, value: Union[str, int,...
method attach_partition (line 211) | def attach_partition(self, partition_name: str, value: Union[str, int,...
method detach_partition (line 229) | def detach_partition(self, partition_name: str, tablespace: str = None...
FILE: pg_partitioning/migrations/0001_initial.py
class Migration (line 7) | class Migration(migrations.Migration):
FILE: pg_partitioning/models.py
class PartitionConfig (line 17) | class PartitionConfig(models.Model):
method save (line 36) | def save(self, force_insert=False, force_update=False, using=None, upd...
class PartitionLog (line 59) | class PartitionLog(models.Model):
method save (line 72) | def save(self, force_insert=False, force_update=False, using=None, upd...
method delete (line 132) | def delete(self, using=None, keep_parents=False):
class Meta (line 138) | class Meta:
FILE: pg_partitioning/patch/schema.py
function create_model (line 16) | def create_model(self, model):
FILE: pg_partitioning/shortcuts.py
function single_quote (line 11) | def single_quote(name: str) -> str:
function double_quote (line 19) | def double_quote(name: str) -> str:
function execute_sql (line 27) | def execute_sql(sql_sequence: Union[str, List[str], Tuple[str]], fetch: ...
function generate_set_indexes_tablespace_sql (line 44) | def generate_set_indexes_tablespace_sql(table_name: str, tablespace: str...
function set_tablespace (line 59) | def set_tablespace(table_name: str, tablespace: str) -> None:
function truncate_table (line 72) | def truncate_table(table_name: str) -> None:
function drop_table (line 82) | def drop_table(table_name: str) -> None:
FILE: run_test.py
function setup_django_environment (line 9) | def setup_django_environment():
FILE: setup.py
function rel (line 6) | def rel(*xs):
FILE: tests/models.py
class TimeRangeTableA (line 10) | class TimeRangeTableA(models.Model):
class Meta (line 14) | class Meta:
class TimeRangeTableB (line 22) | class TimeRangeTableB(models.Model):
class Meta (line 26) | class Meta:
class ListTableText (line 33) | class ListTableText(models.Model):
class ListTableInt (line 39) | class ListTableInt(models.Model):
class ListTableBool (line 45) | class ListTableBool(models.Model):
FILE: tests/tests.py
function t (line 17) | def t(year=2018, month=8, day=25, hour=7, minute=15, second=15, millisec...
function tz (line 22) | def tz(time):
class GeneralTestCase (line 26) | class GeneralTestCase(TestCase):
method assertTablespace (line 27) | def assertTablespace(self, table_name, tablespace):
class TimeRangePartitioningTestCase (line 40) | class TimeRangePartitioningTestCase(GeneralTestCase):
method assertTimeRangeEqual (line 41) | def assertTimeRangeEqual(self, model, time_start, time_end):
method _create_partition (line 48) | def _create_partition(self, period, start_date, delta):
method test_create_partition_week (line 61) | def test_create_partition_week(self):
method test_create_partition_day (line 65) | def test_create_partition_day(self):
method test_create_partition_month (line 69) | def test_create_partition_month(self):
method test_create_partition_year (line 73) | def test_create_partition_year(self):
method _update_config_period (line 77) | def _update_config_period(cls, config: PartitionConfig, period: str):
method test_create_partition (line 81) | def test_create_partition(self):
method test_max_days_to_next_partition (line 115) | def test_max_days_to_next_partition(self):
method test_attach_or_detach_partition (line 131) | def test_attach_or_detach_partition(self):
method test_delete_partition (line 177) | def test_delete_partition(self):
method test_attach_detach_tablespace (line 193) | def test_attach_detach_tablespace(self):
class ListPartitioningTestCase (line 210) | class ListPartitioningTestCase(GeneralTestCase):
method assertCreated (line 212) | def assertCreated(cls, model, category):
method test_create_partition (line 216) | def test_create_partition(self):
method assertTablespace (line 247) | def assertTablespace(self, table_name, tablespace):
method test_attach_or_detach_partition (line 253) | def test_attach_or_detach_partition(self):
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (71K chars).
[
{
"path": ".gitignore",
"chars": 206,
"preview": "*.egg-info\ndist/\nbuild/\ndocs/_build\ndocs/source/.doctrees/\n__pycache__/\n.cache\n*.sqlite3\n.tox/\n*.swp\n*.pyc\n.coverage*\n.p"
},
{
"path": ".readthedocs.yml",
"chars": 157,
"preview": "version: 2\nbuild:\n image: latest\nsphinx:\n configuration: docs/source/conf.py\npython:\n version: 3.6\n install:\n "
},
{
"path": ".travis.yml",
"chars": 852,
"preview": "dist: xenial\nlanguage: python\ncache: pip\nsudo: required\nmatrix:\n fast_finish: true\n include:\n - { python: \"3.6\", en"
},
{
"path": "CONTRIBUTING.rst",
"chars": 2487,
"preview": "Contributing Guidelines\n=======================\n\nIssue tracker\n-------------\n您可以通过 `issue tracker <https://github.com/ch"
},
{
"path": "CONTRIBUTORS",
"chars": 71,
"preview": "virusdefender <virusdefender@outlook.com>\nmonouno <xingji2163@163.com>\n"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "Copyright (c) 2019 Chaitin Tech Co., Ltd.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.rst",
"chars": 3533,
"preview": "django-pg-partitioning\n======================\n.. image:: https://img.shields.io/badge/License-MIT-orange.svg?style=flat-"
},
{
"path": "codecov.yml",
"chars": 131,
"preview": "coverage:\n precision: 2\n round: down\n range: \"80...100\"\n\n status:\n project: yes\n patch: no\n changes: no\n\nco"
},
{
"path": "dev/docker-compose.yml",
"chars": 363,
"preview": "version: \"3\"\nservices:\n postgres:\n image: postgres:${POSTGRES_VERSION:-11.1-alpine}\n container_name: postgres\n "
},
{
"path": "dev/postgres_init.sh",
"chars": 177,
"preview": "#!/usr/bin/env bash\n\nmkdir /tmp/data1 /tmp/data2\npsql -U test -c \"CREATE TABLESPACE data1 LOCATION '/tmp/data1'\"\npsql -U"
},
{
"path": "docs/Makefile",
"chars": 627,
"preview": "# Minimal makefile for Sphinx documentation\n\n\n# You can set these variables from the command line.\nSPHINXOPTS =\nSPHIN"
},
{
"path": "docs/readthedocs.txt",
"chars": 139,
"preview": "alabaster==0.7.12\nsphinx==1.8.3\nsphinxcontrib-napoleon==0.7\nPygments==2.3.1\npsycopg2-binary==2.7.6.1\nDjango>=2.0,<2.2\npy"
},
{
"path": "docs/source/_templates/sidebarlogo.html",
"chars": 203,
"preview": "<p class=\"logo\">\n <a href=\"{{ pathto(master_doc) }}\">\n <img class=\"logo\" src=\"{{ pathto('_static/logo.png', 1) }}\" w"
},
{
"path": "docs/source/api.rst",
"chars": 622,
"preview": "API Reference\n=============\n\n.. py:currentmodule:: pg_partitioning.manager\n\nTime Range Partitioning\n--------------------"
},
{
"path": "docs/source/conf.py",
"chars": 1651,
"preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport os\nimport sys\n\nimport django\nfrom django.conf import settings\n\nsy"
},
{
"path": "docs/source/decorators.rst",
"chars": 751,
"preview": "Decorators\n==========\n\nNow the decorator must be used on a non-abstract model class that has not yet built a table in th"
},
{
"path": "docs/source/design.rst",
"chars": 2073,
"preview": "Design\n======\n\nA brief description of the architecture of django-pg-partitioning.\n\nPartitioned Table Support\n-----------"
},
{
"path": "docs/source/index.rst",
"chars": 329,
"preview": ".. include:: ../../README.rst\n\n.. toctree::\n :hidden:\n :maxdepth: 2\n\n installation\n design\n decorators\n api\n"
},
{
"path": "docs/source/installation.rst",
"chars": 895,
"preview": "Installation\n============\n\nPyPI\n----\n\n.. code-block:: bash\n\n $ pip install django-pg-partitioning\n\nOr you can install "
},
{
"path": "docs/source/signals.rst",
"chars": 487,
"preview": "Signals\n=======\n\nNote that these signals are only triggered when the save methods of ``PartitionConfig`` and ``Partition"
},
{
"path": "pg_partitioning/__init__.py",
"chars": 571,
"preview": "\"\"\"\nA Django extension that supports PostgreSQL 11 time ranges and list partitioning.\n\"\"\"\nREQUIRED_DJANGO_VERSION = [(2,"
},
{
"path": "pg_partitioning/apps.py",
"chars": 184,
"preview": "from django.apps import AppConfig as DefaultAppConfig\n\n\nclass AppConfig(DefaultAppConfig):\n name = \"pg_partitioning\"\n"
},
{
"path": "pg_partitioning/constants.py",
"chars": 1393,
"preview": "SQL_CREATE_TIME_RANGE_PARTITION = \"\"\"\\\nCREATE TABLE IF NOT EXISTS %(child)s PARTITION OF %(parent)s FOR VALUES FROM (%(d"
},
{
"path": "pg_partitioning/decorators.py",
"chars": 2906,
"preview": "import logging\nfrom typing import Type\n\nfrom django.db import models\n\nfrom pg_partitioning.manager import ListPartitionM"
},
{
"path": "pg_partitioning/manager.py",
"chars": 10256,
"preview": "import datetime\nfrom collections import Iterable\nfrom typing import Optional, Type, Union\n\nimport pytz\nfrom dateutil.rel"
},
{
"path": "pg_partitioning/migrations/0001_initial.py",
"chars": 1551,
"preview": "# Generated by Django 2.1.7 on 2019-02-17 12:00\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
},
{
"path": "pg_partitioning/migrations/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "pg_partitioning/models.py",
"chars": 7063,
"preview": "from django.apps import apps\nfrom django.db import models, transaction\n\nfrom pg_partitioning.signals import post_attach_"
},
{
"path": "pg_partitioning/patch/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "pg_partitioning/patch/schema.py",
"chars": 1647,
"preview": "import logging\nfrom importlib import import_module\n\nfrom django.apps.config import MODELS_MODULE_NAME\nfrom django.db.bac"
},
{
"path": "pg_partitioning/shortcuts.py",
"chars": 2641,
"preview": "import logging\nfrom typing import List, Optional, Tuple, Union\n\nfrom django.db import connection\n\nfrom pg_partitioning.c"
},
{
"path": "pg_partitioning/signals.py",
"chars": 356,
"preview": "from django.dispatch import Signal\n\npost_create_partition = Signal(providing_args=[\"partition_log\"])\n\"\"\"Sent when a part"
},
{
"path": "run_test.py",
"chars": 2495,
"preview": "import argparse\nimport sys\n\nimport dj_database_url\nimport django\nfrom django.core.management import call_command\n\n\ndef s"
},
{
"path": "setup.cfg",
"chars": 481,
"preview": "[flake8]\nmax-complexity = 20\nmax-line-length = 157\n\ninline-quotes = \"\nmultiline-quotes = \"\"\"\n\n[isort]\nnot_skip = __init_"
},
{
"path": "setup.py",
"chars": 2324,
"preview": "import os\n\nfrom setuptools import setup\n\n\ndef rel(*xs):\n return os.path.join(os.path.abspath(os.path.dirname(__file__"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/models.py",
"chars": 1742,
"preview": "from django.contrib.postgres.indexes import BrinIndex\nfrom django.db import models\nfrom django.utils import timezone\n\nfr"
},
{
"path": "tests/tests.py",
"chars": 12199,
"preview": "import datetime\nfrom unittest.mock import patch\n\nfrom dateutil.relativedelta import MO, relativedelta\nfrom django.db imp"
},
{
"path": "tox.ini",
"chars": 893,
"preview": "[tox]\nenvlist =\n {py36,py37}-django{20,21,22}\n docs\n lint\n\n[travis:env]\nDJANGO =\n 2.0: django20\n 2.1: dja"
}
]
About this extraction
This page contains the full source code of the chaitin/django-pg-timepart GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (64.0 KB), approximately 16.3k tokens, and a symbol index with 72 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.