Skip to content

Commit e1098c3

Browse files
authored
Publish initial package code (#1)
1 parent cea7278 commit e1098c3

19 files changed

+630
-2
lines changed

.bandit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bandit]
2+
exclude: tests

.checks.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- bandit
2+
- flake8
3+
- pydocstyle

.travis.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
language: python
2+
sudo: required
3+
dist: xenial
4+
python: 3.7
5+
cache: pip
6+
env:
7+
matrix:
8+
- TOXENV=django-slugify
9+
- TOXENV=unicode-slugify
10+
install: pip install -U pip tox codecov
11+
script: tox -e $TOXENV
12+
after_success: codecov
13+
deploy:
14+
provider: pypi
15+
user: codingjoe
16+
password:
17+
secure: dfstvb+TXu32T3rAaHITddoMmYYfNWGVVyfyNyjq8vHigawJKieUNHmy9UKqP5Wb8gHaw8O/zpqodYVAt+8JvYfxALEGJBOSOE3E8KPqNZBgQbwutC7vvna+Sz+f/H4OGLWY+YWm54rujB2J/eSqlG4NV/6fePebMy0wq+mGqYWYfrH4KMTwG3oYZfmEDZcMD55lFSKxryHzlq3C5eibtq+iLTolKE+r1QlazMw07TADsboFDigAHmDPRuZJvhfzfkY+3V3rkoiaHXtpN8m386nokqpMSfeEO5BXjod80yRvvtNWGH1x2N19mWf3YZAhBovGqkZR0ezVc9ho60z9+aTUGLcVhbRWqsUob8nQ0KqsepeldLAaopDb/TgCEJmOMjBCD0NH+Q/d1UVtFi/MUguxKyC+Kbi1/3qx/8ArELw74rTJURDHIRqIUB0wX3pzIu24U2pV0uII1DECn5dfVE2RZwbqnt/+JVIUA8pWoAd8VWilCGHm0NfHhMfplPwbSYhbWyCgCnwzAfScvZQOtzquEX+kbXB7BK89kFIICOueh1IASxn2WTxEj+8QNNhe1PnznGmA+bOKtEmEkhE65n606Q9WSwKGiyLLDgrqC3tGxeQmEHYaIds3Ali3ayRbMlSwlSH90NrXmh1zoK8TLHGSO01fAm4SBfHnQDEJSwM=
18+
on:
19+
tags: true
20+
distributions: sdist bdist_wheel
21+
repo: codingjoe/django-dynamic-filenames
22+
branch: master

README.md

Lines changed: 0 additions & 2 deletions
This file was deleted.

README.rst

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
========================
2+
Django Dynamic Filenames
3+
========================
4+
5+
Write advanced filename patterns using the `Format Specification Mini-Language`__.
6+
7+
__ https://docs.python.org/3/library/string.html#format-string-syntax
8+
9+
Getting Started
10+
---------------
11+
12+
Installation
13+
~~~~~~~~~~~~
14+
15+
.. code-block:: bash
16+
17+
pip install django-dynamic-filenames
18+
19+
Samples
20+
~~~~~~~
21+
22+
Basic example:
23+
24+
.. code-block:: python
25+
26+
from django.db import models
27+
from dynamic_names import FilePattern
28+
29+
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid_base32}{ext}')
30+
31+
class FileModel(models.Model):
32+
my_file = models.FileField(upload_to=upload_to_pattern)
33+
34+
35+
Auto slug example:
36+
37+
.. code-block:: python
38+
39+
from django.db import models
40+
from dynamic_names import FilePattern
41+
42+
class SlugPattern(FilePattern):
43+
filename_pattern = '{app_name:.25}/{model_name:.30}/{slug}{ext}'
44+
45+
class FileModel(models.Model):
46+
title = models.CharField(max_length=100)
47+
my_file = models.FileField(upload_to=SlugPattern(populate_slug_from='title'))
48+
49+
Supported Attributes
50+
--------------------
51+
52+
``ext``
53+
File extension including the dot.
54+
55+
``name``
56+
Filename excluding the folders.
57+
58+
``model_name``
59+
Name of the Django model.
60+
61+
``app_label``
62+
App label of the Django model.
63+
64+
``uuid_base16``
65+
Base16 (hex) representation of a UUID.
66+
67+
``uuid_base32``
68+
Base32 representation of a UUID.
69+
70+
``uuid_base64``
71+
Base64 representation of a UUID. Not supported by all file systems.
72+
73+
``slug``
74+
Auto created slug based on another field on the model instance.
75+
76+
``slug_from``
77+
Name of the field the slug should be populated from.
78+
79+
.. note:: The field name itself is not part of the pattern.

dynamic_filenames.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import base64
2+
import os
3+
import uuid
4+
5+
try: # use unicode-slugify library if installed
6+
from slugify import slugify
7+
8+
SLUGIFY_KWARGS = dict(only_ascii=True)
9+
except ImportError:
10+
from django.utils.text import slugify
11+
12+
SLUGIFY_KWARGS = dict(allow_unicode=False)
13+
14+
15+
class FilePattern:
16+
"""
17+
Write advanced filename patterns using the Format Specification Mini-Language.
18+
19+
Basic example:
20+
21+
.. code-block:: python
22+
23+
from django.db import models
24+
from dynamic_names import FilePattern
25+
26+
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid_base32}{ext}')
27+
28+
class FileModel(models.Model):
29+
my_file = models.FileField(upload_to=upload_to_pattern)
30+
31+
Args:
32+
33+
ext: File extension including the dot.
34+
name: Filename excluding the folders.
35+
model_name: Name of the Django model.
36+
app_label: App label of the Django model.
37+
uuid_base16: Base16 (hex) representation of a UUID.
38+
uuid_base32: Base32 representation of a UUID.
39+
uuid_base64: Base64 representation of a UUID. Not supported by all file systems.
40+
slug: Auto created slug based on another field on the model instance.
41+
slug_from: Name of the field the slug should be populated from.
42+
43+
44+
Auto slug example:
45+
46+
.. code-block:: python
47+
48+
from django.db import models
49+
from dynamic_names import FilePattern
50+
51+
class SlugPattern(FilePattern):
52+
filename_pattern = '{app_name:.25}/{model_name:.30}/{slug}{ext}'
53+
54+
class FileModel(models.Model):
55+
title = models.CharField(max_length=100)
56+
my_file = models.FileField(upload_to=SlugPattern(slug_from='title'))
57+
58+
"""
59+
60+
slug_from = None
61+
62+
filename_pattern = '{name}{ext}'
63+
64+
def __call__(self, instance, filename):
65+
"""Return filename based for given instance and filename."""
66+
# UUID needs to be set on call, not per instance to avoid state leakage.
67+
guid = self.get_uuid()
68+
path, ext = os.path.splitext(filename)
69+
path, name = os.path.split(path)
70+
defaults = {
71+
'ext': ext,
72+
'name': name,
73+
'model_name': instance._meta.model_name,
74+
'app_label': instance._meta.app_label,
75+
'uuid_base10': self.uuid_2_base10(guid),
76+
'uuid_base16': self.uuid_2_base16(guid),
77+
'uuid_base32': self.uuid_2_base32(guid),
78+
'uuid_base64': self.uuid_2_base64(guid),
79+
}
80+
defaults.update(self.override_values)
81+
if self.slug_from is not None:
82+
field_value = getattr(instance, self.slug_from)
83+
defaults['slug'] = slugify(field_value, **SLUGIFY_KWARGS)
84+
return self.filename_pattern.format(**defaults)
85+
86+
def __init__(self, **kwargs):
87+
self.kwargs = kwargs
88+
override_values = kwargs.copy()
89+
self.slug_from = override_values.pop('slug_from', self.slug_from)
90+
self.filename_pattern = override_values.pop('filename_pattern', self.filename_pattern)
91+
self.override_values = override_values
92+
93+
def deconstruct(self):
94+
"""Destruct callable to support Django migrations."""
95+
path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
96+
return path, [], self.kwargs
97+
98+
@staticmethod
99+
def get_uuid():
100+
"""Return UUID version 4."""
101+
return uuid.uuid4()
102+
103+
@staticmethod
104+
def uuid_2_base10(uuid):
105+
"""Return 39 digits long integer UUID as Base10."""
106+
return uuid.int
107+
108+
@staticmethod
109+
def uuid_2_base16(uuid):
110+
"""Return 32 char long UUID as Base16 (hex)."""
111+
return uuid.hex
112+
113+
@staticmethod
114+
def uuid_2_base32(uuid):
115+
"""Return 27 char long UUIDv4 as Base32."""
116+
return base64.b32encode(
117+
uuid.bytes
118+
).decode('utf-8').rstrip('=\n')
119+
120+
@staticmethod
121+
def uuid_2_base64(uuid):
122+
"""
123+
Return 23 char long UUIDv4 as Base64.
124+
125+
.. warning:: Not all file systems support Base64 file names.
126+
e.g. The Apple File System (APFS) is case insensitive by default.
127+
128+
"""
129+
return base64.urlsafe_b64encode(
130+
uuid.bytes
131+
).decode('utf-8').rstrip('=\n')

requirements-dev.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-e .
2+
coverage
3+
pytest
4+
pytest-django

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Django>=1.11

setup.cfg

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[metadata]
2+
name = django-dynamic-filenames
3+
author = Johannes Hoppe
4+
author-email = info@johanneshoppe.com
5+
summary = Write advanced filename patterns using the Format Specification Mini-Language.
6+
description-file = README.rst
7+
home-page = https://github.com/codingjoe/django-dynamic-filenames
8+
license = MIT License
9+
classifier =
10+
Development Status :: 5 - Production/Stable
11+
Environment :: Web Environment
12+
Intended Audience :: Developers
13+
License :: OSI Approved :: MIT License±
14+
Operating System :: OS Independent
15+
Programming Language :: Python
16+
Programming Language :: Python :: 3
17+
Framework :: Django
18+
19+
[pbr]
20+
skip_authors = true
21+
skip_changelog = true
22+
23+
[tool:pytest]
24+
norecursedirs = env docs .eggs
25+
addopts = --tb=short -rxs
26+
DJANGO_SETTINGS_MODULE=tests.testapp.settings
27+
28+
[flake8]
29+
max-line-length = 99
30+
max-complexity = 10
31+
statistics = true
32+
show-source = true
33+
34+
[pydocstyle]
35+
add-ignore = D1
36+
match-dir = (?!tests|env|docs|\.).*
37+
38+
[isort]
39+
atomic = true
40+
multi_line_output = 5
41+
line_length = 79
42+
skip = manage.py,docs,.tox,env
43+
known_first_party = dynamic_filenames, tests
44+
known_third_party = django, slugify
45+
combine_as_imports = true

setup.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env python
2+
3+
from setuptools import setup
4+
5+
setup(
6+
setup_requires=['pbr'],
7+
py_modules=['dynamic_filenames'],
8+
pbr=True,
9+
)

0 commit comments

Comments
 (0)