bazis-uploadable 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ try:
16
+ from importlib.metadata import PackageNotFoundError, version
17
+ __version__ = version('bazis-uploadable')
18
+ except PackageNotFoundError:
19
+ __version__ = 'dev'
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '2.2.0'
32
+ __version_tuple__ = version_tuple = (2, 2, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,25 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from django.contrib import admin
16
+
17
+ from . import models
18
+
19
+
20
+ @admin.register(models.FileUpload)
21
+ class FileUploadAdmin(admin.ModelAdmin):
22
+ list_display = ('name',)
23
+ list_filter = ('extension',)
24
+ search_fields = ('name', 'extension')
25
+
@@ -0,0 +1,22 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from django.utils.translation import gettext_lazy as _
16
+
17
+ from bazis.core.apps import BaseConfig
18
+
19
+
20
+ class UploadableConfig(BaseConfig):
21
+ name = 'bazis.contrib.uploadable'
22
+ verbose_name = _('Uploadable files')
@@ -0,0 +1,26 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from django.utils.translation import gettext_lazy as _
16
+
17
+ from pydantic import Field
18
+
19
+ from bazis.core.utils.schemas import BazisSettings
20
+
21
+
22
+ class Settings(BazisSettings):
23
+ BAZIS_STORAGE_FILE_UPLOAD: str = Field('', title=_('Uploadable files storage engine'))
24
+
25
+
26
+ settings = Settings()
@@ -0,0 +1,39 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: PACKAGE VERSION\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2024-07-03 15:09+0000\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "Language: \n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20
+
21
+ #: apps.py:8 models_abstract.py:45 router.py:8
22
+ msgid "Uploadable files"
23
+ msgstr ""
24
+
25
+ #: conf.py:9
26
+ msgid "Uploadable files storage engine"
27
+ msgstr ""
28
+
29
+ #: models_abstract.py:22 models_abstract.py:44
30
+ msgid "File"
31
+ msgstr ""
32
+
33
+ #: models_abstract.py:23
34
+ msgid "Name"
35
+ msgstr ""
36
+
37
+ #: models_abstract.py:24
38
+ msgid "Extension"
39
+ msgstr ""
@@ -0,0 +1,41 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: PACKAGE VERSION\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2024-07-03 15:09+0000\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "Language: \n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
20
+ "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
21
+ "%100>=11 && n%100<=14)? 2 : 3);\n"
22
+
23
+ #: apps.py:8 models_abstract.py:45 router.py:8
24
+ msgid "Uploadable files"
25
+ msgstr "Загружаемые файлы"
26
+
27
+ #: conf.py:9
28
+ msgid "Uploadable files storage engine"
29
+ msgstr "Движок хранилища загружаемых файлов"
30
+
31
+ #: models_abstract.py:22 models_abstract.py:44
32
+ msgid "File"
33
+ msgstr "Файл"
34
+
35
+ #: models_abstract.py:23
36
+ msgid "Name"
37
+ msgstr "Наименование"
38
+
39
+ #: models_abstract.py:24
40
+ msgid "Extension"
41
+ msgstr "Расширение"
@@ -0,0 +1,47 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Generated by Django 5.0.6 on 2024-07-03 13:40
16
+
17
+ import django.core.files.storage.filesystem
18
+ from django.db import migrations, models
19
+
20
+ import bazis.core.utils.orm
21
+ import model_clone.mixin
22
+
23
+
24
+ class Migration(migrations.Migration):
25
+
26
+ initial = True
27
+
28
+ dependencies = [
29
+ ]
30
+
31
+ operations = [
32
+ migrations.CreateModel(
33
+ name='FileUpload',
34
+ fields=[
35
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36
+ ('file', models.FileField(storage=django.core.files.storage.filesystem.FileSystemStorage, upload_to=bazis.core.utils.orm.get_file_path, verbose_name='File')),
37
+ ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Name')),
38
+ ('extension', models.CharField(blank=True, max_length=50, null=True, verbose_name='Extension')),
39
+ ],
40
+ options={
41
+ 'verbose_name': 'File',
42
+ 'verbose_name_plural': 'Uploadable files',
43
+ 'abstract': False,
44
+ },
45
+ bases=(model_clone.mixin.CloneMixin, models.Model),
46
+ ),
47
+ ]
@@ -0,0 +1,34 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Generated by Django 5.0.1 on 2025-02-12 13:39
16
+
17
+ import bazis.core.utils.orm
18
+ import django.core.files.storage.filesystem
19
+ from django.db import migrations, models
20
+
21
+
22
+ class Migration(migrations.Migration):
23
+
24
+ dependencies = [
25
+ ('uploadable', '0001_initial'),
26
+ ]
27
+
28
+ operations = [
29
+ migrations.AlterField(
30
+ model_name='fileupload',
31
+ name='file',
32
+ field=models.FileField(max_length=255, storage=django.core.files.storage.filesystem.FileSystemStorage, upload_to=bazis.core.utils.orm.get_file_path, verbose_name='File'),
33
+ ),
34
+ ]
@@ -0,0 +1,14 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
@@ -0,0 +1,19 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
16
+
17
+
18
+ class FileUpload(FileUploadAbstract):
19
+ pass
@@ -0,0 +1,63 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+
17
+ from django.conf import settings
18
+ from django.core.files.storage import FileSystemStorage
19
+ from django.db import models
20
+ from django.utils.functional import cached_property
21
+ from django.utils.translation import gettext_lazy as _
22
+
23
+ from bazis.core.models_abstract import JsonApiMixin
24
+ from bazis.core.utils.imp import import_class
25
+ from bazis.core.utils.orm import get_file_path
26
+
27
+
28
+ if settings.BAZIS_STORAGE_FILE_UPLOAD:
29
+ FileStorage = import_class(settings.BAZIS_STORAGE_FILE_UPLOAD)
30
+ else:
31
+ FileStorage = FileSystemStorage
32
+
33
+
34
+ class FileUploadAbstract(JsonApiMixin):
35
+ file = models.FileField(_('File'), upload_to=get_file_path, max_length=255)
36
+ name = models.CharField(_('Name'), max_length=255, blank=True, null=True)
37
+ extension = models.CharField(_('Extension'), max_length=50, blank=True, null=True)
38
+
39
+ def __init__(self, *args, **kwargs):
40
+ super().__init__(*args, **kwargs)
41
+ self._meta.get_field('file').storage = FileStorage()
42
+
43
+ @cached_property
44
+ def size(self) -> int | None:
45
+ try:
46
+ return self.file.size
47
+ except FileNotFoundError:
48
+ return 0
49
+
50
+ def __str__(self):
51
+ return self.file.name
52
+
53
+ def save(self, *args, **kwargs):
54
+ if not self.name:
55
+ self.name = os.path.basename(self.file.name)
56
+ _, self.extension = (os.path.splitext(self.name) + ('',))[:2]
57
+ self.extension = self.extension[1:]
58
+ return super().save(*args, **kwargs)
59
+
60
+ class Meta:
61
+ verbose_name = _('File')
62
+ verbose_name_plural = _('Uploadable files')
63
+ abstract = True
@@ -0,0 +1,23 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from django.utils.translation import gettext_lazy as _
16
+
17
+ from bazis.core.routing import BazisRouter
18
+
19
+ from .routes import FileUploadRouteSet
20
+
21
+
22
+ router = BazisRouter(tags=[_('Uploadable files')])
23
+ router.register(FileUploadRouteSet.as_router())
@@ -0,0 +1,81 @@
1
+ # Copyright 2026 EcoFuture Technology Services LLC and contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from functools import partial
16
+
17
+ from django.apps import apps
18
+
19
+ from fastapi import Form, Request
20
+
21
+ from bazis.core.routes_abstract.initial import http_post
22
+ from bazis.core.routes_abstract.jsonapi import (
23
+ JsonapiRouteBase,
24
+ api_action_init,
25
+ api_action_jsonapi_init,
26
+ api_action_response_init,
27
+ item_data_typing,
28
+ meta_fields_addition,
29
+ )
30
+ from bazis.core.schemas import CrudApiAction, SchemaFields
31
+ from bazis.core.utils.django_types import UploadFileDjango
32
+
33
+
34
+ class FileUploadRouteSet(JsonapiRouteBase):
35
+ model = apps.get_model('uploadable.FileUpload')
36
+
37
+ fields = {
38
+ None: SchemaFields(
39
+ include={
40
+ 'size': None,
41
+ 'extension': None,
42
+ }
43
+ )
44
+ }
45
+
46
+ @http_post(
47
+ '/',
48
+ status_code=201,
49
+ endpoint_callbacks=[
50
+ partial(meta_fields_addition, api_action=CrudApiAction.CREATE),
51
+ partial(api_action_init, api_action=CrudApiAction.CREATE),
52
+ partial(api_action_response_init, api_action=CrudApiAction.RETRIEVE),
53
+ partial(item_data_typing, api_action=CrudApiAction.CREATE),
54
+ api_action_jsonapi_init,
55
+ ],
56
+ )
57
+ def action_create(
58
+ self,
59
+ request: Request,
60
+ file: UploadFileDjango,
61
+ name: str | None = Form(None),
62
+ id: str | None = Form(None),
63
+ **kwargs,
64
+ ):
65
+ if not isinstance(file, UploadFileDjango):
66
+ file.__class__ = UploadFileDjango
67
+
68
+ request._json = {
69
+ 'data': {
70
+ 'id': id,
71
+ 'type': self.model.get_resource_label(),
72
+ 'attributes': {
73
+ 'name': name,
74
+ 'file': file,
75
+ }
76
+ },
77
+ }
78
+
79
+ item_data = self.schema_defaults[CrudApiAction.CREATE].model_validate(request._json)
80
+
81
+ return super().action_create.func(self, request, item_data)
@@ -0,0 +1,682 @@
1
+ Metadata-Version: 2.4
2
+ Name: bazis-uploadable
3
+ Version: 2.2.0
4
+ Summary: File upload management module for Bazis framework.
5
+ Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
+ Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
7
+ Project-URL: Home, https://github.com/ecofuture-tech/bazis-uploadable
8
+ Keywords: bazis,django,fastapi,pydantic,framework,jsonapi,uploadfiles,storage
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Framework :: Django
17
+ Classifier: Framework :: FastAPI
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: bazis
21
+ Requires-Dist: python-multipart
22
+ Provides-Extra: test
23
+ Requires-Dist: bazis-test-utils; extra == "test"
24
+ Provides-Extra: dev
25
+ Requires-Dist: ruff; extra == "dev"
26
+
27
+ # Bazis Uploadable
28
+
29
+ [![PyPI version](https://img.shields.io/pypi/v/bazis-uploadable.svg)](https://pypi.org/project/bazis-uploadable/)
30
+ [![Python Versions](https://img.shields.io/pypi/pyversions/bazis-uploadable.svg)](https://pypi.org/project/bazis-uploadable/)
31
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
32
+
33
+ An extension package for Bazis, providing a ready-to-use solution for file uploading and management with support for various storage backends.
34
+
35
+ ## Quick Start
36
+
37
+ ```bash
38
+ # Install the package
39
+ uv add bazis-uploadable
40
+
41
+ # Create a model for file uploads
42
+ from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
43
+ from bazis.core.models_abstract import DtMixin, UuidMixin
44
+
45
+ class Document(FileUploadAbstract, DtMixin, UuidMixin):
46
+ """Uploadable document"""
47
+
48
+ class Meta:
49
+ verbose_name = 'Document'
50
+ verbose_name_plural = 'Documents'
51
+
52
+ # Create a route for uploads
53
+ from bazis.contrib.uploadable.routes_abstract import FileUploadRouteSet
54
+ from django.apps import apps
55
+
56
+ class DocumentRouteSet(FileUploadRouteSet):
57
+ model = apps.get_model('myapp.Document')
58
+ ```
59
+
60
+ ## Table of Contents
61
+
62
+ - [Description](#description)
63
+ - [Requirements](#requirements)
64
+ - [Installation](#installation)
65
+ - [Core Components](#core-components)
66
+ - [FileUploadAbstract](#fileuploadabstract)
67
+ - [FileUploadRouteSet](#fileuploadrouteset)
68
+ - [Storage Configuration](#storage-configuration)
69
+ - [Usage](#usage)
70
+ - [Creating a File Model](#creating-a-file-model)
71
+ - [Creating an Upload Route](#creating-an-upload-route)
72
+ - [Uploading Files via API](#uploading-files-via-api)
73
+ - [Storage Configuration](#storage-configuration)
74
+ - [Examples](#examples)
75
+ - [License](#license)
76
+ - [Links](#links)
77
+
78
+ ## Description
79
+
80
+ **Bazis Uploadable** is an extension package for the Bazis framework that provides a ready-to-use solution for file uploading and management. The package includes:
81
+
82
+ - **FileUploadAbstract** — abstract model with fields for storing files and metadata
83
+ - **FileUploadRouteSet** — ready-to-use route with multipart/form-data upload support
84
+ - **Flexible storage configuration** — support for local storage, S3, and other backends
85
+ - **Automatic metadata extraction** — filename, extension, size
86
+ - **JSON:API integration** — standardized response format
87
+
88
+ **This package requires the base `bazis` package to be installed.**
89
+
90
+ ## Requirements
91
+
92
+ - **Python**: 3.12+
93
+ - **bazis**: latest version
94
+ - **PostgreSQL**: 12+
95
+ - **Redis**: For caching
96
+
97
+ ## Installation
98
+
99
+ ### Using uv (recommended)
100
+
101
+ ```bash
102
+ uv add bazis-uploadable
103
+ ```
104
+
105
+ ### Using pip
106
+
107
+ ```bash
108
+ pip install bazis-uploadable
109
+ ```
110
+
111
+ ## Core Components
112
+
113
+ ### FileUploadAbstract
114
+
115
+ Abstract model for storing uploaded files.
116
+
117
+ **Location**: `bazis.contrib.uploadable.models_abstract.FileUploadAbstract`
118
+
119
+ **Fields**:
120
+
121
+ - `file` — FileField for storing the file
122
+ - `name` — filename (optional, automatically filled from file name)
123
+ - `extension` — file extension (optional, automatically extracted)
124
+
125
+ **Properties**:
126
+
127
+ - `size` — file size in bytes (cached_property)
128
+
129
+ **Features**:
130
+
131
+ - Automatically extracts filename and extension on save
132
+ - Uses `get_file_path` to generate storage path
133
+ - Supports custom storage backends via settings
134
+ - Returns 0 for `size` when file is absent instead of raising an error
135
+
136
+ **Usage example**:
137
+
138
+ ```python
139
+ from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
140
+ from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
141
+
142
+ class Document(FileUploadAbstract, DtMixin, UuidMixin):
143
+ """Document with additional fields"""
144
+ description = models.TextField('Description', blank=True, null=True)
145
+
146
+ class Meta:
147
+ verbose_name = 'Document'
148
+ verbose_name_plural = 'Documents'
149
+ ```
150
+
151
+ ### FileUploadRouteSet
152
+
153
+ Ready-to-use route for file uploading via multipart/form-data.
154
+
155
+ **Location**: `bazis.contrib.uploadable.routes_abstract.FileUploadRouteSet`
156
+
157
+ **Features**:
158
+
159
+ - Accepts files via `multipart/form-data`
160
+ - Automatically includes `size` and `extension` fields in schema
161
+ - Supports optional `name` field for renaming files
162
+ - Returns data in JSON:API format
163
+
164
+ **Endpoint**:
165
+
166
+ - **POST /** — upload a new file
167
+
168
+ **Usage example**:
169
+
170
+ ```python
171
+ from bazis.contrib.uploadable.routes_abstract import FileUploadRouteSet
172
+ from django.apps import apps
173
+
174
+ class DocumentRouteSet(FileUploadRouteSet):
175
+ model = apps.get_model('myapp.Document')
176
+ ```
177
+
178
+ ### Storage Configuration
179
+
180
+ By default, Django's standard `FileSystemStorage` is used. You can configure custom storage via settings.
181
+
182
+ **settings.py**:
183
+
184
+ ```python
185
+ # Local storage (default)
186
+ BAZIS_STORAGE_FILE_UPLOAD = None # or 'django.core.files.storage.FileSystemStorage'
187
+
188
+ # S3-compatible storage
189
+ BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.s3boto3.S3Boto3Storage'
190
+
191
+ # Other storage backends
192
+ BAZIS_STORAGE_FILE_UPLOAD = 'path.to.your.CustomStorage'
193
+ ```
194
+
195
+ ## Usage
196
+
197
+ ### Creating a File Model
198
+
199
+ ```python
200
+ from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
201
+ from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
202
+ from django.db import models
203
+
204
+ class Document(FileUploadAbstract, DtMixin, UuidMixin):
205
+ """Document with metadata"""
206
+ description = models.TextField('Description', blank=True, null=True)
207
+ category = models.CharField('Category', max_length=100, blank=True, null=True)
208
+
209
+ class Meta:
210
+ verbose_name = 'Document'
211
+ verbose_name_plural = 'Documents'
212
+
213
+ def __str__(self):
214
+ return self.name or self.file.name
215
+ ```
216
+
217
+ ### Creating an Upload Route
218
+
219
+ **routes.py**:
220
+
221
+ ```python
222
+ from bazis.contrib.uploadable.routes_abstract import FileUploadRouteSet
223
+ from bazis.core.schemas.fields import SchemaField, SchemaFields
224
+ from django.apps import apps
225
+
226
+ class DocumentRouteSet(FileUploadRouteSet):
227
+ model = apps.get_model('myapp.Document')
228
+
229
+ # Adding additional fields to schema
230
+ fields = {
231
+ None: SchemaFields(
232
+ include={
233
+ 'description': None,
234
+ 'category': None,
235
+ },
236
+ ),
237
+ }
238
+ ```
239
+
240
+ **router.py**:
241
+
242
+ ```python
243
+ from bazis.core.routing import BazisRouter
244
+ from . import routes
245
+
246
+ router = BazisRouter(tags=['Documents'])
247
+ router.register(routes.DocumentRouteSet.as_router())
248
+ ```
249
+
250
+ ### Uploading Files via API
251
+
252
+ #### Uploading a file with automatic name
253
+
254
+ ```bash
255
+ POST /api/v1/documents/document/
256
+ Content-Type: multipart/form-data
257
+
258
+ --boundary
259
+ Content-Disposition: form-data; name="file"; filename="report.pdf"
260
+ Content-Type: application/pdf
261
+
262
+ [binary data]
263
+ --boundary--
264
+ ```
265
+
266
+ **Response**:
267
+ ```json
268
+ {
269
+ "data": {
270
+ "type": "myapp.document",
271
+ "id": "123e4567-e89b-12d3-a456-426614174000",
272
+ "attributes": {
273
+ "name": "report.pdf",
274
+ "extension": "pdf",
275
+ "size": 102400,
276
+ "dt_created": "2024-01-15T10:30:00Z"
277
+ },
278
+ "relationships": {
279
+ "file": {
280
+ "data": {
281
+ "url": "/media/uploads/2024/01/15/report.pdf"
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }
287
+ ```
288
+
289
+ #### Uploading a file with custom name
290
+
291
+ ```bash
292
+ POST /api/v1/documents/document/
293
+ Content-Type: multipart/form-data
294
+
295
+ --boundary
296
+ Content-Disposition: form-data; name="file"; filename="original.pdf"
297
+ Content-Type: application/pdf
298
+
299
+ [binary data]
300
+ --boundary
301
+ Content-Disposition: form-data; name="name"
302
+
303
+ Monthly Report January 2024.pdf
304
+ --boundary--
305
+ ```
306
+
307
+ **Response**:
308
+ ```json
309
+ {
310
+ "data": {
311
+ "type": "myapp.document",
312
+ "id": "123e4567-e89b-12d3-a456-426614174000",
313
+ "attributes": {
314
+ "name": "Monthly Report January 2024.pdf",
315
+ "extension": "pdf",
316
+ "size": 102400
317
+ }
318
+ }
319
+ }
320
+ ```
321
+
322
+ #### Uploading with additional fields
323
+
324
+ ```bash
325
+ POST /api/v1/documents/document/
326
+ Content-Type: multipart/form-data
327
+
328
+ --boundary
329
+ Content-Disposition: form-data; name="file"; filename="invoice.pdf"
330
+
331
+ [binary data]
332
+ --boundary
333
+ Content-Disposition: form-data; name="name"
334
+
335
+ Invoice #12345
336
+ --boundary
337
+ Content-Disposition: form-data; name="description"
338
+
339
+ Invoice for January 2024 services
340
+ --boundary
341
+ Content-Disposition: form-data; name="category"
342
+
343
+ Finance
344
+ --boundary--
345
+ ```
346
+
347
+ ## Storage Configuration
348
+
349
+ ### Local Storage (default)
350
+
351
+ ```python
352
+ # settings.py
353
+ MEDIA_ROOT = '/var/www/media/'
354
+ MEDIA_URL = '/media/'
355
+
356
+ # FileSystemStorage is used by default
357
+ BAZIS_STORAGE_FILE_UPLOAD = None
358
+ ```
359
+
360
+ ### Amazon S3
361
+
362
+ ```bash
363
+ # Install dependencies
364
+ uv add django-storages boto3
365
+ ```
366
+
367
+ ```python
368
+ # settings.py
369
+ BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.s3boto3.S3Boto3Storage'
370
+
371
+ # S3 configuration
372
+ AWS_ACCESS_KEY_ID = 'your-access-key'
373
+ AWS_SECRET_ACCESS_KEY = 'your-secret-key'
374
+ AWS_STORAGE_BUCKET_NAME = 'your-bucket-name'
375
+ AWS_S3_REGION_NAME = 'us-east-1'
376
+ AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
377
+ AWS_DEFAULT_ACL = 'public-read'
378
+ ```
379
+
380
+ ### MinIO (S3-compatible storage)
381
+
382
+ ```python
383
+ # settings.py
384
+ BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.s3boto3.S3Boto3Storage'
385
+
386
+ AWS_ACCESS_KEY_ID = 'minio-access-key'
387
+ AWS_SECRET_ACCESS_KEY = 'minio-secret-key'
388
+ AWS_STORAGE_BUCKET_NAME = 'uploads'
389
+ AWS_S3_ENDPOINT_URL = 'http://minio:9000'
390
+ AWS_S3_USE_SSL = False
391
+ AWS_DEFAULT_ACL = 'public-read'
392
+ ```
393
+
394
+ ### Google Cloud Storage
395
+
396
+ ```bash
397
+ # Install dependencies
398
+ uv add django-storages google-cloud-storage
399
+ ```
400
+
401
+ ```python
402
+ # settings.py
403
+ BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.gcloud.GoogleCloudStorage'
404
+
405
+ GS_BUCKET_NAME = 'your-bucket-name'
406
+ GS_PROJECT_ID = 'your-project-id'
407
+ GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
408
+ 'path/to/service-account.json'
409
+ )
410
+ ```
411
+
412
+ ### Custom Storage
413
+
414
+ You can create your own storage by inheriting from Django Storage:
415
+
416
+ ```python
417
+ # myapp/storage.py
418
+ from django.core.files.storage import FileSystemStorage
419
+
420
+ class CustomStorage(FileSystemStorage):
421
+ def __init__(self):
422
+ super().__init__(
423
+ location='/custom/path/',
424
+ base_url='/custom/'
425
+ )
426
+
427
+ def _save(self, name, content):
428
+ # Custom save logic
429
+ return super()._save(name, content)
430
+ ```
431
+
432
+ ```python
433
+ # settings.py
434
+ BAZIS_STORAGE_FILE_UPLOAD = 'myapp.storage.CustomStorage'
435
+ ```
436
+
437
+ ## Examples
438
+
439
+ ### Complete document application example
440
+
441
+ **models.py**:
442
+
443
+ ```python
444
+ from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
445
+ from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
446
+ from django.db import models
447
+
448
+ class DocumentCategory(models.TextChoices):
449
+ INVOICE = 'invoice', 'Invoice'
450
+ CONTRACT = 'contract', 'Contract'
451
+ REPORT = 'report', 'Report'
452
+ OTHER = 'other', 'Other'
453
+
454
+ class Document(FileUploadAbstract, DtMixin, UuidMixin):
455
+ """Document with category and description"""
456
+ description = models.TextField('Description', blank=True, null=True)
457
+ category = models.CharField(
458
+ 'Category',
459
+ max_length=20,
460
+ choices=DocumentCategory.choices,
461
+ default=DocumentCategory.OTHER
462
+ )
463
+
464
+ class Meta:
465
+ verbose_name = 'Document'
466
+ verbose_name_plural = 'Documents'
467
+
468
+ def __str__(self):
469
+ return f'{self.get_category_display()}: {self.name}'
470
+ ```
471
+
472
+ **routes.py**:
473
+
474
+ ```python
475
+ from bazis.contrib.uploadable.routes_abstract import FileUploadRouteSet
476
+ from bazis.core.schemas.fields import SchemaFields
477
+ from django.apps import apps
478
+
479
+ class DocumentRouteSet(FileUploadRouteSet):
480
+ model = apps.get_model('myapp.Document')
481
+
482
+ fields = {
483
+ None: SchemaFields(
484
+ include={
485
+ 'description': None,
486
+ 'category': None,
487
+ },
488
+ ),
489
+ }
490
+ ```
491
+
492
+ **router.py**:
493
+
494
+ ```python
495
+ from bazis.core.routing import BazisRouter
496
+ from . import routes
497
+
498
+ router = BazisRouter(tags=['Documents'])
499
+ router.register(routes.DocumentRouteSet.as_router())
500
+ ```
501
+
502
+ **admin.py**:
503
+
504
+ ```python
505
+ from django.contrib import admin
506
+ from bazis.core.admin_abstract import DtAdminMixin
507
+ from .models import Document
508
+
509
+ @admin.register(Document)
510
+ class DocumentAdmin(DtAdminMixin, admin.ModelAdmin):
511
+ list_display = ('id', 'name', 'category', 'extension', 'size_display', 'dt_created')
512
+ list_filter = ('category', 'extension')
513
+ search_fields = ('name', 'description')
514
+ readonly_fields = ('extension', 'size', 'dt_created', 'dt_updated')
515
+
516
+ def size_display(self, obj):
517
+ """Human-readable file size"""
518
+ size = obj.size
519
+ if size < 1024:
520
+ return f'{size} B'
521
+ elif size < 1024 * 1024:
522
+ return f'{size / 1024:.2f} KB'
523
+ else:
524
+ return f'{size / (1024 * 1024):.2f} MB'
525
+ size_display.short_description = 'Size'
526
+ ```
527
+
528
+ ### Client application usage example
529
+
530
+ **JavaScript (file upload)**:
531
+
532
+ ```javascript
533
+ async function uploadDocument(file, category, description) {
534
+ const formData = new FormData();
535
+ formData.append('file', file);
536
+ formData.append('category', category);
537
+
538
+ if (description) {
539
+ formData.append('description', description);
540
+ }
541
+
542
+ const response = await fetch('/api/v1/documents/document/', {
543
+ method: 'POST',
544
+ headers: {
545
+ 'Authorization': `Bearer ${token}`
546
+ },
547
+ body: formData
548
+ });
549
+
550
+ if (!response.ok) {
551
+ throw new Error('Upload failed');
552
+ }
553
+
554
+ return await response.json();
555
+ }
556
+
557
+ // Usage
558
+ const fileInput = document.getElementById('fileInput');
559
+ const file = fileInput.files[0];
560
+
561
+ try {
562
+ const result = await uploadDocument(file, 'invoice', 'Monthly invoice');
563
+ console.log('Uploaded:', result.data.id);
564
+ console.log('File URL:', result.data.attributes.file);
565
+ } catch (error) {
566
+ console.error('Error:', error);
567
+ }
568
+ ```
569
+
570
+ **Python (file upload via requests)**:
571
+
572
+ ```python
573
+ import requests
574
+
575
+ def upload_document(file_path, category, description=None):
576
+ url = 'http://api.example.com/api/v1/documents/document/'
577
+
578
+ with open(file_path, 'rb') as file:
579
+ files = {'file': file}
580
+ data = {
581
+ 'category': category,
582
+ }
583
+
584
+ if description:
585
+ data['description'] = description
586
+
587
+ headers = {
588
+ 'Authorization': f'Bearer {token}'
589
+ }
590
+
591
+ response = requests.post(url, files=files, data=data, headers=headers)
592
+ response.raise_for_status()
593
+
594
+ return response.json()
595
+
596
+ # Usage
597
+ result = upload_document(
598
+ '/path/to/invoice.pdf',
599
+ category='invoice',
600
+ description='Monthly invoice for January'
601
+ )
602
+
603
+ print(f"Uploaded: {result['data']['id']}")
604
+ print(f"Size: {result['data']['attributes']['size']} bytes")
605
+ ```
606
+
607
+ ### Getting list of files
608
+
609
+ ```bash
610
+ GET /api/v1/documents/document/
611
+ Authorization: Bearer <token>
612
+ ```
613
+
614
+ **Response**:
615
+ ```json
616
+ {
617
+ "data": [
618
+ {
619
+ "type": "myapp.document",
620
+ "id": "123e4567-e89b-12d3-a456-426614174000",
621
+ "attributes": {
622
+ "name": "invoice.pdf",
623
+ "extension": "pdf",
624
+ "size": 102400,
625
+ "category": "invoice",
626
+ "description": "Monthly invoice",
627
+ "dt_created": "2024-01-15T10:30:00Z"
628
+ }
629
+ },
630
+ {
631
+ "type": "myapp.document",
632
+ "id": "987e6543-e21b-32d1-b654-426614174001",
633
+ "attributes": {
634
+ "name": "report.docx",
635
+ "extension": "docx",
636
+ "size": 256000,
637
+ "category": "report",
638
+ "dt_created": "2024-01-16T14:20:00Z"
639
+ }
640
+ }
641
+ ],
642
+ "meta": {
643
+ "count": 2
644
+ }
645
+ }
646
+ ```
647
+
648
+ ### Filtering by extension
649
+
650
+ ```bash
651
+ GET /api/v1/documents/document/?filter[extension]=pdf
652
+ ```
653
+
654
+ ### Filtering by category
655
+
656
+ ```bash
657
+ GET /api/v1/documents/document/?filter[category]=invoice
658
+ ```
659
+
660
+ ## License
661
+
662
+ Apache License 2.0
663
+
664
+ See [LICENSE](LICENSE) file for details.
665
+
666
+ ## Links
667
+
668
+ - [Bazis Documentation](https://github.com/ecofuture-tech/bazis) — main repository
669
+ - [Bazis Uploadable Repository](https://github.com/ecofuture-tech/bazis-uploadable) — package repository
670
+ - [Issue Tracker](https://github.com/ecofuture-tech/bazis-uploadable/issues) — report bugs or request features
671
+ - [Django Storages](https://django-storages.readthedocs.io/) — storage backend documentation
672
+
673
+ ## Support
674
+
675
+ If you have questions or issues:
676
+ - Check the [Bazis documentation](https://github.com/ecofuture-tech/bazis)
677
+ - Search through [existing issues](https://github.com/ecofuture-tech/bazis-uploadable/issues)
678
+ - Create a [new issue](https://github.com/ecofuture-tech/bazis-uploadable/issues/new) with detailed information
679
+
680
+ ---
681
+
682
+ Made with ❤️ by Bazis team
@@ -0,0 +1,20 @@
1
+ bazis/contrib/uploadable/__init__.py,sha256=yrZO1q_ut-i1aU1GYeg4bypwe_xjNccaqDrQmrUAniw,784
2
+ bazis/contrib/uploadable/_version.py,sha256=6OGz4a0gjMGlckPyPCNiJDWyFDO-tWO8O_ZNx4ajT2Y,704
3
+ bazis/contrib/uploadable/admin.py,sha256=218mMcrIER0bG58waDCoh5G2ctElTcVmioAHAGd5z0I,853
4
+ bazis/contrib/uploadable/apps.py,sha256=M8rxoRma5YzI6HLIaht9IEYpiseLrT_r2J8qckhkstw,827
5
+ bazis/contrib/uploadable/conf.py,sha256=y0AsAfMbJbz-oP0ld1HinXbprwCgEiLUB63I9KYWcNs,898
6
+ bazis/contrib/uploadable/models.py,sha256=IW7oUTUlJqEsU6fz3iaKdTlTeiFMwpGJy-lLs8Bt3Qw,736
7
+ bazis/contrib/uploadable/models_abstract.py,sha256=7HG5LGIKv2KRG9wDwwk-mm2qr8uOZAwksINuev_HA_Q,2197
8
+ bazis/contrib/uploadable/router.py,sha256=SZW_3eLKP_KlvSsyyAQtomLtevYTFOr5q2ENacXoK30,855
9
+ bazis/contrib/uploadable/routes.py,sha256=P9Zb87wEsln071jxDwSB1vEkiPUgievFJD2Z-NXgoxc,2516
10
+ bazis/contrib/uploadable/locale/en/LC_MESSAGES/django.mo,sha256=N1pb17IfLd0ASiKO8d68-B4ygSpDkhKOCs8YTzMXQo0,380
11
+ bazis/contrib/uploadable/locale/en/LC_MESSAGES/django.po,sha256=gYhnI4Ci3W-7Ioo7uSF6fGiql8C6mpZS-__tbovoYOI,948
12
+ bazis/contrib/uploadable/locale/ru/LC_MESSAGES/django.mo,sha256=1oAU-_1G-Rez_Wgt21g_pkxM1dVxDC-pfr3ZeMCEXDc,856
13
+ bazis/contrib/uploadable/locale/ru/LC_MESSAGES/django.po,sha256=5Gu4PyiP5fiHjIlGLHjoO2LS2MxEOgd1RbbZncxS6iw,1244
14
+ bazis/contrib/uploadable/migrations/0001_initial.py,sha256=ZuA4ZRT9wJtWja0cly_if4D8bVfrdbZwaIKkCFuf85U,1773
15
+ bazis/contrib/uploadable/migrations/0002_alter_fileupload_file.py,sha256=Me826ZVKboEOze051_b9N29PfTCjIslX7GSnkZH8Le0,1199
16
+ bazis/contrib/uploadable/migrations/__init__.py,sha256=7MU6gF_TkvnR3NFi-Nfci4RIacoZ6bqMjwlR6ONK8Ms,615
17
+ bazis_uploadable-2.2.0.dist-info/METADATA,sha256=Ah6I62SlUQgSWfiAEHbdRF1W1h5X0ExcuWs3BO5SxR4,17018
18
+ bazis_uploadable-2.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
19
+ bazis_uploadable-2.2.0.dist-info/top_level.txt,sha256=WgdrPZTZBMG8i_EqxA3vU5qI4ETQ_RsqKqSqsfIApHY,6
20
+ bazis_uploadable-2.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ bazis