bazis-author 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.
- bazis/contrib/author/__init__.py +19 -0
- bazis/contrib/author/_version.py +34 -0
- bazis/contrib/author/admin_abstract.py +53 -0
- bazis/contrib/author/apps.py +22 -0
- bazis/contrib/author/locale/en/LC_MESSAGES/django.mo +0 -0
- bazis/contrib/author/locale/en/LC_MESSAGES/django.po +22 -0
- bazis/contrib/author/locale/ru/LC_MESSAGES/django.mo +0 -0
- bazis/contrib/author/locale/ru/LC_MESSAGES/django.po +24 -0
- bazis/contrib/author/models_abstract.py +49 -0
- bazis/contrib/author/routes_abstract.py +50 -0
- bazis_author-2.2.0.dist-info/METADATA +589 -0
- bazis_author-2.2.0.dist-info/RECORD +14 -0
- bazis_author-2.2.0.dist-info/WHEEL +5 -0
- bazis_author-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -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-author')
|
|
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,53 @@
|
|
|
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 admin_auto_filters.filters import AutocompleteFilterFactory
|
|
18
|
+
|
|
19
|
+
from bazis.core.utils.sets_order import OrderedSet
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthorAdminMixin:
|
|
23
|
+
def get_search_fields(self, request):
|
|
24
|
+
return tuple(OrderedSet(super().get_search_fields(request) + ('author__username',)))
|
|
25
|
+
|
|
26
|
+
def get_readonly_fields(self, request, obj=None):
|
|
27
|
+
return tuple(
|
|
28
|
+
OrderedSet(super().get_readonly_fields(request, obj) + ('author', 'author_updated'))
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def get_list_filter(self, request):
|
|
32
|
+
return (AutocompleteFilterFactory(_('Author'), 'author'),) + super().get_list_filter(
|
|
33
|
+
request
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def save_model(self, request, obj, form, change):
|
|
37
|
+
if not obj.author:
|
|
38
|
+
obj.author = request.user
|
|
39
|
+
else:
|
|
40
|
+
obj.author_updated = request.user
|
|
41
|
+
return super().save_model(request, obj, form, change)
|
|
42
|
+
|
|
43
|
+
def save_formset(self, request, form, formset, change):
|
|
44
|
+
instances = formset.save(commit=False)
|
|
45
|
+
for obj in formset.deleted_objects:
|
|
46
|
+
obj.delete()
|
|
47
|
+
for obj in instances:
|
|
48
|
+
if not obj.author:
|
|
49
|
+
obj.author = request.user
|
|
50
|
+
else:
|
|
51
|
+
obj.author_updated = request.user
|
|
52
|
+
obj.save()
|
|
53
|
+
formset.save_m2m()
|
|
@@ -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.utils.apps import BaseConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthingConfig(BaseConfig):
|
|
21
|
+
name = 'bazis.contrib.author'
|
|
22
|
+
verbose_name = _('Author')
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
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-04 07:22+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
|
+
#: admin_abstract.py:20 apps.py:7
|
|
21
|
+
msgid "Author"
|
|
22
|
+
msgstr ""
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
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-04 07:22+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 && "
|
|
20
|
+
"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || "
|
|
21
|
+
"(n%100>=11 && n%100<=14)? 2 : 3);\n"
|
|
22
|
+
#: admin_abstract.py:20 apps.py:7
|
|
23
|
+
msgid "Author"
|
|
24
|
+
msgstr "Автор"
|
|
@@ -0,0 +1,49 @@
|
|
|
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.conf import settings
|
|
16
|
+
from django.contrib.gis.db import models
|
|
17
|
+
|
|
18
|
+
from bazis.contrib.users.models_abstract import UserMixin
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthorMixin(UserMixin):
|
|
22
|
+
"""
|
|
23
|
+
Mixin that enables working with author fields in a model
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
author = models.ForeignKey(
|
|
27
|
+
settings.AUTH_USER_MODEL,
|
|
28
|
+
related_name='author_%(class)s',
|
|
29
|
+
blank=True,
|
|
30
|
+
null=True,
|
|
31
|
+
on_delete=models.SET_NULL,
|
|
32
|
+
)
|
|
33
|
+
author_updated = models.ForeignKey(
|
|
34
|
+
settings.AUTH_USER_MODEL,
|
|
35
|
+
related_name='author_updated_%(class)s',
|
|
36
|
+
blank=True,
|
|
37
|
+
null=True,
|
|
38
|
+
on_delete=models.SET_NULL,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
class Meta:
|
|
42
|
+
abstract = True
|
|
43
|
+
|
|
44
|
+
def save(self, *args, **kwargs):
|
|
45
|
+
if author := self.CTX_USER_REQUEST.get():
|
|
46
|
+
if not self.author:
|
|
47
|
+
self.author = author
|
|
48
|
+
self.author_updated = author
|
|
49
|
+
super().save(*args, **kwargs)
|
|
@@ -0,0 +1,50 @@
|
|
|
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.users.routes_abstract import UserRequiredRouteBase, UserRouteBase
|
|
16
|
+
from bazis.core.routes_abstract.jsonapi import JsonapiRouteBase
|
|
17
|
+
from bazis.core.schemas import ApiAction, CrudApiAction, SchemaFields
|
|
18
|
+
|
|
19
|
+
from .models_abstract import AuthorMixin
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthorRouteMixin(JsonapiRouteBase):
|
|
23
|
+
abstract: bool = True
|
|
24
|
+
|
|
25
|
+
fields: dict[ApiAction, SchemaFields] = {
|
|
26
|
+
CrudApiAction.CREATE: SchemaFields(
|
|
27
|
+
exclude={'author': None, 'author_updated': None},
|
|
28
|
+
),
|
|
29
|
+
CrudApiAction.UPDATE: SchemaFields(
|
|
30
|
+
exclude={'author': None, 'author_updated': None},
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def hook_before_create(self, item: AuthorMixin):
|
|
35
|
+
if not self.inject.user.is_anonymous:
|
|
36
|
+
item.author = self.inject.user
|
|
37
|
+
super().hook_before_create(item)
|
|
38
|
+
|
|
39
|
+
def hook_before_update(self, item: AuthorMixin):
|
|
40
|
+
if not self.inject.user.is_anonymous:
|
|
41
|
+
item.author_updated = self.inject.user
|
|
42
|
+
super().hook_before_update(item)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AuthorRouteBase(AuthorRouteMixin, UserRouteBase):
|
|
46
|
+
abstract: bool = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AuthorRequiredRouteBase(AuthorRouteMixin, UserRequiredRouteBase):
|
|
50
|
+
abstract: bool = True
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bazis-author
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Author 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-author
|
|
8
|
+
Keywords: bazis,django,fastapi,pydantic,framework,jsonapi,author
|
|
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: bazis-users
|
|
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 Author
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/bazis-author/)
|
|
30
|
+
[](https://pypi.org/project/bazis-author/)
|
|
31
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
32
|
+
|
|
33
|
+
An extension package for Bazis that provides automatic authorship tracking for models (who created and who updated records).
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Install the package
|
|
39
|
+
uv add bazis-author
|
|
40
|
+
|
|
41
|
+
# Create model with authorship tracking
|
|
42
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
43
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
44
|
+
from django.db import models
|
|
45
|
+
|
|
46
|
+
class Article(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
47
|
+
"""Article with author tracking"""
|
|
48
|
+
title = models.CharField(max_length=255)
|
|
49
|
+
content = models.TextField()
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
verbose_name = 'Article'
|
|
53
|
+
verbose_name_plural = 'Articles'
|
|
54
|
+
|
|
55
|
+
# Create route with automatic author population
|
|
56
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase
|
|
57
|
+
from django.apps import apps
|
|
58
|
+
|
|
59
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
60
|
+
model = apps.get_model('myapp.Article')
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Table of Contents
|
|
64
|
+
|
|
65
|
+
- [Description](#description)
|
|
66
|
+
- [Requirements](#requirements)
|
|
67
|
+
- [Installation](#installation)
|
|
68
|
+
- [Core Components](#core-components)
|
|
69
|
+
- [Model Mixins](#model-mixins)
|
|
70
|
+
- [Base Routes](#base-routes)
|
|
71
|
+
- [Admin Mixins](#admin-mixins)
|
|
72
|
+
- [Usage](#usage)
|
|
73
|
+
- [Creating Model with Authorship](#creating-model-with-authorship)
|
|
74
|
+
- [Creating Route with Authorship](#creating-route-with-authorship)
|
|
75
|
+
- [Admin Setup](#admin-setup)
|
|
76
|
+
- [How It Works](#how-it-works)
|
|
77
|
+
- [Examples](#examples)
|
|
78
|
+
- [License](#license)
|
|
79
|
+
- [Links](#links)
|
|
80
|
+
|
|
81
|
+
## Description
|
|
82
|
+
|
|
83
|
+
**Bazis Author** is an extension package for the Bazis framework that provides automatic authorship tracking for records. The package includes:
|
|
84
|
+
|
|
85
|
+
- **AuthorMixin** — mixin for models adding `author` (who created) and `author_updated` (who updated) fields
|
|
86
|
+
- **AuthorRouteBase** and **AuthorRequiredRouteBase** — base route classes with automatic author field population
|
|
87
|
+
- **AuthorAdminMixin** — admin mixin with convenient filters and readonly fields
|
|
88
|
+
|
|
89
|
+
The main advantage of the package is fully automatic author field population without the need to write additional code.
|
|
90
|
+
|
|
91
|
+
**This package requires `bazis` and `bazis-users` packages to be installed.**
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
- **Python**: 3.12+
|
|
96
|
+
- **bazis**: latest version
|
|
97
|
+
- **bazis-users**: latest version
|
|
98
|
+
- **PostgreSQL**: 12+
|
|
99
|
+
- **Redis**: For caching
|
|
100
|
+
|
|
101
|
+
## Installation
|
|
102
|
+
|
|
103
|
+
### Using uv (recommended)
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv add bazis-author
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Using pip
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install bazis-author
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Core Components
|
|
116
|
+
|
|
117
|
+
### Model Mixins
|
|
118
|
+
|
|
119
|
+
#### AuthorMixin
|
|
120
|
+
|
|
121
|
+
Mixin for models adding authorship tracking fields.
|
|
122
|
+
|
|
123
|
+
**Location**: `bazis.contrib.author.models_abstract.AuthorMixin`
|
|
124
|
+
|
|
125
|
+
**Added fields**:
|
|
126
|
+
|
|
127
|
+
- `author` — Foreign Key to user who created the record (nullable)
|
|
128
|
+
- `author_updated` — Foreign Key to user who last updated the record (nullable)
|
|
129
|
+
|
|
130
|
+
**Features**:
|
|
131
|
+
|
|
132
|
+
- Inherits from `UserMixin`, has access to user context via `CTX_USER_REQUEST`
|
|
133
|
+
- Automatically populates `author` when creating record
|
|
134
|
+
- Automatically updates `author_updated` on every save
|
|
135
|
+
- Fields are optional (nullable), so records can be created without a user
|
|
136
|
+
|
|
137
|
+
**Example usage**:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
141
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
142
|
+
from django.db import models
|
|
143
|
+
|
|
144
|
+
class Document(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
145
|
+
"""Document with authorship tracking"""
|
|
146
|
+
title = models.CharField('Title', max_length=255)
|
|
147
|
+
content = models.TextField('Content')
|
|
148
|
+
|
|
149
|
+
class Meta:
|
|
150
|
+
verbose_name = 'Document'
|
|
151
|
+
verbose_name_plural = 'Documents'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Base Routes
|
|
155
|
+
|
|
156
|
+
#### AuthorRouteMixin
|
|
157
|
+
|
|
158
|
+
Base mixin for routes that automatically populate author fields.
|
|
159
|
+
|
|
160
|
+
**Location**: `bazis.contrib.author.routes_abstract.AuthorRouteMixin`
|
|
161
|
+
|
|
162
|
+
**Features**:
|
|
163
|
+
|
|
164
|
+
- Excludes `author` and `author_updated` fields from CREATE and UPDATE schemas (they are populated automatically)
|
|
165
|
+
- Uses `hook_before_create` and `hook_before_update` hooks to set author
|
|
166
|
+
- Sets author only for authenticated users
|
|
167
|
+
|
|
168
|
+
**Methods**:
|
|
169
|
+
|
|
170
|
+
- `hook_before_create(item)` — sets `author` before creating record
|
|
171
|
+
- `hook_before_update(item)` — sets `author_updated` before updating record
|
|
172
|
+
|
|
173
|
+
#### AuthorRouteBase
|
|
174
|
+
|
|
175
|
+
Base route class with authorship support for optional authentication.
|
|
176
|
+
|
|
177
|
+
**Location**: `bazis.contrib.author.routes_abstract.AuthorRouteBase`
|
|
178
|
+
|
|
179
|
+
**Inheritance**: `AuthorRouteMixin` + `UserRouteBase`
|
|
180
|
+
|
|
181
|
+
Use this class when endpoints are accessible to both authenticated and anonymous users.
|
|
182
|
+
|
|
183
|
+
**Example usage**:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase
|
|
187
|
+
from django.apps import apps
|
|
188
|
+
|
|
189
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
190
|
+
"""Route for articles with automatic authorship"""
|
|
191
|
+
model = apps.get_model('myapp.Article')
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### AuthorRequiredRouteBase
|
|
195
|
+
|
|
196
|
+
Base route class with authorship support for required authentication.
|
|
197
|
+
|
|
198
|
+
**Location**: `bazis.contrib.author.routes_abstract.AuthorRequiredRouteBase`
|
|
199
|
+
|
|
200
|
+
**Inheritance**: `AuthorRouteMixin` + `UserRequiredRouteBase`
|
|
201
|
+
|
|
202
|
+
Use this class when endpoints require mandatory authentication (return 401 for unauthenticated requests).
|
|
203
|
+
|
|
204
|
+
**Example usage**:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from bazis.contrib.author.routes_abstract import AuthorRequiredRouteBase
|
|
208
|
+
from django.apps import apps
|
|
209
|
+
|
|
210
|
+
class PrivateDocumentRouteSet(AuthorRequiredRouteBase):
|
|
211
|
+
"""Route for private documents (authenticated only)"""
|
|
212
|
+
model = apps.get_model('myapp.PrivateDocument')
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Admin Mixins
|
|
216
|
+
|
|
217
|
+
#### AuthorAdminMixin
|
|
218
|
+
|
|
219
|
+
Mixin for Django admin classes adding convenient work with author fields.
|
|
220
|
+
|
|
221
|
+
**Location**: `bazis.contrib.author.admin_abstract.AuthorAdminMixin`
|
|
222
|
+
|
|
223
|
+
**Capabilities**:
|
|
224
|
+
|
|
225
|
+
- Adds `author__username` to search fields
|
|
226
|
+
- Makes `author` and `author_updated` fields readonly
|
|
227
|
+
- Adds autocomplete filter by author to list view
|
|
228
|
+
- Automatically populates author fields when saving through admin
|
|
229
|
+
|
|
230
|
+
**Example usage**:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from django.contrib import admin
|
|
234
|
+
from bazis.contrib.author.admin_abstract import AuthorAdminMixin
|
|
235
|
+
from bazis.core.admin_abstract import DtAdminMixin
|
|
236
|
+
from .models import Article
|
|
237
|
+
|
|
238
|
+
@admin.register(Article)
|
|
239
|
+
class ArticleAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
240
|
+
list_display = ('id', 'title', 'author', 'author_updated', 'dt_created')
|
|
241
|
+
search_fields = ('title', 'content')
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Usage
|
|
245
|
+
|
|
246
|
+
### Creating Model with Authorship
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
250
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
251
|
+
from django.db import models
|
|
252
|
+
|
|
253
|
+
class Article(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
254
|
+
"""Article with automatic author tracking"""
|
|
255
|
+
title = models.CharField('Title', max_length=255)
|
|
256
|
+
content = models.TextField('Content')
|
|
257
|
+
is_published = models.BooleanField('Published', default=False)
|
|
258
|
+
|
|
259
|
+
class Meta:
|
|
260
|
+
verbose_name = 'Article'
|
|
261
|
+
verbose_name_plural = 'Articles'
|
|
262
|
+
|
|
263
|
+
def __str__(self):
|
|
264
|
+
return self.title
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Creating Route with Authorship
|
|
268
|
+
|
|
269
|
+
#### For public endpoints (optional authentication)
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase
|
|
273
|
+
from django.apps import apps
|
|
274
|
+
|
|
275
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
276
|
+
"""
|
|
277
|
+
Articles can be created by anonymous users.
|
|
278
|
+
If user is authenticated - author fields will be populated automatically.
|
|
279
|
+
"""
|
|
280
|
+
model = apps.get_model('myapp.Article')
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Behavior**:
|
|
284
|
+
- Anonymous user creates record → `author` and `author_updated` remain `null`
|
|
285
|
+
- Authenticated user creates record → `author` is automatically populated
|
|
286
|
+
- Any user updates record → `author_updated` is populated if user is authenticated
|
|
287
|
+
|
|
288
|
+
#### For private endpoints (required authentication)
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from bazis.contrib.author.routes_abstract import AuthorRequiredRouteBase
|
|
292
|
+
from django.apps import apps
|
|
293
|
+
|
|
294
|
+
class PrivateDocumentRouteSet(AuthorRequiredRouteBase):
|
|
295
|
+
"""
|
|
296
|
+
Documents are accessible only to authenticated users.
|
|
297
|
+
Anonymous users will get 401.
|
|
298
|
+
"""
|
|
299
|
+
model = apps.get_model('myapp.PrivateDocument')
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Behavior**:
|
|
303
|
+
- Anonymous user tries to create/update record → 401 Unauthorized
|
|
304
|
+
- Authenticated user creates record → `author` is automatically populated
|
|
305
|
+
- Authenticated user updates record → `author_updated` is automatically updated
|
|
306
|
+
|
|
307
|
+
### Admin Setup
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from django.contrib import admin
|
|
311
|
+
from bazis.contrib.author.admin_abstract import AuthorAdminMixin
|
|
312
|
+
from bazis.core.admin_abstract import DtAdminMixin
|
|
313
|
+
from .models import Article
|
|
314
|
+
|
|
315
|
+
@admin.register(Article)
|
|
316
|
+
class ArticleAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
317
|
+
list_display = ('id', 'title', 'author', 'author_updated', 'is_published', 'dt_created')
|
|
318
|
+
search_fields = ('title', 'content')
|
|
319
|
+
list_filter = ('is_published',)
|
|
320
|
+
readonly_fields = ('dt_created', 'dt_updated')
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**What AuthorAdminMixin provides**:
|
|
324
|
+
|
|
325
|
+
1. **Autocomplete filter by author** in list view
|
|
326
|
+
2. **Search by author username** (`author__username`)
|
|
327
|
+
3. **Readonly fields** `author` and `author_updated` (cannot be edited)
|
|
328
|
+
4. **Automatic population** when saving through admin
|
|
329
|
+
|
|
330
|
+
## How It Works
|
|
331
|
+
|
|
332
|
+
### In Models
|
|
333
|
+
|
|
334
|
+
When calling `save()` on a model with `AuthorMixin`:
|
|
335
|
+
|
|
336
|
+
1. Check for user presence in `CTX_USER_REQUEST` context
|
|
337
|
+
2. If user exists:
|
|
338
|
+
- When creating record (`author` is empty) → set `author`
|
|
339
|
+
- When updating record → update `author_updated`
|
|
340
|
+
3. Call `super().save()` to save
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
def save(self, *args, **kwargs):
|
|
344
|
+
if author := self.CTX_USER_REQUEST.get():
|
|
345
|
+
if not self.author:
|
|
346
|
+
self.author = author
|
|
347
|
+
self.author_updated = author
|
|
348
|
+
super().save(*args, **kwargs)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### In Routes
|
|
352
|
+
|
|
353
|
+
When creating/updating record via API:
|
|
354
|
+
|
|
355
|
+
1. **In CREATE**: `hook_before_create(item)` is triggered
|
|
356
|
+
- If user is NOT anonymous → set `item.author`
|
|
357
|
+
|
|
358
|
+
2. **In UPDATE**: `hook_before_update(item)` is triggered
|
|
359
|
+
- If user is NOT anonymous → set `item.author_updated`
|
|
360
|
+
|
|
361
|
+
3. Fields `author` and `author_updated` are **excluded** from CREATE/UPDATE schemas, so client cannot pass them
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
fields: dict[ApiAction, SchemaFields] = {
|
|
365
|
+
CrudApiAction.CREATE: SchemaFields(
|
|
366
|
+
exclude={'author': None, 'author_updated': None},
|
|
367
|
+
),
|
|
368
|
+
CrudApiAction.UPDATE: SchemaFields(
|
|
369
|
+
exclude={'author': None, 'author_updated': None},
|
|
370
|
+
),
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### In Admin
|
|
375
|
+
|
|
376
|
+
When saving through Django Admin:
|
|
377
|
+
|
|
378
|
+
1. `save_model()` — sets author from `request.user`
|
|
379
|
+
2. `save_formset()` — sets author for inline models
|
|
380
|
+
|
|
381
|
+
## Examples
|
|
382
|
+
|
|
383
|
+
### Complete Application Example
|
|
384
|
+
|
|
385
|
+
**models.py**:
|
|
386
|
+
```python
|
|
387
|
+
from bazis.contrib.author.models_abstract import AuthorMixin
|
|
388
|
+
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
|
|
389
|
+
from django.db import models
|
|
390
|
+
|
|
391
|
+
class Article(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
392
|
+
"""Public article"""
|
|
393
|
+
title = models.CharField('Title', max_length=255)
|
|
394
|
+
content = models.TextField('Content')
|
|
395
|
+
is_published = models.BooleanField('Published', default=False)
|
|
396
|
+
|
|
397
|
+
class Meta:
|
|
398
|
+
verbose_name = 'Article'
|
|
399
|
+
verbose_name_plural = 'Articles'
|
|
400
|
+
|
|
401
|
+
class PrivateNote(AuthorMixin, DtMixin, UuidMixin, JsonApiMixin):
|
|
402
|
+
"""Private note (authenticated only)"""
|
|
403
|
+
title = models.CharField('Title', max_length=255)
|
|
404
|
+
content = models.TextField('Content')
|
|
405
|
+
|
|
406
|
+
class Meta:
|
|
407
|
+
verbose_name = 'Note'
|
|
408
|
+
verbose_name_plural = 'Notes'
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**routes.py**:
|
|
412
|
+
```python
|
|
413
|
+
from bazis.contrib.author.routes_abstract import AuthorRouteBase, AuthorRequiredRouteBase
|
|
414
|
+
from django.apps import apps
|
|
415
|
+
|
|
416
|
+
class ArticleRouteSet(AuthorRouteBase):
|
|
417
|
+
"""Articles accessible to everyone, but author is tracked if user is authenticated"""
|
|
418
|
+
model = apps.get_model('myapp.Article')
|
|
419
|
+
|
|
420
|
+
class PrivateNoteRouteSet(AuthorRequiredRouteBase):
|
|
421
|
+
"""Notes accessible only to authenticated users"""
|
|
422
|
+
model = apps.get_model('myapp.PrivateNote')
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**router.py**:
|
|
426
|
+
```python
|
|
427
|
+
from bazis.core.routing import BazisRouter
|
|
428
|
+
from . import routes
|
|
429
|
+
|
|
430
|
+
router = BazisRouter(tags=['Content'])
|
|
431
|
+
router.register(routes.ArticleRouteSet.as_router())
|
|
432
|
+
router.register(routes.PrivateNoteRouteSet.as_router())
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**admin.py**:
|
|
436
|
+
```python
|
|
437
|
+
from django.contrib import admin
|
|
438
|
+
from bazis.contrib.author.admin_abstract import AuthorAdminMixin
|
|
439
|
+
from bazis.core.admin_abstract import DtAdminMixin
|
|
440
|
+
from .models import Article, PrivateNote
|
|
441
|
+
|
|
442
|
+
@admin.register(Article)
|
|
443
|
+
class ArticleAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
444
|
+
list_display = ('id', 'title', 'author', 'is_published', 'dt_created')
|
|
445
|
+
search_fields = ('title', 'content')
|
|
446
|
+
list_filter = ('is_published',)
|
|
447
|
+
|
|
448
|
+
@admin.register(PrivateNote)
|
|
449
|
+
class PrivateNoteAdmin(AuthorAdminMixin, DtAdminMixin, admin.ModelAdmin):
|
|
450
|
+
list_display = ('id', 'title', 'author', 'dt_created')
|
|
451
|
+
search_fields = ('title', 'content')
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### API Usage Examples
|
|
455
|
+
|
|
456
|
+
**Creating article by anonymous user**:
|
|
457
|
+
```bash
|
|
458
|
+
POST /api/v1/content/article/
|
|
459
|
+
Content-Type: application/json
|
|
460
|
+
|
|
461
|
+
{
|
|
462
|
+
"data": {
|
|
463
|
+
"type": "myapp.article",
|
|
464
|
+
"attributes": {
|
|
465
|
+
"title": "My Article",
|
|
466
|
+
"content": "Article content",
|
|
467
|
+
"is_published": true
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Response: 201 Created
|
|
473
|
+
# author and author_updated will be null
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Creating article by authenticated user**:
|
|
477
|
+
```bash
|
|
478
|
+
POST /api/v1/content/article/
|
|
479
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
480
|
+
Content-Type: application/json
|
|
481
|
+
|
|
482
|
+
{
|
|
483
|
+
"data": {
|
|
484
|
+
"type": "myapp.article",
|
|
485
|
+
"attributes": {
|
|
486
|
+
"title": "My Article",
|
|
487
|
+
"content": "Article content",
|
|
488
|
+
"is_published": true
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Response: 201 Created
|
|
494
|
+
# author is automatically set to current user
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Attempting to create private note without authentication**:
|
|
498
|
+
```bash
|
|
499
|
+
POST /api/v1/content/private_note/
|
|
500
|
+
Content-Type: application/json
|
|
501
|
+
|
|
502
|
+
{
|
|
503
|
+
"data": {
|
|
504
|
+
"type": "myapp.privatenote",
|
|
505
|
+
"attributes": {
|
|
506
|
+
"title": "Secret Note",
|
|
507
|
+
"content": "Secret content"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# Response: 401 Unauthorized
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Creating private note with authentication**:
|
|
516
|
+
```bash
|
|
517
|
+
POST /api/v1/content/private_note/
|
|
518
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
519
|
+
Content-Type: application/json
|
|
520
|
+
|
|
521
|
+
{
|
|
522
|
+
"data": {
|
|
523
|
+
"type": "myapp.privatenote",
|
|
524
|
+
"attributes": {
|
|
525
|
+
"title": "Secret Note",
|
|
526
|
+
"content": "Secret content"
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# Response: 201 Created
|
|
532
|
+
# author is automatically set to current user
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Authorship Check in Responses
|
|
536
|
+
|
|
537
|
+
Fields `author` and `author_updated` are returned in the `relationships` block:
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{
|
|
541
|
+
"data": {
|
|
542
|
+
"type": "myapp.article",
|
|
543
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
544
|
+
"attributes": {
|
|
545
|
+
"title": "My Article",
|
|
546
|
+
"content": "Article content",
|
|
547
|
+
"is_published": true
|
|
548
|
+
},
|
|
549
|
+
"relationships": {
|
|
550
|
+
"author": {
|
|
551
|
+
"data": {
|
|
552
|
+
"type": "users.user",
|
|
553
|
+
"id": "456e7890-e89b-12d3-a456-426614174000"
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
"author_updated": {
|
|
557
|
+
"data": {
|
|
558
|
+
"type": "users.user",
|
|
559
|
+
"id": "456e7890-e89b-12d3-a456-426614174000"
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## License
|
|
568
|
+
|
|
569
|
+
Apache License 2.0
|
|
570
|
+
|
|
571
|
+
See [LICENSE](LICENSE) file for details.
|
|
572
|
+
|
|
573
|
+
## Links
|
|
574
|
+
|
|
575
|
+
- [Bazis Documentation](https://github.com/ecofuture-tech/bazis) — main repository
|
|
576
|
+
- [Bazis Users](https://github.com/ecofuture-tech/bazis-users) — user management package
|
|
577
|
+
- [Bazis Author Repository](https://github.com/ecofuture-tech/bazis-author) — package repository
|
|
578
|
+
- [Issue Tracker](https://github.com/ecofuture-tech/bazis-author/issues) — report bugs or request features
|
|
579
|
+
|
|
580
|
+
## Support
|
|
581
|
+
|
|
582
|
+
If you have questions or issues:
|
|
583
|
+
- Check the [Bazis documentation](https://github.com/ecofuture-tech/bazis)
|
|
584
|
+
- Search [existing issues](https://github.com/ecofuture-tech/bazis-author/issues)
|
|
585
|
+
- Create a [new issue](https://github.com/ecofuture-tech/bazis-author/issues/new) with detailed information
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
Made with ❤️ by the Bazis team
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
bazis/contrib/author/__init__.py,sha256=GF_6m8rcR8n6hcDDb-LLoiJk9lR-FoaZZEzkg2q-rK8,780
|
|
2
|
+
bazis/contrib/author/_version.py,sha256=6OGz4a0gjMGlckPyPCNiJDWyFDO-tWO8O_ZNx4ajT2Y,704
|
|
3
|
+
bazis/contrib/author/admin_abstract.py,sha256=xYUi8TPJ4_ifFxWzkMDziPXP0sIirVlUg8WSNt4gghs,1938
|
|
4
|
+
bazis/contrib/author/apps.py,sha256=2AP3Po84KEgjKbjjc8waa9ccR4xalw4cWRkdG1KCebY,816
|
|
5
|
+
bazis/contrib/author/models_abstract.py,sha256=lYSoo6l3EKOjRerK5fxFzcWmbyj0cvHUqyfi51WFdjQ,1525
|
|
6
|
+
bazis/contrib/author/routes_abstract.py,sha256=mVC5PuSbQEcSr1GX3FpzyyjLsOXgBaRBSJmn-AXvcmc,1793
|
|
7
|
+
bazis/contrib/author/locale/en/LC_MESSAGES/django.mo,sha256=N1pb17IfLd0ASiKO8d68-B4ygSpDkhKOCs8YTzMXQo0,380
|
|
8
|
+
bazis/contrib/author/locale/en/LC_MESSAGES/django.po,sha256=q5JTOSSpmRY-nFh4-6HTpu_OpLdVqA7JgQEHfatBc-w,686
|
|
9
|
+
bazis/contrib/author/locale/ru/LC_MESSAGES/django.mo,sha256=ypfFf1v7X21aXoTlA16HGjykaNbh--si-73UHNXCW8Q,560
|
|
10
|
+
bazis/contrib/author/locale/ru/LC_MESSAGES/django.po,sha256=qkgoI90R73pNNtchLEgL6WC7vNTaMa91VzqFmEwi4nk,840
|
|
11
|
+
bazis_author-2.2.0.dist-info/METADATA,sha256=njWDVKMMGvsB5WIuR205j5TXwgR00hT8ww6atJ55yRY,16937
|
|
12
|
+
bazis_author-2.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
13
|
+
bazis_author-2.2.0.dist-info/top_level.txt,sha256=WgdrPZTZBMG8i_EqxA3vU5qI4ETQ_RsqKqSqsfIApHY,6
|
|
14
|
+
bazis_author-2.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bazis
|