├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dbparti ├── __init__.py ├── admin.py ├── backends │ ├── __init__.py │ ├── exceptions.py │ ├── mysql │ │ ├── __init__.py │ │ ├── filters.py │ │ └── partition.py │ ├── postgresql │ │ ├── __init__.py │ │ ├── filters.py │ │ └── partition.py │ └── utilities.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── partition.py └── models.py └── setup.py /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Changelog 4 | --------- 5 | 6 | 0.3.3 (2014-04-17) 7 | ~~~~~~~~~~~~~~~~~~ 8 | 9 | - Fixed a bug with ``partition`` command not working for MySQL backend (Issue #11) 10 | 11 | 0.3.2 (2014-03-27) 12 | ~~~~~~~~~~~~~~~~~~ 13 | 14 | - Added automatic determination of primary key column name, previously this was hardcoded to ``id`` 15 | (thanks to `fjcapdevila `__) 16 | - Python 2.6 compatibility (thanks to `Daniel Kontsek `__) 17 | 18 | 0.3.1 (2014-02-02) 19 | ~~~~~~~~~~~~~~~~~~ 20 | 21 | - Added support for DateField and DateTimeField with auto_now and auto_now_add attributes set (Issue #3) 22 | - Fixed an issue with unnecessary calling of partitioning functions while reading data from database 23 | - MySQL: Fixed inability to create partitions for December when range was set to ``month`` 24 | - MySQL: Backend was completely broken in previous version, now everything should work properly (Issue #4) 25 | 26 | 0.3.0 (2013-09-15) 27 | ~~~~~~~~~~~~~~~~~~ 28 | 29 | - Rewritten from scratch, introduced new API to add support for new backends and partition types 30 | - All default model settings which are done inside model's Meta class are now set to ``None``, that means 31 | that there are no more default settings. Everything should be explicitly defined inside each model class. 32 | - Introduced new model setting ``partition_type`` which currently accepts only one value ``range`` 33 | - Introduced new model setting ``partition_subtype`` which currently accepts only one value ``date`` and 34 | is used only with ``partition_type`` when it's set to ``range`` 35 | - Better error handling, django-db-parti tries it's best to tell you where and why an error occured 36 | - Added support for day and year partitioning for all backends in addition to week and month 37 | - PostgreSQL: new partitions are now created at the database level, that gave some speed improvement, 38 | also we don't rely on Django's save() method anymore, that means that there is no more limitation 39 | with Django's bulk_create() method, you can use it freely with partitioned tables 40 | - PostgreSQL: fixed an error when last day of the week or month wasn't inserted into partition 41 | 42 | 0.2.1 (2013-08-24) 43 | ~~~~~~~~~~~~~~~~~~ 44 | 45 | - Updated readme 46 | - Python 3 compatibility 47 | - Datetime with timezone support (Issue #1) 48 | 49 | 0.2.0 (2013-06-10) 50 | ~~~~~~~~~~~~~~~~~~ 51 | 52 | - Added mysql backend 53 | - Fixed incorrect handling of datetime object in DateTimeMixin 54 | 55 | 0.1.5 (2013-06-08) 56 | ~~~~~~~~~~~~~~~~~~ 57 | 58 | - Updated readme 59 | - Fixed postgresql backend error which sometimes tried to insert the data into partitions that don't exist 60 | - Moved all the database partition system stuff to the command ``partition`` (see readme), that gave a lot 61 | in speed improvement because we don't need to check for trigger existance and some other things at runtime 62 | anymore 63 | 64 | 0.1.4 (2013-06-01) 65 | ~~~~~~~~~~~~~~~~~~ 66 | 67 | - Packaging fix 68 | 69 | 0.1.3 (2013-06-01) 70 | ~~~~~~~~~~~~~~~~~~ 71 | 72 | - Packaging fix 73 | 74 | 0.1.2 (2013-06-01) 75 | ~~~~~~~~~~~~~~~~~~ 76 | 77 | - Packaging fix 78 | 79 | 0.1.1 (2013-06-01) 80 | ~~~~~~~~~~~~~~~~~~ 81 | 82 | - Packaging fix 83 | 84 | 0.1.0 (2013-06-01) 85 | ~~~~~~~~~~~~~~~~~~ 86 | 87 | - Initial release 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Max Tepkeev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of this project nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGELOG.rst 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **THIS LIBRARY IS DEPRECATED IN FAVOR OF** `ARCHITECT `_. **ALL 2 | FEATURES THAT ARE NOT CURRENTLY BACKPORTED TO ARCHITECT WILL BE BACKPORTED IN THE NEAR TIME. PLEASE 3 | CONSIDER SWITCHING TO ARCHITECT ASAP, AS DJANGO-DB-PARTI WILL RECEIVE NO FUTURE UPDATES AND BUG FIXES.** 4 | 5 | Django DB Parti 6 | =============== 7 | 8 | Django DB Parti is a package for Django which aim is to make table partitioning on the fly. Partitioning is a 9 | division of one large table into several smaller tables which represent that table. Partitioning is usually 10 | done for manageability, performance or availability reasons. If you are unsure whether you need partitioning 11 | or not, then you almost certainly don't need it. 12 | 13 | .. image:: https://badge.fury.io/py/django-db-parti.png 14 | :target: http://badge.fury.io/py/django-db-parti 15 | 16 | .. image:: https://pypip.in/d/django-db-parti/badge.png 17 | :target: https://crate.io/packages/django-db-parti 18 | 19 | .. contents:: Table of contents: 20 | 21 | Features 22 | -------- 23 | 24 | Django DB Parti supports multiple database backends, each database differ from each other in many ways, that 25 | means that some features may be available for one database backend, but not for the other, below you will find 26 | list of supported database backends and detailed information which database backend supports which features. 27 | 28 | PostgreSQL 29 | ~~~~~~~~~~ 30 | 31 | Implementation 32 | ++++++++++++++ 33 | 34 | PostgreSQL's partitioning implementation in Django DB Parti is done purely at the database level. That means 35 | that Django DB Parti creates several triggers and functions and inserts them directly into the database, so 36 | even if you issue direct insert statement from database console and not from Django, everything will work as 37 | expected and record will be inserted into the correct partition, if partition doesn't exist, it will be created 38 | for you automatically. Also partitions may be created in any order and not only from lower to higher. 39 | 40 | Partitioning types 41 | ++++++++++++++++++ 42 | 43 | * Range partitioning by date/datetime for the following periods: 44 | 45 | - day 46 | - week 47 | - month 48 | - year 49 | 50 | Limitations 51 | +++++++++++ 52 | 53 | * Currently there are no known limitations for this backend, except that not all partitioning types are supported. 54 | New types will be added in next releases of Django DB Parti. 55 | 56 | MySQL 57 | ~~~~~ 58 | 59 | Implementation 60 | ++++++++++++++ 61 | 62 | MySQL's partitioning implementation in Django DB Parti is done in a mixed way, half at the python level and half 63 | at the database level. Unfortunately MySQL doesn't support dynamic sql in triggers or functions that are called 64 | within triggers, so the only way to create partitions automatically is to calculate everything at the python 65 | level, then to create needed sql statements based on calculations and issue that statement into the database. 66 | 67 | Partitioning types 68 | ++++++++++++++++++ 69 | 70 | * Range partitioning by date/datetime for the following periods: 71 | 72 | - day 73 | - week 74 | - month 75 | - year 76 | 77 | Limitations 78 | +++++++++++ 79 | 80 | * Not all partitioning types are supported. New types will be added in next releases of Django DB Parti. 81 | * Partitioning is not available for bulk inserts (i.e. Django's bulk_create() method) because it doesn't call 82 | model's save() method which this backend relies on. Currently there is no known way to remove this limitation. 83 | * New partitions can be created only from lower to higher, you can overcome this with MySQL's special command 84 | REORGANIZE PARTITION which you have to issue from the database console. You can read more about it at the 85 | MySQL's documentation. We plan to remove this limitation in one of the future releases of Django DB Parti. 86 | 87 | Requirements 88 | ------------ 89 | 90 | * Django_ >= 1.4.x 91 | 92 | Installation 93 | ------------ 94 | 95 | From pypi_: 96 | 97 | .. code-block:: bash 98 | 99 | $ pip install django-db-parti 100 | 101 | or clone from github_: 102 | 103 | .. code-block:: bash 104 | 105 | $ git clone git://github.com/maxtepkeev/django-db-parti.git 106 | 107 | Configuration 108 | ------------- 109 | 110 | Add dbparti to PYTHONPATH and installed applications: 111 | 112 | .. code-block:: python 113 | 114 | INSTALLED_APPS = ( 115 | ... 116 | 'dbparti' 117 | ) 118 | 119 | Create the model as usual which will represent the partitioned table and run syncdb to create a table for the 120 | model, if you are using South for migrations, you can also create the model as usual via migrate. No additional 121 | steps required. After that we need to make a few changes to the model: 122 | 123 | | 1) In models.py add the following import statement at the top of the file: 124 | 125 | .. code-block:: python 126 | 127 | from dbparti.models import Partitionable 128 | 129 | | 2) Make your model to inherit from Partitionable, to do that change: 130 | 131 | .. code-block:: python 132 | 133 | class YourModelName(models.Model): 134 | 135 | to: 136 | 137 | .. code-block:: python 138 | 139 | class YourModelName(Partitionable): 140 | 141 | | 3) Add a Meta class to your model which inherits from Partitionable.Meta with a few settings (or if you already 142 | have a Meta class change it as the following, keep in mind that this is just an example configuration for a 143 | model, you have to enter values which represent your exact situation): 144 | 145 | .. code-block:: python 146 | 147 | class Meta(Partitionable.Meta): 148 | partition_type = 'range' 149 | partition_subtype = 'date' 150 | partition_range = 'month' 151 | partition_column = 'added' 152 | 153 | | 4) Lastly we need to initialize some database stuff, to do that execute the following command: 154 | 155 | .. code-block:: bash 156 | 157 | $ python manage.py partition app_name 158 | 159 | That's it! Easy right?! Now a few words about what we just did. We made our model to inherit from Partitionable, 160 | also we used "month" as partition range and "added" as partition column, that means that from now on, a new 161 | partition will be created every month and a value from "added" column will be used to determine into what 162 | partition the data should be saved. Keep in mind that if you add new partitioned models to your apps or change 163 | any settings in the existing partitioned models, you need to rerun the command from step 4, otherwise the database 164 | won't know about your changes. You can also customize how data from that model will be displayed in the Django 165 | admin interface, for that you need to do the following: 166 | 167 | | 1) In admin.py add the following import statement at the top of the file: 168 | 169 | .. code-block:: python 170 | 171 | from dbparti.admin import PartitionableAdmin 172 | 173 | | 2) Create admin model as usual and then change: 174 | 175 | .. code-block:: python 176 | 177 | class YourAdminModelName(admin.ModelAdmin): 178 | 179 | to: 180 | 181 | .. code-block:: python 182 | 183 | class YourAdminModelName(PartitionableAdmin): 184 | 185 | | 3) Add a setting inside ModelAdmin class which tells how records are displayed in Django admin interface: 186 | 187 | .. code-block:: python 188 | 189 | partition_show = 'all' 190 | 191 | Available settings 192 | ------------------ 193 | 194 | Model settings 195 | ~~~~~~~~~~~~~~ 196 | 197 | All model settings are done inside model's Meta class which should inherit from Partitionable.Meta 198 | 199 | ``partition_type`` - what partition type will be used on the model, currently accepts the following: 200 | 201 | * range 202 | 203 | ``partition_subtype`` - what partition subtype will be used on the model, currently used only when 204 | "partition_type" is set to "range" and accepts the following values: 205 | 206 | * date 207 | 208 | ``partition_range`` - how often a new partition will be created, currently accepts the following: 209 | 210 | * day 211 | * week 212 | * month 213 | * year 214 | 215 | ``partition_column`` - column, which value will be used to determine which partition record belongs to 216 | 217 | ModelAdmin settings 218 | ~~~~~~~~~~~~~~~~~~~ 219 | 220 | All model admin settings are done inside model admin class itself 221 | 222 | ``partition_show`` - data from which partition will be shown in Django admin, accepts the following values: 223 | 224 | * all (default) 225 | * current 226 | * previous 227 | 228 | Example 229 | ------- 230 | 231 | Let's imagine that we would like to create a table for storing log files. Without partitioning our table would 232 | have millions of rows very soon and as the table grows performance will become slower. With partitioning we can 233 | tell database that we want a new table to be created every month and that we will use a value from some column 234 | to determine to which partition every new record belongs to. To be more specific let's call our table "logs", it 235 | will have only 3 columns: id, content and added. Now when we insert the following record: id='1', content='blah', 236 | added='2013-05-20', this record will be inserted not to our "logs" table but to the "logs_y2013m05" partition, 237 | then if we insert another record like that: id='2', content='yada', added='2013-07-16' it will be inserted to the 238 | partition "logs_y2013m07" BUT the great thing about all of that is that you are doing your inserts/updates/selects 239 | on the table "logs"! Again, you are working with the table "logs" as usual and you don't may even know that 240 | actually your data is stored in a lot of different partitions, everything is done for you automatically at the 241 | database level, isn't that cool ?! 242 | 243 | Contact and Support 244 | ------------------- 245 | 246 | I will be glad to get your feedback, pull requests, issues, whatever. Feel free to contact me for any questions. 247 | 248 | Copyright and License 249 | --------------------- 250 | 251 | ``django-db-parti`` is protected by BSD licence. Check the LICENCE_ for details. 252 | 253 | .. _LICENCE: https://github.com/maxtepkeev/django-db-parti/blob/master/LICENSE 254 | .. _pypi: https://pypi.python.org/pypi/django-db-parti 255 | .. _github: https://github.com/maxtepkeev/django-db-parti 256 | .. _Django: https://www.djangoproject.com 257 | -------------------------------------------------------------------------------- /dbparti/__init__.py: -------------------------------------------------------------------------------- 1 | from django.db import connection, transaction 2 | from dbparti.backends.exceptions import BackendError 3 | 4 | 5 | try: 6 | backend = __import__('dbparti.backends.{0}'.format(connection.vendor), fromlist='*') 7 | except ImportError: 8 | import pkgutil, os 9 | raise BackendError( 10 | allowed_values=[name for _, name, is_package in pkgutil.iter_modules( 11 | [os.path.join(os.path.dirname(__file__), 'backends')]) if is_package] 12 | ) 13 | -------------------------------------------------------------------------------- /dbparti/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from dbparti import backend 3 | from dbparti.backends.exceptions import PartitionColumnError, PartitionFilterError 4 | 5 | 6 | class PartitionableAdmin(admin.ModelAdmin): 7 | partition_show = 'all' 8 | 9 | def __init__(self, *args, **kwargs): 10 | super(PartitionableAdmin, self).__init__(*args, **kwargs) 11 | 12 | if not self.opts.partition_column in self.opts.get_all_field_names(): 13 | raise PartitionColumnError( 14 | model=self.opts.__dict__['object_name'], 15 | current_value=self.opts.partition_column, 16 | allowed_values=self.opts.get_all_field_names() 17 | ) 18 | 19 | try: 20 | self.filter = getattr(backend.filters, '{0}PartitionFilter'.format( 21 | self.opts.partition_type.capitalize()))(self.partition_show, **self.opts.__dict__) 22 | except AttributeError: 23 | import re 24 | raise PartitionFilterError( 25 | model=self.opts.__dict__['object_name'], 26 | current_value=self.opts.partition_type, 27 | allowed_values=[c.replace('PartitionFilter', '').lower() for c in dir( 28 | backend.filters) if re.match('\w+PartitionFilter', c) is not None and 'Base' not in c] 29 | ) 30 | 31 | def queryset(self, request): 32 | """Determines data from what partitions should be shown in django admin""" 33 | qs = super(PartitionableAdmin, self).queryset(request) 34 | 35 | if self.partition_show != 'all': 36 | qs = qs.extra(where=self.filter.apply()) 37 | 38 | return qs 39 | -------------------------------------------------------------------------------- /dbparti/backends/__init__.py: -------------------------------------------------------------------------------- 1 | from dbparti import connection, transaction 2 | 3 | 4 | class BasePartition(object): 5 | """Base partition class for all backends. All backends should inherit from it.""" 6 | def __init__(self, column_value, column_type, **kwargs): 7 | self.cursor = connection.cursor() 8 | self.model = kwargs['object_name'] 9 | self.table = kwargs['db_table'] 10 | self.partition_pk = kwargs['pk'] 11 | self.partition_column = kwargs['partition_column'] 12 | self.column_value = column_value 13 | self.column_type = column_type 14 | 15 | def prepare(self): 16 | """Prepares everything that is needed to initialize partitioning""" 17 | raise NotImplementedError('Prepare method not implemented for partition type: {0}'.format(self.__class__.__name__)) 18 | 19 | def exists(self): 20 | """Checks if partition exists""" 21 | raise NotImplementedError('Exists method not implemented for partition type: {0}'.format(self.__class__.__name__)) 22 | 23 | def create(self): 24 | """Creates new partition""" 25 | raise NotImplementedError('Create method not implemented for partition type: {0}'.format(self.__class__.__name__)) 26 | 27 | def _get_name(self): 28 | """Defines name for a new partition""" 29 | raise NotImplementedError('Name method not implemented for partition type: {0}'.format(self.__class__.__name__)) 30 | 31 | def _get_partition_function(self): 32 | """Contains a partition function that is used to create new partitions at database level""" 33 | raise NotImplementedError('Partition function method not implemented for partition type: {0}'.format(self.__class__.__name__)) 34 | 35 | 36 | class BasePartitionFilter(object): 37 | """Base class for all filter types. All filter types should inherit from it.""" 38 | def __init__(self, partition_show, **kwargs): 39 | self.partition_show = partition_show 40 | self.model = kwargs['object_name'] 41 | self.table = kwargs['db_table'] 42 | self.partition_column = kwargs['partition_column'] 43 | 44 | def apply(self): 45 | """Contains a filter that needs to be applied to queryset""" 46 | raise NotImplementedError('Filter not implemented for type: {0}'.format(self.__class__.__name__)) 47 | -------------------------------------------------------------------------------- /dbparti/backends/exceptions.py: -------------------------------------------------------------------------------- 1 | from dbparti import connection 2 | 3 | 4 | class BasePartitionError(Exception): 5 | """Base exception class for backend exceptions""" 6 | def __init__(self, message, **kwargs): 7 | self.message = message 8 | self.model = kwargs.get('model', None) 9 | self.current_value = kwargs.get('current_value', None) 10 | self.allowed_values = kwargs.get('allowed_values', None) 11 | 12 | def __str__(self): 13 | return self.message.format( 14 | model=self.model, 15 | current=self.current_value, 16 | vendor=connection.vendor, 17 | allowed=', '.join(list(self.allowed_values)) 18 | ) 19 | 20 | 21 | class BackendError(BasePartitionError): 22 | """Unsupported database backend""" 23 | def __init__(self, **kwargs): 24 | super(BackendError, self).__init__( 25 | 'Unsupported database backend "{vendor}", supported backends are: {allowed}', 26 | **kwargs 27 | ) 28 | 29 | 30 | class PartitionColumnError(BasePartitionError): 31 | """Undefined partition column""" 32 | def __init__(self, **kwargs): 33 | super(PartitionColumnError, self).__init__( 34 | 'Undefined partition column "{current}" in "{model}" model, available columns are: {allowed}', 35 | **kwargs 36 | ) 37 | 38 | 39 | class PartitionTypeError(BasePartitionError): 40 | """Unsupported partition type""" 41 | def __init__(self, **kwargs): 42 | super(PartitionTypeError, self).__init__( 43 | 'Unsupported partition type "{current}" in "{model}" model, supported types for "{vendor}" backend are: {allowed}', 44 | **kwargs 45 | ) 46 | 47 | 48 | class PartitionFilterError(BasePartitionError): 49 | """Unsupported partition filter""" 50 | def __init__(self, **kwargs): 51 | super(PartitionFilterError, self).__init__( 52 | 'Unsupported partition filter "{current}" in "{model}" model, supported filters for "{vendor}" backend are: {allowed}', 53 | **kwargs 54 | ) 55 | 56 | 57 | class PartitionRangeError(BasePartitionError): 58 | """Unsupported partition range""" 59 | def __init__(self, **kwargs): 60 | super(PartitionRangeError, self).__init__( 61 | 'Unsupported partition range "{current}" in "{model}" model, supported partition ranges for backend "{vendor}" are: {allowed}', 62 | **kwargs 63 | ) 64 | 65 | 66 | class PartitionRangeSubtypeError(BasePartitionError): 67 | """Unsupported partition range subtype""" 68 | def __init__(self, **kwargs): 69 | super(PartitionRangeSubtypeError, self).__init__( 70 | 'Unsupported partition range subtype "{current}" in "{model}" model, supported range subtypes for backend "{vendor}" are: {allowed}', 71 | **kwargs 72 | ) 73 | 74 | 75 | class PartitionShowError(BasePartitionError): 76 | """Unsupported partition show type""" 77 | def __init__(self, **kwargs): 78 | super(PartitionShowError, self).__init__( 79 | 'Unsupported partition show type "{current}" in "{model}" admin class, supported partition show types for backend "{vendor}" are: {allowed}', 80 | **kwargs 81 | ) 82 | 83 | 84 | class PartitionFunctionError(BasePartitionError): 85 | """Unsupported partition function""" 86 | def __init__(self, **kwargs): 87 | super(PartitionFunctionError, self).__init__( 88 | 'Unsupported partition function for column type "{current}", supported column types for "{vendor}" backend are: {allowed}', 89 | **kwargs 90 | ) 91 | -------------------------------------------------------------------------------- /dbparti/backends/mysql/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ('filters', 'partition') 2 | -------------------------------------------------------------------------------- /dbparti/backends/mysql/filters.py: -------------------------------------------------------------------------------- 1 | from dbparti.backends import BasePartitionFilter 2 | from dbparti.backends.exceptions import ( 3 | PartitionRangeError, 4 | PartitionRangeSubtypeError, 5 | PartitionShowError 6 | ) 7 | 8 | 9 | """MySQL backend partition filters for django admin""" 10 | class PartitionFilter(BasePartitionFilter): 11 | """Common methods for all partition filters""" 12 | pass 13 | 14 | 15 | class RangePartitionFilter(PartitionFilter): 16 | """Range partition filter implementation""" 17 | def __init__(self, *args, **kwargs): 18 | super(RangePartitionFilter, self).__init__(*args, **kwargs) 19 | self.partition_range = kwargs['partition_range'] 20 | self.partition_subtype = kwargs['partition_subtype'] 21 | 22 | def apply(self): 23 | """Dynamically loads needed partition filter depending on the partition subtype""" 24 | try: 25 | return getattr(self, '_get_{0}_filter'.format(self.partition_subtype))() 26 | except AttributeError: 27 | import re 28 | raise PartitionRangeSubtypeError( 29 | model=self.model, 30 | current_value=self.partition_subtype, 31 | allowed_values=[re.match('_get_(\w+)_filter', c).group(1) for c in dir( 32 | self) if re.match('_get_\w+_filter', c) is not None] 33 | ) 34 | 35 | def _get_date_filter(self): 36 | """Contains a partition filter for date partition subtype""" 37 | ranges = { 38 | 'year': [ 39 | "EXTRACT(YEAR FROM {0}.{1}) = EXTRACT(YEAR FROM {2})", 40 | ], 41 | 'month': [ 42 | "EXTRACT(YEAR FROM {0}.{1}) = EXTRACT(YEAR FROM NOW())", 43 | "EXTRACT(MONTH FROM {2}.{3}) = EXTRACT(MONTH FROM {4})", 44 | ], 45 | 'week': [ 46 | "EXTRACT(YEAR FROM {0}.{1}) = EXTRACT(YEAR FROM NOW())", 47 | "EXTRACT(WEEK FROM {2}.{3}) = EXTRACT(WEEK FROM {4})", 48 | ], 49 | 'day': [ 50 | "EXTRACT(YEAR FROM {0}.{1}) = EXTRACT(YEAR FROM NOW())", 51 | "EXTRACT(MONTH FROM {2}.{3}) = EXTRACT(MONTH FROM NOW())", 52 | "EXTRACT(DAY FROM {4}.{5}) = EXTRACT(DAY FROM {6})", 53 | ], 54 | } 55 | 56 | try: 57 | show_range = ranges[self.partition_range] 58 | except KeyError: 59 | raise PartitionRangeError(model=self.model, current_value=self.partition_range, allowed_values=ranges.keys()) 60 | 61 | shows = { 62 | 'current': 'NOW()', 63 | 'previous': 'NOW() - INTERVAL 1 {0}', 64 | } 65 | 66 | try: 67 | show = shows[self.partition_show] 68 | except KeyError: 69 | raise PartitionShowError(model=self.model, current_value=self.partition_show, allowed_values=shows.keys()) 70 | 71 | return [item.format(self.table, self.partition_column, show.format(self.partition_range)) for item in show_range] 72 | -------------------------------------------------------------------------------- /dbparti/backends/mysql/partition.py: -------------------------------------------------------------------------------- 1 | from dbparti.backends import BasePartition, transaction 2 | from dbparti.backends.utilities import DateTimeUtil 3 | from dbparti.backends.exceptions import PartitionRangeSubtypeError, PartitionFunctionError 4 | 5 | 6 | """ 7 | MySQL partition backend. 8 | 9 | MySQL supports partitioning natively via PARTITION BY clause. Thing to keep in 10 | mind is that python MySQLdb doesn't support compound MySQL statements, so each 11 | separate statement should be executed inside each self.cursor.execute(). 12 | """ 13 | class Partition(BasePartition): 14 | """Common methods for all partition types""" 15 | def prepare(self): 16 | """Converts original table to partitioned one""" 17 | self.cursor.execute(""" 18 | -- We need to rebuild primary key for our partitioning to work 19 | ALTER table {parent_table} DROP PRIMARY KEY, add PRIMARY KEY ({pk}, {partition_column}); 20 | """.format( 21 | pk=self.partition_pk.column, 22 | parent_table=self.table, 23 | partition_column=self.partition_column, 24 | )) 25 | 26 | transaction.commit_unless_managed() 27 | 28 | def exists(self): 29 | """Checks if partition exists""" 30 | self.cursor.execute(""" 31 | SELECT EXISTS( 32 | SELECT 1 FROM information_schema.partitions 33 | WHERE table_name='{parent_table}' AND partition_name='{partition_name}'); 34 | """.format( 35 | parent_table=self.table, 36 | partition_name=self._get_name(), 37 | )) 38 | 39 | return self.cursor.fetchone()[0] 40 | 41 | 42 | class RangePartition(Partition): 43 | """Range partition type implementation""" 44 | def __init__(self, *args, **kwargs): 45 | super(RangePartition, self).__init__(*args, **kwargs) 46 | self.partition_range = kwargs['partition_range'] 47 | self.partition_subtype = kwargs['partition_subtype'] 48 | self.datetime = DateTimeUtil(self.column_value, self.partition_range, model=self.model) 49 | 50 | def prepare(self): 51 | """Converts original table to partitioned one""" 52 | super(RangePartition, self).prepare() 53 | self.datetime.now = None 54 | 55 | self.cursor.execute(""" 56 | -- We need to create zero partition to speed up things due to the partitioning 57 | -- implementation in the early versions of MySQL database (see bug #49754) 58 | ALTER TABLE {parent_table} PARTITION BY RANGE ({function}({partition_column}))( 59 | PARTITION {partition_pattern} VALUES LESS THAN (0) 60 | ); 61 | """.format( 62 | parent_table=self.table, 63 | partition_column=self.partition_column, 64 | partition_pattern=self._get_name(), 65 | function=self._get_partition_function(), 66 | )) 67 | 68 | transaction.commit_unless_managed() 69 | 70 | def create(self): 71 | """Creates new partition""" 72 | self.cursor.execute(""" 73 | ALTER TABLE {parent_table} ADD PARTITION ( 74 | PARTITION {child_table} VALUES LESS THAN ({function}('{period_end}') + {addition}) 75 | ); 76 | """.format( 77 | child_table=self._get_name(), 78 | parent_table=self.table, 79 | function=self._get_partition_function(), 80 | period_end=self.datetime.get_period()[1], 81 | addition='86400' if self._get_column_type() == 'timestamp' else '1', 82 | )) 83 | 84 | transaction.commit_unless_managed() 85 | 86 | def _get_name(self): 87 | """Dynamically defines new partition name depending on the partition subtype""" 88 | try: 89 | return getattr(self, '_get_{0}_name'.format(self.partition_subtype))() 90 | except AttributeError: 91 | import re 92 | raise PartitionRangeSubtypeError( 93 | model=self.model, 94 | current_value=self.partition_subtype, 95 | allowed_values=[re.match('_get_(\w+)_name', c).group(1) for c in dir( 96 | self) if re.match('_get_\w+_name', c) is not None] 97 | ) 98 | 99 | def _get_date_name(self): 100 | """Defines name for a new partition for date partition subtype""" 101 | return '{0}_{1}'.format(self.table, self.datetime.get_name()) 102 | 103 | def _get_partition_function(self): 104 | """Returns correct partition function depending on the MySQL column type""" 105 | functions = { 106 | 'date': 'TO_DAYS', 107 | 'datetime': 'TO_DAYS', 108 | 'timestamp': 'UNIX_TIMESTAMP', 109 | } 110 | 111 | column_type = self._get_column_type() 112 | 113 | try: 114 | return functions[column_type] 115 | except KeyError: 116 | raise PartitionFunctionError(current_value=column_type, allowed_values=functions.keys()) 117 | 118 | def _get_column_type(self): 119 | """ 120 | We can't rely on self.column_type in MySQL, because Django uses only date 121 | and datetime types internally, but MySQL has an additional timestamp type 122 | and we need to know that, otherwise we can apply incorrect partition function 123 | """ 124 | self.cursor.execute(""" 125 | SELECT data_type 126 | FROM information_schema.columns 127 | WHERE table_name = '{parent_table}' AND column_name = '{partition_column}'; 128 | """.format( 129 | parent_table=self.table, 130 | partition_column=self.partition_column, 131 | )) 132 | 133 | return self.cursor.fetchone()[0] 134 | -------------------------------------------------------------------------------- /dbparti/backends/postgresql/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ('filters', 'partition') 2 | -------------------------------------------------------------------------------- /dbparti/backends/postgresql/filters.py: -------------------------------------------------------------------------------- 1 | from dbparti.backends import BasePartitionFilter 2 | from dbparti.backends.exceptions import ( 3 | PartitionRangeError, 4 | PartitionRangeSubtypeError, 5 | PartitionShowError 6 | ) 7 | 8 | 9 | """PostgreSQL backend partition filters for django admin""" 10 | class PartitionFilter(BasePartitionFilter): 11 | """Common methods for all partition filters""" 12 | pass 13 | 14 | 15 | class RangePartitionFilter(PartitionFilter): 16 | """Range partition filter implementation""" 17 | def __init__(self, *args, **kwargs): 18 | super(RangePartitionFilter, self).__init__(*args, **kwargs) 19 | self.partition_range = kwargs['partition_range'] 20 | self.partition_subtype = kwargs['partition_subtype'] 21 | 22 | def apply(self): 23 | """Dynamically loads needed partition filter depending on the partition subtype""" 24 | try: 25 | return getattr(self, '_get_{0}_filter'.format(self.partition_subtype))() 26 | except AttributeError: 27 | import re 28 | raise PartitionRangeSubtypeError( 29 | model=self.model, 30 | current_value=self.partition_subtype, 31 | allowed_values=[re.match('_get_(\w+)_filter', c).group(1) for c in dir( 32 | self) if re.match('_get_\w+_filter', c) is not None] 33 | ) 34 | 35 | def _get_date_filter(self): 36 | """Contains a partition filter for date partition subtype""" 37 | ranges = { 38 | 'year': [ 39 | "EXTRACT('year' FROM {0}.{1}) = EXTRACT('year' FROM {2})", 40 | ], 41 | 'month': [ 42 | "EXTRACT('year' FROM {0}.{1}) = EXTRACT('year' FROM NOW())", 43 | "EXTRACT('month' FROM {2}.{3}) = EXTRACT('month' FROM {4})", 44 | ], 45 | 'week': [ 46 | "EXTRACT('year' FROM {0}.{1}) = EXTRACT('year' FROM NOW())", 47 | "EXTRACT('week' FROM {2}.{3}) = EXTRACT('week' FROM {4})", 48 | ], 49 | 'day': [ 50 | "EXTRACT('year' FROM {0}.{1}) = EXTRACT('year' FROM NOW())", 51 | "EXTRACT('month' FROM {2}.{3}) = EXTRACT('month' FROM NOW())", 52 | "EXTRACT('day' FROM {4}.{5}) = EXTRACT('day' FROM {6})", 53 | ], 54 | } 55 | 56 | try: 57 | show_range = ranges[self.partition_range] 58 | except KeyError: 59 | raise PartitionRangeError(model=self.model, current_value=self.partition_range, allowed_values=ranges.keys()) 60 | 61 | shows = { 62 | 'current': 'NOW()', 63 | 'previous': "NOW() - '1 {0}'::interval", 64 | } 65 | 66 | try: 67 | show = shows[self.partition_show] 68 | except KeyError: 69 | raise PartitionShowError(model=self.model, current_value=self.partition_show, allowed_values=shows.keys()) 70 | 71 | return [item.format(self.table, self.partition_column, show.format(self.partition_range)) for item in show_range] 72 | -------------------------------------------------------------------------------- /dbparti/backends/postgresql/partition.py: -------------------------------------------------------------------------------- 1 | from dbparti.backends import BasePartition, transaction 2 | from dbparti.backends.exceptions import ( 3 | PartitionRangeError, 4 | PartitionRangeSubtypeError 5 | ) 6 | 7 | 8 | """ 9 | PostgreSQL partition backend. 10 | 11 | PostgreSQL supports partitioning via inheritance. 12 | """ 13 | class Partition(BasePartition): 14 | """Common methods for all partition types""" 15 | def prepare(self): 16 | """Prepares needed triggers and functions for those triggers""" 17 | self.cursor.execute(""" 18 | -- We need to create a before insert function 19 | CREATE OR REPLACE FUNCTION {parent_table}_insert_child() 20 | RETURNS TRIGGER AS $$ 21 | {partition_function} 22 | $$ LANGUAGE plpgsql; 23 | 24 | -- Then we create a trigger which calls the before insert function 25 | DO $$ 26 | BEGIN 27 | IF NOT EXISTS( 28 | SELECT 1 29 | FROM information_schema.triggers 30 | WHERE event_object_table = '{parent_table}' 31 | AND trigger_name = 'before_insert_{parent_table}_trigger' 32 | ) THEN 33 | CREATE TRIGGER before_insert_{parent_table}_trigger 34 | BEFORE INSERT ON {parent_table} 35 | FOR EACH ROW EXECUTE PROCEDURE {parent_table}_insert_child(); 36 | END IF; 37 | END $$; 38 | 39 | -- Then we create a function to delete duplicate row from the master table after insert 40 | CREATE OR REPLACE FUNCTION {parent_table}_delete_master() 41 | RETURNS TRIGGER AS $$ 42 | BEGIN 43 | DELETE FROM ONLY {parent_table} WHERE {pk} = NEW.{pk}; 44 | RETURN NEW; 45 | END; 46 | $$ LANGUAGE plpgsql; 47 | 48 | -- Lastly we create the after insert trigger that calls the after insert function 49 | DO $$ 50 | BEGIN 51 | IF NOT EXISTS( 52 | SELECT 1 53 | FROM information_schema.triggers 54 | WHERE event_object_table = '{parent_table}' 55 | AND trigger_name = 'after_insert_{parent_table}_trigger' 56 | ) THEN 57 | CREATE TRIGGER after_insert_{parent_table}_trigger 58 | AFTER INSERT ON {parent_table} 59 | FOR EACH ROW EXECUTE PROCEDURE {parent_table}_delete_master(); 60 | END IF; 61 | END $$; 62 | """.format( 63 | pk=self.partition_pk.column, 64 | parent_table=self.table, 65 | partition_function=self._get_partition_function() 66 | )) 67 | 68 | transaction.commit_unless_managed() 69 | 70 | def exists(self): 71 | """Checks if partition exists. Not used in this backend because everything is done at the database level""" 72 | return True 73 | 74 | def create(self): 75 | """Creates new partition. Not used in this backend because everything is done at the database level""" 76 | pass 77 | 78 | 79 | class RangePartition(Partition): 80 | """Range partition type implementation""" 81 | def __init__(self, *args, **kwargs): 82 | super(RangePartition, self).__init__(*args, **kwargs) 83 | self.partition_range = kwargs['partition_range'] 84 | self.partition_subtype = kwargs['partition_subtype'] 85 | 86 | def _get_partition_function(self): 87 | """Dynamically loads needed before insert function body depending on the partition subtype""" 88 | try: 89 | return getattr(self, '_get_{0}_partition_function'.format(self.partition_subtype))() 90 | except AttributeError: 91 | import re 92 | raise PartitionRangeSubtypeError( 93 | model=self.model, 94 | current_value=self.partition_subtype, 95 | allowed_values=[re.match('_get_(\w+)_partition_function', c).group(1) for c in dir( 96 | self) if re.match('_get_\w+_partition_function', c) is not None] 97 | ) 98 | 99 | def _get_date_partition_function(self): 100 | """Contains a before insert function body for date partition subtype""" 101 | patterns = { 102 | 'day': '"y"YYYY"d"DDD', 103 | 'week': '"y"IYYY"w"IW', 104 | 'month': '"y"YYYY"m"MM', 105 | 'year': '"y"YYYY', 106 | } 107 | 108 | try: 109 | partition_pattern = patterns[self.partition_range] 110 | except KeyError: 111 | raise PartitionRangeError(model=self.model, current_value=self.partition_range, allowed_values=patterns.keys()) 112 | 113 | return """ 114 | DECLARE tablename TEXT; 115 | DECLARE columntype TEXT; 116 | DECLARE startdate TIMESTAMP; 117 | BEGIN 118 | startdate := date_trunc('{partition_range}', NEW.{partition_column}); 119 | tablename := '{parent_table}_' || to_char(NEW.{partition_column}, '{partition_pattern}'); 120 | 121 | IF NOT EXISTS( 122 | SELECT 1 FROM information_schema.tables WHERE table_name=tablename) 123 | THEN 124 | SELECT data_type INTO columntype 125 | FROM information_schema.columns 126 | WHERE table_name = '{parent_table}' AND column_name = '{partition_column}'; 127 | 128 | EXECUTE 'CREATE TABLE ' || tablename || ' ( 129 | CHECK ( 130 | {partition_column} >= ''' || startdate || '''::' || columntype || ' AND 131 | {partition_column} < ''' || (startdate + '1 {partition_range}'::interval) || '''::' || columntype || ' 132 | ) 133 | ) INHERITS ({parent_table});'; 134 | 135 | EXECUTE 'CREATE INDEX ' || tablename || '_{partition_column} ON ' || tablename || ' ({partition_column});'; 136 | END IF; 137 | 138 | EXECUTE 'INSERT INTO ' || tablename || ' VALUES (($1).*);' USING NEW; 139 | RETURN NEW; 140 | END; 141 | """.format( 142 | parent_table=self.table, 143 | partition_range=self.partition_range, 144 | partition_column=self.partition_column, 145 | partition_pattern=partition_pattern 146 | ) 147 | -------------------------------------------------------------------------------- /dbparti/backends/utilities.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from dbparti.backends.exceptions import PartitionRangeError 3 | 4 | 5 | """Provides date and time calculations for some database backends""" 6 | class DateTimeUtil(object): 7 | def __init__(self, now, period, format='%Y-%m-%d %H:%M:%S', model=None): 8 | self.now = now 9 | self.period = period 10 | self.format = format 11 | self.model = model 12 | 13 | def get_name(self): 14 | """Returns name of the partition depending on the given date and period""" 15 | patterns = { 16 | 'day': {'real': 'y%Yd%j', 'none': 'y0000d000'}, 17 | 'week': {'real': 'y%Yw%V', 'none': 'y0000w00'}, 18 | 'month': {'real': 'y%Ym%m', 'none': 'y0000m00'}, 19 | 'year': {'real': 'y%Y', 'none': 'y0000'}, 20 | } 21 | 22 | try: 23 | return patterns[self.period]['none'] if self.now is None else self.now.strftime(patterns[self.period]['real']) 24 | except KeyError: 25 | raise PartitionRangeError(model=self.model, current_value=self.period, allowed_values=patterns.keys()) 26 | 27 | def get_period(self): 28 | """Dynamically returns beginning and an end depending on the given period""" 29 | try: 30 | return getattr(self, '_get_{0}_period'.format(self.period))() 31 | except AttributeError: 32 | import re 33 | raise PartitionRangeError( 34 | model=self.model, 35 | current_value=self.period, 36 | allowed_values=[re.match('_get_(\w+)_period', c).group(1) for c in dir( 37 | self) if re.match('_get_\w+_period', c) is not None] 38 | ) 39 | 40 | def _get_day_period(self): 41 | """Returns beginning and an end for a day period""" 42 | start = self.now.replace(hour=0, minute=0, second=0, microsecond=0) 43 | end = self.now.replace(hour=23, minute=59, second=59, microsecond=999999) 44 | 45 | return start.strftime(self.format), end.strftime(self.format) 46 | 47 | def _get_week_period(self): 48 | """Returns beginning and an end for a week period""" 49 | date_ = datetime(self.now.year, 1, 1) 50 | 51 | if date_.weekday() > 3: 52 | date_ = date_ + timedelta(7 - date_.weekday()) 53 | else: 54 | date_ = date_ - timedelta(date_.weekday()) 55 | 56 | days = timedelta(days=(int(self.now.strftime('%V')) - 1) * 7) 57 | 58 | start = (date_ + days) 59 | end = (date_ + days + timedelta(days=6)).replace(hour=23, minute=59, second=59, microsecond=999999) 60 | 61 | return start.strftime(self.format), end.strftime(self.format) 62 | 63 | def _get_month_period(self): 64 | """Returns beginning and an end for a month period""" 65 | fday = datetime(self.now.year, self.now.month, 1) 66 | 67 | if self.now.month == 12: 68 | lday = datetime(self.now.year, self.now.month, 31, 23, 59, 59, 999999) 69 | else: 70 | lday = (datetime(self.now.year, self.now.month + 1, 1, 23, 59, 59, 999999) - timedelta(days=1)) 71 | 72 | return fday.strftime(self.format), lday.strftime(self.format) 73 | 74 | def _get_year_period(self): 75 | """Returns beginning and an end for a year period""" 76 | fday = datetime(self.now.year, 1, 1) 77 | lday = (datetime(self.now.year + 1, 1, 1, 23, 59, 59, 999999) - timedelta(days=1)) 78 | 79 | return fday.strftime(self.format), lday.strftime(self.format) 80 | -------------------------------------------------------------------------------- /dbparti/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxtepkeev/django-db-parti/e672b2c771d5c99854bf86c41452ad04dd4b93cf/dbparti/management/__init__.py -------------------------------------------------------------------------------- /dbparti/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxtepkeev/django-db-parti/e672b2c771d5c99854bf86c41452ad04dd4b93cf/dbparti/management/commands/__init__.py -------------------------------------------------------------------------------- /dbparti/management/commands/partition.py: -------------------------------------------------------------------------------- 1 | from dbparti.models import Partitionable 2 | from django.db.models import get_models 3 | from django.core.management.base import AppCommand 4 | 5 | 6 | class Command(AppCommand): 7 | help = 'Configures the database for partitioned models' 8 | 9 | def handle_app(self, app, **options): 10 | """Configures all needed database stuff depending on the backend used""" 11 | names = [] 12 | 13 | for model in get_models(app): 14 | if issubclass(model, Partitionable): 15 | names.append(model.__name__) 16 | 17 | model_instance = model() 18 | model_instance.get_partition().prepare() 19 | 20 | if not names: 21 | self.stderr.write('Unable to find any partitionable models in an app: ' + app.__name__.split('.')[0] + '\n') 22 | else: 23 | self.stdout.write( 24 | 'Successfully (re)configured the database for the following models: ' + ', '.join(names) + '\n' 25 | ) 26 | -------------------------------------------------------------------------------- /dbparti/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from dbparti import backend 3 | from dbparti.backends.exceptions import PartitionColumnError, PartitionTypeError 4 | 5 | 6 | models.options.DEFAULT_NAMES += ( 7 | 'partition_range', 8 | 'partition_column', 9 | 'partition_type', 10 | 'partition_subtype', 11 | 'partition_list', 12 | ) 13 | 14 | 15 | class Partitionable(models.Model): 16 | def get_partition(self): 17 | try: 18 | field = self._meta.get_field(self._meta.partition_column) 19 | column_value = field.pre_save(self, self.pk is None) 20 | column_type = field.get_internal_type() 21 | except AttributeError: 22 | raise PartitionColumnError( 23 | model=self.__class__.__name__, 24 | current_value=self._meta.partition_column, 25 | allowed_values=self._meta.get_all_field_names() 26 | ) 27 | 28 | try: 29 | return getattr(backend.partition, '{0}Partition'.format( 30 | self._meta.partition_type.capitalize()))(column_value, column_type, **self._meta.__dict__) 31 | except AttributeError: 32 | import re 33 | raise PartitionTypeError( 34 | model=self.__class__.__name__, 35 | current_value=self._meta.partition_type, 36 | allowed_values=[c.replace('Partition', '').lower() for c in dir( 37 | backend.partition) if re.match('\w+Partition', c) is not None and 'Base' not in c] 38 | ) 39 | 40 | def save(self, *args, **kwargs): 41 | partition = self.get_partition() 42 | 43 | if not partition.exists(): 44 | partition.create() 45 | 46 | super(Partitionable, self).save(*args, **kwargs) 47 | 48 | class Meta: 49 | abstract = True 50 | partition_type = 'None' 51 | partition_subtype = 'None' 52 | partition_range = 'None' 53 | partition_column = 'None' 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name='django-db-parti', 6 | version='0.3.3', 7 | packages=find_packages(), 8 | url='https://github.com/maxtepkeev/django-db-parti', 9 | license=open('LICENSE').read(), 10 | author='Max Tepkeev', 11 | author_email='tepkeev@gmail.com', 12 | description='Fully automatic database table partitioning for Django', 13 | long_description=open('README.rst').read() + '\n\n' + 14 | open('CHANGELOG.rst').read(), 15 | keywords='django,partition,database,table', 16 | install_requires=['Django >= 1.4'], 17 | zip_safe=False, 18 | classifiers=[ 19 | 'Framework :: Django', 20 | 'Development Status :: 4 - Beta', 21 | 'Topic :: Internet', 22 | 'Topic :: Database', 23 | 'License :: OSI Approved :: BSD License', 24 | 'Intended Audience :: Developers', 25 | 'Environment :: Web Environment', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3.3', 28 | ], 29 | ) 30 | --------------------------------------------------------------------------------