Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Packaging Django Projects for PyPI

Packaging Django Projects for PyPI

How to package a Django project to behave and install like a Python package. Lessons learned packaging Mayan EDMS the free open source document manager based in Django.

Roberto Rosario

April 18, 2015
Tweet

More Decks by Roberto Rosario

Other Decks in How-to & DIY

Transcript

  1. virtualenv venv source venv/bin/activate git clone <project> pip install -r

    requirements.txt vi <project>/settings.py # Setup database # Setup media files # Set secret key # Customize other settings $ ./manage.py syncdb You have installed Django's auth system, and don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (leave blank to use 'rosarior'): admin Email address: [email protected] Password: Password (again): Superuser created successfully. $ ./manage.py migrate $ ./manage.py runserver
  2. • Our users are rarely Django/Python programmers • Hard to

    install • Hard to override settings • One settings.py file per instance • Manual SECRET_KEY creation • Flat structure: project, media files and apps at the same level • Media files? Where? • Initial setup is not configurable • Insecure initial admin user creation • Hard to manage after install • Least amount of installation steps
  3. Python package $ source venv/bin/activate $ pip install python-package Mayan

    EDMS $ virtualenv venv $ source venv/bin/activate $ pip install mayan-edms $ mayan-edms.py initialsetup
  4. Namespaces are one honking great idea -- let's do more

    of those! https://www.python.org/dev/peps/pep-0020/
  5. mysite/ manage.py project/ __init__.py settings.py urls.py wsgi.py apps/ __init__.py polls/

    __init__.py admin.py migrations/ __init__.py models.py tests.py views.py
  6. # setup.py #!/usr/bin/env python import os import sys try: from

    setuptools import setup except ImportError: from distutils.core import setup Import mayan PACKAGE_NAME = 'mayan-edms' PACKAGE_DIR = 'mayan'
  7. try: from settings_local import * except ImportError: Pass if DEVELOPMENT:

    INTERNAL_IPS = ('127.0.0.1',) TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ) try: import rosetta INSTALLED_APPS += ('rosetta',) except ImportError: pass try: import django_extensions INSTALLED_APPS += ('django_extensions',) except ImportError: pass
  8. settings/base.py settings/development.py settings/production.py from .base import * INSTALLED_APPS += (

    'rosetta', 'django_extensions', 'debug_toolbar' ) from .base import * DEBUG = False
  9. Instead of: ./manage.py runserver –settings = mayan.robert We get intuitive:

    ./manage.py runserver –settings = mayan.settings.development ./manage.py runserver –settings = mayan.settings.testing DJANGO_SETTINGS_MODULE = 'mayan.settings.celery_redis' ./manage.py runserver
  10. SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position

    92-93: truncated \xXX escape SyntaxError: EOL while scanning string literal
  11. “I don’t know what the SECRET_KEY is used for. I

    fixed it by commenting the line and restarting nginx.”
  12. “I don’t know what the SECRET_KEY is used for. I

    fixed it by commenting the line and restarting nginx.”
  13. # /django/core/management/commands/startproject.py # Create a random SECRET_KEY to put it

    in the main settings. chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' options['secret_key'] = get_random_string(50, chars)
  14. def _generate_secret_key(self): chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' return get_random_string(50, chars) path =

    os.path.join( settings.BASE_DIR, 'mayan', 'settings', 'local.py' ) with open(path, 'w+') as file_object: file_object.write('\n'.join([ 'from __future__ import absolute_import', '', 'from .base import *', '', "SECRET_KEY='{0}'".format(self._generate_secret_key()), '', ]))
  15. path = os.path.join( settings.BASE_DIR, 'mayan', 'settings', 'local.py' ) ... if

    os.path.exists(path): print 'Existing file at: {0}. Backup, remove this file and try again.'.format(path) exit(1)
  16. #!/usr/bin/env python import os import sys if __name__ == "__main__":

    os.environ.setdefault( "DJANGO_SETTINGS_MODULE", "mayan.settings.local" ) from django.core.management import execute_from_command_line execute_from_command_line(sys.argv)
  17. # mayan/settings/__init__.py try: from .local import * # NOQA except

    ImportError: from .base import * # NOQA from mayan import settings # always works now
  18. ./manage.py initialsetup # tries to imports from local.py and fallsback

    to base.py ./manage.py runserver # imports from local.py which imports from base.py wsgi.py # imports from production.py which imports from local.py which import from base.py
  19. settings/base.py settings/development.py settings/production.py From .base import * SECRET_KEY = “qwerty12345”

    #CELERY_ALWAYS_EAGER = False BROKER_URL = 'redis: //127.0.0.1:6379/0' CELERY_RESULT_BACKEND = 'redis://127.0.0.1: 6379/0' from . import * INSTALLED_APPS += ( 'rosetta', 'django_extensions', 'debug_toolbar' ) from . import * DEBUG = False settings/local.py
  20. class Command(management.BaseCommand): help = 'Gets Mayan EDMS ready to be

    used (initializes database,creates a secret key,etc).' def _generate_secret_key(self): chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' return get_random_string(50, chars) def handle(self, *args, **options): path = os.path.join(settings.BASE_DIR, 'mayan', 'settings','local.py') if os.path.exists(path): print 'Existing file at: {0}. Backup, remove this file and try again.'.format(path) Exit(1) with open(path, 'w+') as file_object: file_object.write('\n'.join([ 'from __future__ import absolute_import', '', 'from .base import *', '', "SECRET_KEY = '{0}'".format(self._generate_secret_key()),'', ])) management.call_command('syncdb', migrate=True, interactive=False)
  21. $ ./manage.py initialsetup • Secret key • Create settings/local.py file

    • Generate database w/out creating admin • Don’t overwrite existing settings file https://github.com/mayan-edms/mayan-edms/blob/master/mayan/apps/main/management/c ommands/initialsetup.py
  22. # models.py from solo.models import SingletonModel class AutoAdminSingleton(SingletonModel): account =

    models.ForeignKey( User, null=True, blank=True, related_name='auto_admin_account' ) password = models.CharField( null=True, blank=True, max_length=128 ) password_hash = models.CharField( null=True, blank=True, max_length=128 ) class Meta: verbose_name = verbose_name_plural = _('Auto admin properties')
  23. @receiver(post_migrate, dispatch_uid='create_superuser_and_anonymous_user') def create_superuser_and_anonymous_user(sender, **kwargs): ... assert auth_models.User.objects.create_superuser( AUTO_ADMIN_USERNAME,'[email protected]', AUTO_ADMIN_PASSWORD

    ) admin = auth_models.User.objects.get(username=AUTO_ADMIN_USERNAME) auto_admin_properties, created = AutoAdminSingleton.objects. get_or_create() auto_admin_properties.account = admin auto_admin_properties.password = AUTO_ADMIN_PASSWORD auto_admin_properties.password_hash = admin.password auto_admin_properties.save()
  24. # login.html {% auto_admin_properties %} {% if auto_admin_properties.account %} <h2>{%

    trans "First time login" %}</h2> <p>{% trans 'You have just finished installing <strong>Mayan EDMS</strong>,congratulations!' %}</p> <p>{% trans 'Login using the following credentials:' %}</p> <p>{% blocktrans with auto_admin_properties.account as account %}Username: <strong>{{ account }}</strong>{% endblocktrans %}</p> <p>{% blocktrans with auto_admin_properties.account.email as email %}Email: <strong>{{ email }}</strong>{% endblocktrans %}</p> <p>{% blocktrans with auto_admin_properties.password as password %}Password: <strong>{{ password }}</strong>{% endblocktrans %}</p> <p>{% trans 'Be sure to change the password to increase security and to disable this message.' %}</p> {% endif %}
  25. @receiver(post_save, dispatch_uid='auto_admin_account_passwd_change', sender=User) def auto_admin_account_passwd_change(sender, instance, **kwargs): auto_admin_properties = AutoAdminSingleton.objects.get()

    if instance == auto_admin_properties.account and instance.password != auto_admin_properties.password_hash: # Only delete the auto admin properties when the password has been changed auto_admin_properties.account = None auto_admin_properties.password = None auto_admin_properties.password_hash = None auto_admin_properties.save()
  26. _file_path = os.path.abspath(os.path.dirname(__file__)).split('/') BASE_DIR = '/'.join(_file_path[0:-2]) MEDIA_ROOT = os.path.join(BASE_DIR, 'mayan',

    'media') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(MEDIA_ROOT, 'db.sqlite3'), } } STATIC_ROOT = os.path.join(BASE_DIR, 'mayan', 'media', 'static') https://github.com/mayan-edms/mayan-edms/blob/master/mayan/settings/base.py
  27. mysite/ manage.py project/ bin/ mayan-edms.py # Copy of manage.py __init__.py

    settings/ __init__.py base.py media/ urls.py wsgi.py apps/ __init__.py
  28. $ virtualenv venv $ source venv/bin/activate $ pip install mayan-edms

    $ mayan-edms.py initialsetup $ mayan-edms.py runserver $ mayan-edms.py collectstatic --noinput
  29. class MissingItem(object): _registry = [] @classmethod def get_all(cls): return cls._registry

    def __init__(self, label, condition, description, view): self.label = label self.condition = condition self.description = description self.view = view self.__class__._registry.append(self)
  30. # /mayan/apps/documents/apps.py MissingItem( label=_('Create a document type'), description=_('Every uploaded document

    must be assigned a document type, it is the basic way Mayan EDMS categorizes documents.'), condition=lambda: not DocumentType.objects.exists(), view='documents:document_type_list' )
  31. Disadvantages • Long paths • Media file hidden depth inside

    virtualenv’s directories Solution Instead of ./manage.py initialsetup, a startsite command, homologous to Django’s startproject command https://github.com/mayan-edms/mayan-edms/issues/171