drf-parcel 0.1.0__tar.gz
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.
- drf_parcel-0.1.0/.gitignore +31 -0
- drf_parcel-0.1.0/CHANGELOG.fa.md +49 -0
- drf_parcel-0.1.0/CHANGELOG.md +48 -0
- drf_parcel-0.1.0/LICENSE +28 -0
- drf_parcel-0.1.0/PKG-INFO +198 -0
- drf_parcel-0.1.0/README.fa.md +163 -0
- drf_parcel-0.1.0/README.md +163 -0
- drf_parcel-0.1.0/docs/changelog.fa.md +3 -0
- drf_parcel-0.1.0/docs/changelog.md +3 -0
- drf_parcel-0.1.0/docs/contract.fa.md +540 -0
- drf_parcel-0.1.0/docs/contract.md +542 -0
- drf_parcel-0.1.0/docs/explanation/alternatives.fa.md +30 -0
- drf_parcel-0.1.0/docs/explanation/alternatives.md +31 -0
- drf_parcel-0.1.0/docs/explanation/how-an-answer-is-made.fa.md +46 -0
- drf_parcel-0.1.0/docs/explanation/how-an-answer-is-made.md +46 -0
- drf_parcel-0.1.0/docs/explanation/index.fa.md +10 -0
- drf_parcel-0.1.0/docs/explanation/index.md +10 -0
- drf_parcel-0.1.0/docs/explanation/why-one-envelope.fa.md +41 -0
- drf_parcel-0.1.0/docs/explanation/why-one-envelope.md +42 -0
- drf_parcel-0.1.0/docs/faq.fa.md +59 -0
- drf_parcel-0.1.0/docs/faq.md +60 -0
- drf_parcel-0.1.0/docs/getting-started/installation.fa.md +61 -0
- drf_parcel-0.1.0/docs/getting-started/installation.md +62 -0
- drf_parcel-0.1.0/docs/getting-started/quickstart.fa.md +106 -0
- drf_parcel-0.1.0/docs/getting-started/quickstart.md +107 -0
- drf_parcel-0.1.0/docs/hooks.py +16 -0
- drf_parcel-0.1.0/docs/how-to/adopt.fa.md +85 -0
- drf_parcel-0.1.0/docs/how-to/adopt.md +85 -0
- drf_parcel-0.1.0/docs/how-to/choose-a-page.fa.md +64 -0
- drf_parcel-0.1.0/docs/how-to/choose-a-page.md +66 -0
- drf_parcel-0.1.0/docs/how-to/database-constraints.fa.md +20 -0
- drf_parcel-0.1.0/docs/how-to/database-constraints.md +20 -0
- drf_parcel-0.1.0/docs/how-to/frontend.fa.md +77 -0
- drf_parcel-0.1.0/docs/how-to/frontend.md +79 -0
- drf_parcel-0.1.0/docs/how-to/index.fa.md +17 -0
- drf_parcel-0.1.0/docs/how-to/index.md +17 -0
- drf_parcel-0.1.0/docs/how-to/leave-a-view-out.fa.md +34 -0
- drf_parcel-0.1.0/docs/how-to/leave-a-view-out.md +33 -0
- drf_parcel-0.1.0/docs/how-to/own-errors.fa.md +71 -0
- drf_parcel-0.1.0/docs/how-to/own-errors.md +69 -0
- drf_parcel-0.1.0/docs/how-to/own-schema-or-handler.fa.md +45 -0
- drf_parcel-0.1.0/docs/how-to/own-schema-or-handler.md +46 -0
- drf_parcel-0.1.0/docs/how-to/rename-error-codes.fa.md +33 -0
- drf_parcel-0.1.0/docs/how-to/rename-error-codes.md +33 -0
- drf_parcel-0.1.0/docs/how-to/report-bugs.fa.md +40 -0
- drf_parcel-0.1.0/docs/how-to/report-bugs.md +40 -0
- drf_parcel-0.1.0/docs/how-to/testing.fa.md +59 -0
- drf_parcel-0.1.0/docs/how-to/testing.md +58 -0
- drf_parcel-0.1.0/docs/index.fa.md +69 -0
- drf_parcel-0.1.0/docs/index.md +68 -0
- drf_parcel-0.1.0/docs/project/contributing.fa.md +39 -0
- drf_parcel-0.1.0/docs/project/contributing.md +39 -0
- drf_parcel-0.1.0/docs/project/security.fa.md +21 -0
- drf_parcel-0.1.0/docs/project/security.md +21 -0
- drf_parcel-0.1.0/docs/project/versions.fa.md +22 -0
- drf_parcel-0.1.0/docs/project/versions.md +23 -0
- drf_parcel-0.1.0/docs/reference/api/envelope.fa.md +5 -0
- drf_parcel-0.1.0/docs/reference/api/envelope.md +3 -0
- drf_parcel-0.1.0/docs/reference/api/exceptions.fa.md +5 -0
- drf_parcel-0.1.0/docs/reference/api/exceptions.md +3 -0
- drf_parcel-0.1.0/docs/reference/api/handlers.fa.md +5 -0
- drf_parcel-0.1.0/docs/reference/api/handlers.md +3 -0
- drf_parcel-0.1.0/docs/reference/api/pagination.fa.md +5 -0
- drf_parcel-0.1.0/docs/reference/api/pagination.md +3 -0
- drf_parcel-0.1.0/docs/reference/api/responses.fa.md +5 -0
- drf_parcel-0.1.0/docs/reference/api/responses.md +3 -0
- drf_parcel-0.1.0/docs/reference/api/schema.fa.md +5 -0
- drf_parcel-0.1.0/docs/reference/api/schema.md +3 -0
- drf_parcel-0.1.0/docs/reference/checks.fa.md +21 -0
- drf_parcel-0.1.0/docs/reference/checks.md +21 -0
- drf_parcel-0.1.0/docs/reference/error-codes.fa.md +39 -0
- drf_parcel-0.1.0/docs/reference/error-codes.md +38 -0
- drf_parcel-0.1.0/docs/reference/index.fa.md +11 -0
- drf_parcel-0.1.0/docs/reference/index.md +11 -0
- drf_parcel-0.1.0/docs/reference/settings.fa.md +73 -0
- drf_parcel-0.1.0/docs/reference/settings.md +73 -0
- drf_parcel-0.1.0/docs/stylesheets/extra.css +20 -0
- drf_parcel-0.1.0/docs/tutorial/01-the-envelope.fa.md +59 -0
- drf_parcel-0.1.0/docs/tutorial/01-the-envelope.md +61 -0
- drf_parcel-0.1.0/docs/tutorial/02-pages.fa.md +66 -0
- drf_parcel-0.1.0/docs/tutorial/02-pages.md +66 -0
- drf_parcel-0.1.0/docs/tutorial/03-errors.fa.md +86 -0
- drf_parcel-0.1.0/docs/tutorial/03-errors.md +83 -0
- drf_parcel-0.1.0/docs/tutorial/04-schema-and-languages.fa.md +45 -0
- drf_parcel-0.1.0/docs/tutorial/04-schema-and-languages.md +44 -0
- drf_parcel-0.1.0/docs/tutorial/index.fa.md +30 -0
- drf_parcel-0.1.0/docs/tutorial/index.md +29 -0
- drf_parcel-0.1.0/example/README.fa.md +92 -0
- drf_parcel-0.1.0/example/README.md +93 -0
- drf_parcel-0.1.0/example/config/__init__.py +0 -0
- drf_parcel-0.1.0/example/config/settings.py +85 -0
- drf_parcel-0.1.0/example/config/urls.py +9 -0
- drf_parcel-0.1.0/example/manage.py +11 -0
- drf_parcel-0.1.0/example/shop/__init__.py +0 -0
- drf_parcel-0.1.0/example/shop/api.py +120 -0
- drf_parcel-0.1.0/example/shop/apps.py +5 -0
- drf_parcel-0.1.0/example/shop/errors.py +42 -0
- drf_parcel-0.1.0/example/shop/management/__init__.py +0 -0
- drf_parcel-0.1.0/example/shop/management/commands/__init__.py +0 -0
- drf_parcel-0.1.0/example/shop/management/commands/example_data.py +32 -0
- drf_parcel-0.1.0/example/shop/migrations/0001_initial.py +61 -0
- drf_parcel-0.1.0/example/shop/migrations/__init__.py +0 -0
- drf_parcel-0.1.0/example/shop/models.py +32 -0
- drf_parcel-0.1.0/example/shop/reporting.py +16 -0
- drf_parcel-0.1.0/example/shop/services.py +37 -0
- drf_parcel-0.1.0/example/shop/tests.py +187 -0
- drf_parcel-0.1.0/example/shop/urls.py +13 -0
- drf_parcel-0.1.0/pyproject.toml +137 -0
- drf_parcel-0.1.0/src/drf_parcel/__init__.py +0 -0
- drf_parcel-0.1.0/src/drf_parcel/apps.py +15 -0
- drf_parcel-0.1.0/src/drf_parcel/checks.py +167 -0
- drf_parcel-0.1.0/src/drf_parcel/conf.py +106 -0
- drf_parcel-0.1.0/src/drf_parcel/envelope.py +99 -0
- drf_parcel-0.1.0/src/drf_parcel/exceptions.py +109 -0
- drf_parcel-0.1.0/src/drf_parcel/handlers.py +270 -0
- drf_parcel-0.1.0/src/drf_parcel/integrity.py +75 -0
- drf_parcel-0.1.0/src/drf_parcel/locale/fa/LC_MESSAGES/django.mo +0 -0
- drf_parcel-0.1.0/src/drf_parcel/locale/fa/LC_MESSAGES/django.po +79 -0
- drf_parcel-0.1.0/src/drf_parcel/messages.py +54 -0
- drf_parcel-0.1.0/src/drf_parcel/pagination.py +271 -0
- drf_parcel-0.1.0/src/drf_parcel/py.typed +0 -0
- drf_parcel-0.1.0/src/drf_parcel/responses.py +196 -0
- drf_parcel-0.1.0/src/drf_parcel/schema.py +244 -0
- drf_parcel-0.1.0/tests/__init__.py +0 -0
- drf_parcel-0.1.0/tests/conftest.py +31 -0
- drf_parcel-0.1.0/tests/project/__init__.py +0 -0
- drf_parcel-0.1.0/tests/project/documented.py +122 -0
- drf_parcel-0.1.0/tests/project/failures.py +154 -0
- drf_parcel-0.1.0/tests/project/handlers.py +15 -0
- drf_parcel-0.1.0/tests/project/settings.py +69 -0
- drf_parcel-0.1.0/tests/project/shelf/__init__.py +0 -0
- drf_parcel-0.1.0/tests/project/shelf/models.py +23 -0
- drf_parcel-0.1.0/tests/project/shelf/views.py +87 -0
- drf_parcel-0.1.0/tests/project/urls.py +36 -0
- drf_parcel-0.1.0/tests/project/views.py +175 -0
- drf_parcel-0.1.0/tests/test_a_bug_never_tells_its_secrets.py +103 -0
- drf_parcel-0.1.0/tests/test_a_constraint_the_database_refuses.py +110 -0
- drf_parcel-0.1.0/tests/test_a_delete_can_be_read.py +47 -0
- drf_parcel-0.1.0/tests/test_a_failure_undoes_its_write.py +25 -0
- drf_parcel-0.1.0/tests/test_a_form_learns_every_field_that_is_wrong.py +115 -0
- drf_parcel-0.1.0/tests/test_a_list_comes_a_page_at_a_time.py +95 -0
- drf_parcel-0.1.0/tests/test_a_misconfigured_setting_is_caught_at_start.py +140 -0
- drf_parcel-0.1.0/tests/test_a_misspelt_setting_stops_the_start.py +46 -0
- drf_parcel-0.1.0/tests/test_a_page_never_holds_more_than_the_ceiling.py +100 -0
- drf_parcel-0.1.0/tests/test_a_projects_own_error_answers_in_the_envelope.py +55 -0
- drf_parcel-0.1.0/tests/test_a_success_says_what_happened.py +70 -0
- drf_parcel-0.1.0/tests/test_a_view_keeps_its_headers_and_cookies.py +20 -0
- drf_parcel-0.1.0/tests/test_a_view_without_the_mixin.py +25 -0
- drf_parcel-0.1.0/tests/test_an_answer_is_wrapped_once.py +44 -0
- drf_parcel-0.1.0/tests/test_an_endless_feed_is_read_by_cursor.py +101 -0
- drf_parcel-0.1.0/tests/test_errors_work_without_the_librarys_handler.py +17 -0
- drf_parcel-0.1.0/tests/test_every_answer_has_the_same_top_level.py +56 -0
- drf_parcel-0.1.0/tests/test_every_failure_has_its_code.py +138 -0
- drf_parcel-0.1.0/tests/test_the_envelope_holds_its_own_rules.py +40 -0
- drf_parcel-0.1.0/tests/test_the_library_speaks_persian.py +87 -0
- drf_parcel-0.1.0/tests/test_the_schema_is_an_extra.py +41 -0
- drf_parcel-0.1.0/tests/test_the_schema_says_what_the_wire_carries.py +238 -0
- drf_parcel-0.1.0/tests/test_what_is_not_json_is_left_alone.py +77 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
|
|
5
|
+
# Packaging
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# Environments
|
|
11
|
+
.venv/
|
|
12
|
+
.nox/
|
|
13
|
+
|
|
14
|
+
# Tool caches and reports
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
.coverage.*
|
|
20
|
+
coverage.xml
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Editors
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
|
|
27
|
+
# The example project's database
|
|
28
|
+
example/db.sqlite3
|
|
29
|
+
|
|
30
|
+
# The documentation site
|
|
31
|
+
site/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# تغییرات
|
|
2
|
+
|
|
3
|
+
[English](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
## منتشرنشده
|
|
6
|
+
|
|
7
|
+
## 0.1.0 - ۱۴۰۵/۰۸/۲۳ - اولین نسخه
|
|
8
|
+
|
|
9
|
+
همهی آنچه [قرارداد](https://github.com/amirAI-dot/drf-parcel/blob/main/docs/contract.fa.md) میگوید، ساخته شده.
|
|
10
|
+
|
|
11
|
+
* **قالب پاسخ** - `success`، `status_code`، `message` و `data` روی هر پاسخ؛
|
|
12
|
+
`pagination` روی لیست صفحهبندیشده؛ `error_code` روی خطا. در یک جا ساخته
|
|
13
|
+
میشود، که ساختن قالب غلط را رد میکند.
|
|
14
|
+
* **پاسخ موفق میگوید چه شد** - Created، Saved، Deleted یا Done، از روی خود
|
|
15
|
+
درخواست، و برای یک پاسخ، یک view یا کل پروژه قابل تغییر.
|
|
16
|
+
* **`EnvelopeMixin`** پاسخهای موفق view را یک بار در قالب میگذارد. حذف با
|
|
17
|
+
200 همراه با قالب جواب میدهد. چیزی که JSON نیست، و viewی که بیرون میماند،
|
|
18
|
+
دست نمیخورد.
|
|
19
|
+
* **صفحهبندی** شمارهای یا با cursor، که هر view خودش انتخاب میکند. کلاینت
|
|
20
|
+
فقط جایی تعداد هر صفحه را انتخاب میکند که view اجازه بدهد، و همیشه زیر سقف.
|
|
21
|
+
`PAGE_SIZE` خود DRF وقتی کتابخانه عددی نگرفته حفظ میشود.
|
|
22
|
+
* **exception handler** هر خطایی را در قالب جواب میدهد: خطای اعتبارسنجی با
|
|
23
|
+
مشکل هر فیلد و کدش در یک مسیر تخت؛ خطاهای DRF و Django با کد استاندارد و
|
|
24
|
+
هدرهایشان؛ `APIException` خود پروژه بدون تغییر؛ قیدی که دیتابیس رد میکند بر
|
|
25
|
+
اساس نوعش، وقتی روشن باشد؛ و یک باگ با 500 عمومی که متنش هرگز از سرور بیرون
|
|
26
|
+
نمیرود - لاگ میشود، گزارش میشود، و در `DEBUG` بالا میرود. هر خطا نوشتن
|
|
27
|
+
درخواست را برمیگرداند.
|
|
28
|
+
* **`DomainError`** - همان `APIException` خود DRF با `data` - و خطاهای رایج.
|
|
29
|
+
* **اسکیما** با drf-spectacular: هر پاسخ موفق، صفحه و خطا همانطور که به
|
|
30
|
+
کلاینت میرسد.
|
|
31
|
+
* **تنظیم `DRF_PARCEL`** کلیدی را که نمیشناسد رد میکند، و `manage.py check`
|
|
32
|
+
مقداری را که کار نمیکند گزارش میدهد.
|
|
33
|
+
* **فارسی**، برای هر جملهای که کتابخانه میگوید.
|
|
34
|
+
|
|
35
|
+
پیدا شده در یک بررسی پیش از انتشار، و درست شده:
|
|
36
|
+
|
|
37
|
+
* `success_response(status_code=204)` با 200 و همراه بدنهاش جواب میدهد، مثل
|
|
38
|
+
هر 204 دیگری - 204 بدنه ندارد، و کلاینت چیزی نمیخواند.
|
|
39
|
+
* `PermissionDenied` خود Django جملهای را که با آن raise شده نگه میدارد،
|
|
40
|
+
همانطور که handler خود DRF نگه میدارد.
|
|
41
|
+
* viewی که `parcel_wrap = False` دارد همانطور جواب داده میشود که DRF جواب
|
|
42
|
+
میدهد - صفحههایش هم، که هنوز در قالب بودند.
|
|
43
|
+
* `ERROR_CODES`ی که کار نمیکند هرگز پاسخ یک خطا را خراب نمیکند.
|
|
44
|
+
* اسکیما handler خود پروژه را که روی handler کتابخانه ساخته شده میشناسد، و
|
|
45
|
+
دیگر وقتی drf-spectacular صفحهای را بیرون از یک عملیات نامگذاری میکند خطا
|
|
46
|
+
نمیدهد.
|
|
47
|
+
|
|
48
|
+
Python 3.11 تا 3.14؛ Django 5.2، 6.0 و 6.1؛ Django REST framework 3.16 و
|
|
49
|
+
بالاتر؛ drf-spectacular 0.28 و بالاتر.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
[فارسی](CHANGELOG.fa.md)
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.1.0 - 2026-11-14 - the first release
|
|
8
|
+
|
|
9
|
+
Everything [the contract](https://github.com/amirAI-dot/drf-parcel/blob/main/docs/contract.md) says, built.
|
|
10
|
+
|
|
11
|
+
* **The envelope** - `success`, `status_code`, `message` and `data` on every
|
|
12
|
+
answer; `pagination` on a paged list; `error_code` on a failure. Built in
|
|
13
|
+
one place, which refuses to build it wrong.
|
|
14
|
+
* **A success says what happened** - Created, Saved, Deleted or Done, read off
|
|
15
|
+
the request, and replaced per answer, per view or for the project.
|
|
16
|
+
* **`EnvelopeMixin`** wraps a view's successes once. A delete answers 200 with
|
|
17
|
+
the envelope. What is not JSON, and a view that opts out, is left alone.
|
|
18
|
+
* **Pagination** by page number or by cursor, chosen per view. A client
|
|
19
|
+
chooses the page size only where a view allows it, always under a ceiling.
|
|
20
|
+
DRF's own `PAGE_SIZE` is kept when the library is not told one.
|
|
21
|
+
* **The exception handler** answers every failure in the envelope:
|
|
22
|
+
validation errors with every field's problem and its code at a flat path;
|
|
23
|
+
DRF's and Django's failures with a standard code and their headers; a
|
|
24
|
+
project's own `APIException` unchanged; a constraint the database refuses
|
|
25
|
+
by its kind, when turned on; and a bug with a generic 500 whose text never
|
|
26
|
+
leaves the server - logged, reported, and raised under `DEBUG`. Every
|
|
27
|
+
failure undoes the request's write.
|
|
28
|
+
* **`DomainError`** - DRF's `APIException` with `data` - and the common errors.
|
|
29
|
+
* **The schema** with drf-spectacular: every success, page and failure as the
|
|
30
|
+
client receives it.
|
|
31
|
+
* **The `DRF_PARCEL` setting** refuses a key it does not know, and
|
|
32
|
+
`manage.py check` reports a value that cannot work.
|
|
33
|
+
* **Persian**, for every sentence the library says.
|
|
34
|
+
|
|
35
|
+
Found by a review before the release, and fixed:
|
|
36
|
+
|
|
37
|
+
* `success_response(status_code=204)` answers 200 with its body, as every
|
|
38
|
+
other 204 does - a 204 carries none, and the client read nothing.
|
|
39
|
+
* Django's `PermissionDenied` keeps the sentence it was raised with, as DRF's
|
|
40
|
+
own handler does.
|
|
41
|
+
* A view with `parcel_wrap = False` is answered as DRF answers - its pages
|
|
42
|
+
too, which were still in the envelope.
|
|
43
|
+
* An `ERROR_CODES` that cannot work never breaks the answer to a failure.
|
|
44
|
+
* The schema recognises a project's own handler built on the library's, and
|
|
45
|
+
no longer fails when drf-spectacular names a page outside an operation.
|
|
46
|
+
|
|
47
|
+
Python 3.11 to 3.14; Django 5.2, 6.0 and 6.1; Django REST framework 3.16 and
|
|
48
|
+
later; drf-spectacular 0.28 and later.
|
drf_parcel-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, amir mohammad kashanchian
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: drf-parcel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One response shape for a Django REST framework API - success, page and error alike.
|
|
5
|
+
Project-URL: Documentation, https://amirai-dot.github.io/drf-parcel/
|
|
6
|
+
Project-URL: Repository, https://github.com/amirAI-dot/drf-parcel
|
|
7
|
+
Project-URL: Changelog, https://github.com/amirAI-dot/drf-parcel/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Contract, https://github.com/amirAI-dot/drf-parcel/blob/main/docs/contract.md
|
|
9
|
+
Author: amir mohammad kashanchian
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,django,djangorestframework,drf,envelope,errors,pagination,response
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 5.2
|
|
17
|
+
Classifier: Framework :: Django :: 6.0
|
|
18
|
+
Classifier: Framework :: Django :: 6.1
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python
|
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
|
+
Requires-Dist: django>=5.2
|
|
31
|
+
Requires-Dist: djangorestframework>=3.16
|
|
32
|
+
Provides-Extra: schema
|
|
33
|
+
Requires-Dist: drf-spectacular>=0.28; extra == 'schema'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# drf-parcel
|
|
37
|
+
|
|
38
|
+
One response shape for a Django REST framework API - success, page and error
|
|
39
|
+
alike - and the generated schema saying exactly that shape.
|
|
40
|
+
|
|
41
|
+
[فارسی](https://github.com/amirAI-dot/drf-parcel/blob/main/README.fa.md)
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{"success": true, "status_code": 200, "message": "Done.", "data": {"id": 7}}
|
|
45
|
+
{"success": true, "status_code": 200, "message": "Done.", "data": [], "pagination": {"type": "page", "count": 45, "page": 2, "pages": 3, "page_size": 20, "has_next": true, "has_previous": true}}
|
|
46
|
+
{"success": false, "status_code": 409, "message": "...", "data": {"branches": 3}, "error_code": "province_in_use"}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A client reads one shape and never special-cases a payload: the answer is
|
|
50
|
+
under `data`, a person reads `message`, and code branches on `error_code` -
|
|
51
|
+
never on the message, which is translated.
|
|
52
|
+
|
|
53
|
+
**[The documentation](https://amirai-dot.github.io/drf-parcel/)** - a
|
|
54
|
+
quickstart, a tutorial, how-to guides, the reference - in English and
|
|
55
|
+
Persian.
|
|
56
|
+
|
|
57
|
+
## In a project
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install 'drf-parcel[schema]' # the schema extra brings drf-spectacular
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
INSTALLED_APPS = [
|
|
65
|
+
...,
|
|
66
|
+
'rest_framework',
|
|
67
|
+
'drf_parcel',
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
REST_FRAMEWORK = {
|
|
71
|
+
'EXCEPTION_HANDLER': 'drf_parcel.handlers.exception_handler',
|
|
72
|
+
'DEFAULT_PAGINATION_CLASS': 'drf_parcel.pagination.PageNumberPagination',
|
|
73
|
+
'DEFAULT_SCHEMA_CLASS': 'drf_parcel.schema.ParcelAutoSchema',
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each line is independent: the handler alone gives the error envelope and
|
|
78
|
+
nothing else, and a project adopts the rest one view at a time.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from drf_parcel.pagination import CursorPagination
|
|
82
|
+
from drf_parcel.responses import EnvelopeMixin, success_response
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ProductViewSet(EnvelopeMixin, ModelViewSet):
|
|
86
|
+
queryset = Product.objects.all()
|
|
87
|
+
serializer_class = ProductSerializer
|
|
88
|
+
page_size = 12
|
|
89
|
+
client_page_size = True # the client may send ?page_size=
|
|
90
|
+
max_page_size = 48 # and never gets more
|
|
91
|
+
envelope_messages = {'create': _('The product was added.')}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NotificationViewSet(EnvelopeMixin, ModelViewSet):
|
|
95
|
+
pagination_class = CursorPagination
|
|
96
|
+
ordering = '-created_at'
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TotalsView(EnvelopeMixin, APIView):
|
|
100
|
+
def get(self, request):
|
|
101
|
+
return success_response({'total': 12}, message=_('Totals.'))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`EnvelopeMixin` wraps what a view answers once, in `finalize_response`, so
|
|
105
|
+
no action - `list`, `create`, a custom `@action` - has to remember. A delete
|
|
106
|
+
answers 200 with the envelope rather than a 204 with nothing. A file, a
|
|
107
|
+
stream, a redirect or a CSV is left alone, and so is a view with
|
|
108
|
+
`parcel_wrap = False`.
|
|
109
|
+
|
|
110
|
+
A shop that uses every part of it, to run and read, is in
|
|
111
|
+
[`example/`](example/README.md).
|
|
112
|
+
|
|
113
|
+
## Errors
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from drf_parcel.exceptions import DomainError
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ProvinceInUse(DomainError):
|
|
120
|
+
status_code = 409
|
|
121
|
+
default_code = 'province_in_use'
|
|
122
|
+
default_detail = _('This province has branches and cannot be deleted.')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
raise ProvinceInUse(data={'branches': 3})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A `DomainError` is DRF's `APIException` with `data` added, so a project's
|
|
129
|
+
existing errors answer in the envelope unchanged. The handler also:
|
|
130
|
+
|
|
131
|
+
* says a validation error's first problem in `message` and every problem in
|
|
132
|
+
`data.fields`, each with its code at a flat path - `items.0.quantity`;
|
|
133
|
+
* answers DRF's and Django's own failures with a standard code -
|
|
134
|
+
`not_authenticated`, `permission_denied`, `not_found`, `rate_limited` with
|
|
135
|
+
`retry_after` ... - and keeps the headers DRF puts on them;
|
|
136
|
+
* answers anything unexpected with a generic 500 whose text never leaves the
|
|
137
|
+
server: it is logged with its traceback and handed to your `REPORTER`.
|
|
138
|
+
Under `DEBUG` it is raised, so Django's page shows it;
|
|
139
|
+
* undoes the request's write behind every failure it answers.
|
|
140
|
+
|
|
141
|
+
## Pagination
|
|
142
|
+
|
|
143
|
+
Numbered pages or a cursor, chosen per view. `pagination.type` says which, so
|
|
144
|
+
one component on a client reads either. A client chooses the page size only
|
|
145
|
+
where a view allows it, and always under a ceiling.
|
|
146
|
+
|
|
147
|
+
## The schema
|
|
148
|
+
|
|
149
|
+
With `ParcelAutoSchema` - or `ParcelSchemaMixin` in your own `AutoSchema` -
|
|
150
|
+
the OpenAPI document says what the wire carries: every success inside the
|
|
151
|
+
envelope, each page as its own envelope, `page_size` only where it is
|
|
152
|
+
accepted, and the error envelope as every operation's `default` answer.
|
|
153
|
+
|
|
154
|
+
## Settings
|
|
155
|
+
|
|
156
|
+
One dictionary, `DRF_PARCEL`. A key the library does not know stops the
|
|
157
|
+
project at start-up rather than being ignored, and `manage.py check` reports
|
|
158
|
+
a value that cannot work. Every key, every shape and every error code is in
|
|
159
|
+
[the contract](docs/contract.md).
|
|
160
|
+
|
|
161
|
+
## Languages
|
|
162
|
+
|
|
163
|
+
Every sentence the library says is in the request's language. It ships
|
|
164
|
+
Persian; DRF's own messages come in DRF's languages.
|
|
165
|
+
|
|
166
|
+
## Supported
|
|
167
|
+
|
|
168
|
+
Python 3.11 to 3.14; Django 5.2, 6.0 and 6.1; Django REST framework 3.16 and
|
|
169
|
+
later; drf-spectacular 0.28 and later, for the schema.
|
|
170
|
+
|
|
171
|
+
## Working on it
|
|
172
|
+
|
|
173
|
+
Every check runs through [nox](https://nox.thea.codes), the same sessions
|
|
174
|
+
locally and in CI.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv tool install nox
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
nox -s lint types tests
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`tests` runs every supported Python and Django; `oldest` runs the oldest
|
|
185
|
+
release of everything supported; `package` builds the wheel and checks what
|
|
186
|
+
is in it. The suite runs on SQLite by default, and on PostgreSQL with
|
|
187
|
+
`DRF_PARCEL_TEST_DATABASE=postgres` and the standard `PG*` variables.
|
|
188
|
+
|
|
189
|
+
After changing a marked string, translate it in
|
|
190
|
+
`src/drf_parcel/locale/fa/LC_MESSAGES/django.po` and compile it:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
nox -s messages
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Licence
|
|
197
|
+
|
|
198
|
+
BSD 3-Clause - see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# drf-parcel
|
|
2
|
+
|
|
3
|
+
یک شکل پاسخ برای API ساختهشده با Django REST framework - برای پاسخ موفق،
|
|
4
|
+
صفحه و خطا یکسان - و اسکیمایی که دقیقاً همان شکل را میگوید.
|
|
5
|
+
|
|
6
|
+
[English](https://github.com/amirAI-dot/drf-parcel/blob/main/README.md)
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{"success": true, "status_code": 200, "message": "Done.", "data": {"id": 7}}
|
|
10
|
+
{"success": true, "status_code": 200, "message": "Done.", "data": [], "pagination": {"type": "page", "count": 45, "page": 2, "pages": 3, "page_size": 20, "has_next": true, "has_previous": true}}
|
|
11
|
+
{"success": false, "status_code": 409, "message": "...", "data": {"branches": 3}, "error_code": "province_in_use"}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
کلاینت فقط یک شکل را میخواند و هیچ پاسخی را جداگانه مدیریت نمیکند: جواب
|
|
15
|
+
زیر `data` است، انسان `message` را میخواند، و کد بر اساس `error_code` تصمیم
|
|
16
|
+
میگیرد - هرگز بر اساس پیام، که ترجمه میشود.
|
|
17
|
+
|
|
18
|
+
**[مستندات](https://amirai-dot.github.io/drf-parcel/fa/)** - شروع سریع، آموزش،
|
|
19
|
+
راهنماهای کاربردی، مرجع - به انگلیسی و فارسی.
|
|
20
|
+
|
|
21
|
+
## در یک پروژه
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install 'drf-parcel[schema]' # the schema extra brings drf-spectacular
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
INSTALLED_APPS = [
|
|
29
|
+
...,
|
|
30
|
+
'rest_framework',
|
|
31
|
+
'drf_parcel',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
REST_FRAMEWORK = {
|
|
35
|
+
'EXCEPTION_HANDLER': 'drf_parcel.handlers.exception_handler',
|
|
36
|
+
'DEFAULT_PAGINATION_CLASS': 'drf_parcel.pagination.PageNumberPagination',
|
|
37
|
+
'DEFAULT_SCHEMA_CLASS': 'drf_parcel.schema.ParcelAutoSchema',
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
هر خط مستقل است: handler بهتنهایی فقط قالب خطا را میدهد و نه چیز دیگری، و
|
|
42
|
+
پروژه بقیه را view به view به کار میگیرد.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from drf_parcel.pagination import CursorPagination
|
|
46
|
+
from drf_parcel.responses import EnvelopeMixin, success_response
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ProductViewSet(EnvelopeMixin, ModelViewSet):
|
|
50
|
+
queryset = Product.objects.all()
|
|
51
|
+
serializer_class = ProductSerializer
|
|
52
|
+
page_size = 12
|
|
53
|
+
client_page_size = True # the client may send ?page_size=
|
|
54
|
+
max_page_size = 48 # and never gets more
|
|
55
|
+
envelope_messages = {'create': _('The product was added.')}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NotificationViewSet(EnvelopeMixin, ModelViewSet):
|
|
59
|
+
pagination_class = CursorPagination
|
|
60
|
+
ordering = '-created_at'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TotalsView(EnvelopeMixin, APIView):
|
|
64
|
+
def get(self, request):
|
|
65
|
+
return success_response({'total': 12}, message=_('Totals.'))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`EnvelopeMixin` پاسخ view را یک بار، در `finalize_response`، در قالب
|
|
69
|
+
میگذارد، پس هیچ اکشنی - `list`، `create`، یک `@action` سفارشی - لازم نیست
|
|
70
|
+
یادش بماند. حذف به جای 204 بدون بدنه، 200 همراه با قالب جواب میدهد. فایل،
|
|
71
|
+
استریم، ریدایرکت یا CSV دست نمیخورد، و viewی هم که `parcel_wrap = False`
|
|
72
|
+
دارد.
|
|
73
|
+
|
|
74
|
+
یک فروشگاه که همهی بخشهای آن را به کار میبرد، برای اجرا کردن و خواندن، در
|
|
75
|
+
[`example/`](example/README.fa.md) است.
|
|
76
|
+
|
|
77
|
+
## خطاها
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from drf_parcel.exceptions import DomainError
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ProvinceInUse(DomainError):
|
|
84
|
+
status_code = 409
|
|
85
|
+
default_code = 'province_in_use'
|
|
86
|
+
default_detail = _('This province has branches and cannot be deleted.')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
raise ProvinceInUse(data={'branches': 3})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`DomainError` همان `APIException` خود DRF است با `data` اضافه، پس خطاهای فعلی
|
|
93
|
+
پروژه بدون تغییر در قالب جواب میدهند. handler این کارها را هم میکند:
|
|
94
|
+
|
|
95
|
+
* در خطای اعتبارسنجی، اولین مشکل را در `message` و همهی مشکلها را در
|
|
96
|
+
`data.fields` میگوید، هر کدام با کدش و با یک مسیر تخت - `items.0.quantity`؛
|
|
97
|
+
* خطاهای خود DRF و Django را با کد استاندارد جواب میدهد - `not_authenticated`،
|
|
98
|
+
`permission_denied`، `not_found`، `rate_limited` با `retry_after` و ... - و
|
|
99
|
+
هدرهایی را که DRF روی آنها میگذارد حفظ میکند؛
|
|
100
|
+
* هر خطای پیشبینینشده را با یک 500 عمومی جواب میدهد که متنش هرگز از سرور
|
|
101
|
+
بیرون نمیرود: با traceback لاگ میشود و به `REPORTER` شما داده میشود. در
|
|
102
|
+
`DEBUG` خطا بالا میرود تا صفحهی Django آن را نشان بدهد؛
|
|
103
|
+
* نوشتن درخواست را پشت هر خطایی که جواب میدهد برمیگرداند.
|
|
104
|
+
|
|
105
|
+
## صفحهبندی
|
|
106
|
+
|
|
107
|
+
صفحهبندی شمارهای یا cursor، که هر view خودش انتخاب میکند. `pagination.type`
|
|
108
|
+
میگوید کدام است، تا یک کامپوننت در کلاینت هر دو را بخواند. کلاینت فقط جایی
|
|
109
|
+
تعداد هر صفحه را انتخاب میکند که view اجازه بدهد، و همیشه زیر سقف.
|
|
110
|
+
|
|
111
|
+
## اسکیما
|
|
112
|
+
|
|
113
|
+
با `ParcelAutoSchema` - یا `ParcelSchemaMixin` در `AutoSchema` خودتان - سند
|
|
114
|
+
OpenAPI همان چیزی را میگوید که واقعاً منتقل میشود: هر پاسخ موفق داخل قالب،
|
|
115
|
+
هر صفحه بهعنوان قالب خودش، `page_size` فقط جایی که پذیرفته میشود، و قالب خطا
|
|
116
|
+
بهعنوان پاسخ `default` هر عملیات.
|
|
117
|
+
|
|
118
|
+
## تنظیمات
|
|
119
|
+
|
|
120
|
+
یک دیکشنری، `DRF_PARCEL`. کلیدی که کتابخانه نمیشناسد پروژه را هنگام start
|
|
121
|
+
متوقف میکند، نه اینکه نادیده گرفته شود، و `manage.py check` مقداری را که کار
|
|
122
|
+
نمیکند گزارش میدهد. همهی کلیدها، همهی شکلها و همهی کدهای خطا در
|
|
123
|
+
[قرارداد](docs/contract.fa.md) آمدهاند.
|
|
124
|
+
|
|
125
|
+
## زبانها
|
|
126
|
+
|
|
127
|
+
هر جملهای که کتابخانه میگوید به زبان درخواست است. فارسی همراه کتابخانه است؛
|
|
128
|
+
پیامهای خود DRF به زبانهایی است که DRF دارد.
|
|
129
|
+
|
|
130
|
+
## پشتیبانی
|
|
131
|
+
|
|
132
|
+
Python 3.11 تا 3.14؛ Django 5.2، 6.0 و 6.1؛ Django REST framework 3.16 و
|
|
133
|
+
بالاتر؛ drf-spectacular 0.28 و بالاتر، برای اسکیما.
|
|
134
|
+
|
|
135
|
+
## کار روی پروژه
|
|
136
|
+
|
|
137
|
+
همهی بررسیها با [nox](https://nox.thea.codes) اجرا میشوند؛ همان sessionها
|
|
138
|
+
هم روی سیستم خودتان و هم در CI.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uv tool install nox
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
nox -s lint types tests
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`tests` همهی نسخههای پشتیبانیشدهی Python و Django را اجرا میکند؛ `oldest`
|
|
149
|
+
قدیمیترین نسخهی همهی وابستگیها را؛ و `package` فایل wheel را میسازد و
|
|
150
|
+
بررسی میکند چه چیزی در آن است. تستها بهطور پیشفرض روی SQLite اجرا میشوند،
|
|
151
|
+
و روی PostgreSQL با `DRF_PARCEL_TEST_DATABASE=postgres` و متغیرهای استاندارد
|
|
152
|
+
`PG*`.
|
|
153
|
+
|
|
154
|
+
بعد از تغییر یک رشتهی علامتخورده، آن را در
|
|
155
|
+
`src/drf_parcel/locale/fa/LC_MESSAGES/django.po` ترجمه و کامپایل کنید:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
nox -s messages
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## لایسنس
|
|
162
|
+
|
|
163
|
+
BSD 3-Clause - فایل [`LICENSE`](LICENSE) را ببینید.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# drf-parcel
|
|
2
|
+
|
|
3
|
+
One response shape for a Django REST framework API - success, page and error
|
|
4
|
+
alike - and the generated schema saying exactly that shape.
|
|
5
|
+
|
|
6
|
+
[فارسی](https://github.com/amirAI-dot/drf-parcel/blob/main/README.fa.md)
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{"success": true, "status_code": 200, "message": "Done.", "data": {"id": 7}}
|
|
10
|
+
{"success": true, "status_code": 200, "message": "Done.", "data": [], "pagination": {"type": "page", "count": 45, "page": 2, "pages": 3, "page_size": 20, "has_next": true, "has_previous": true}}
|
|
11
|
+
{"success": false, "status_code": 409, "message": "...", "data": {"branches": 3}, "error_code": "province_in_use"}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A client reads one shape and never special-cases a payload: the answer is
|
|
15
|
+
under `data`, a person reads `message`, and code branches on `error_code` -
|
|
16
|
+
never on the message, which is translated.
|
|
17
|
+
|
|
18
|
+
**[The documentation](https://amirai-dot.github.io/drf-parcel/)** - a
|
|
19
|
+
quickstart, a tutorial, how-to guides, the reference - in English and
|
|
20
|
+
Persian.
|
|
21
|
+
|
|
22
|
+
## In a project
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install 'drf-parcel[schema]' # the schema extra brings drf-spectacular
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
INSTALLED_APPS = [
|
|
30
|
+
...,
|
|
31
|
+
'rest_framework',
|
|
32
|
+
'drf_parcel',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
REST_FRAMEWORK = {
|
|
36
|
+
'EXCEPTION_HANDLER': 'drf_parcel.handlers.exception_handler',
|
|
37
|
+
'DEFAULT_PAGINATION_CLASS': 'drf_parcel.pagination.PageNumberPagination',
|
|
38
|
+
'DEFAULT_SCHEMA_CLASS': 'drf_parcel.schema.ParcelAutoSchema',
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each line is independent: the handler alone gives the error envelope and
|
|
43
|
+
nothing else, and a project adopts the rest one view at a time.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from drf_parcel.pagination import CursorPagination
|
|
47
|
+
from drf_parcel.responses import EnvelopeMixin, success_response
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProductViewSet(EnvelopeMixin, ModelViewSet):
|
|
51
|
+
queryset = Product.objects.all()
|
|
52
|
+
serializer_class = ProductSerializer
|
|
53
|
+
page_size = 12
|
|
54
|
+
client_page_size = True # the client may send ?page_size=
|
|
55
|
+
max_page_size = 48 # and never gets more
|
|
56
|
+
envelope_messages = {'create': _('The product was added.')}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NotificationViewSet(EnvelopeMixin, ModelViewSet):
|
|
60
|
+
pagination_class = CursorPagination
|
|
61
|
+
ordering = '-created_at'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TotalsView(EnvelopeMixin, APIView):
|
|
65
|
+
def get(self, request):
|
|
66
|
+
return success_response({'total': 12}, message=_('Totals.'))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`EnvelopeMixin` wraps what a view answers once, in `finalize_response`, so
|
|
70
|
+
no action - `list`, `create`, a custom `@action` - has to remember. A delete
|
|
71
|
+
answers 200 with the envelope rather than a 204 with nothing. A file, a
|
|
72
|
+
stream, a redirect or a CSV is left alone, and so is a view with
|
|
73
|
+
`parcel_wrap = False`.
|
|
74
|
+
|
|
75
|
+
A shop that uses every part of it, to run and read, is in
|
|
76
|
+
[`example/`](example/README.md).
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from drf_parcel.exceptions import DomainError
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ProvinceInUse(DomainError):
|
|
85
|
+
status_code = 409
|
|
86
|
+
default_code = 'province_in_use'
|
|
87
|
+
default_detail = _('This province has branches and cannot be deleted.')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
raise ProvinceInUse(data={'branches': 3})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A `DomainError` is DRF's `APIException` with `data` added, so a project's
|
|
94
|
+
existing errors answer in the envelope unchanged. The handler also:
|
|
95
|
+
|
|
96
|
+
* says a validation error's first problem in `message` and every problem in
|
|
97
|
+
`data.fields`, each with its code at a flat path - `items.0.quantity`;
|
|
98
|
+
* answers DRF's and Django's own failures with a standard code -
|
|
99
|
+
`not_authenticated`, `permission_denied`, `not_found`, `rate_limited` with
|
|
100
|
+
`retry_after` ... - and keeps the headers DRF puts on them;
|
|
101
|
+
* answers anything unexpected with a generic 500 whose text never leaves the
|
|
102
|
+
server: it is logged with its traceback and handed to your `REPORTER`.
|
|
103
|
+
Under `DEBUG` it is raised, so Django's page shows it;
|
|
104
|
+
* undoes the request's write behind every failure it answers.
|
|
105
|
+
|
|
106
|
+
## Pagination
|
|
107
|
+
|
|
108
|
+
Numbered pages or a cursor, chosen per view. `pagination.type` says which, so
|
|
109
|
+
one component on a client reads either. A client chooses the page size only
|
|
110
|
+
where a view allows it, and always under a ceiling.
|
|
111
|
+
|
|
112
|
+
## The schema
|
|
113
|
+
|
|
114
|
+
With `ParcelAutoSchema` - or `ParcelSchemaMixin` in your own `AutoSchema` -
|
|
115
|
+
the OpenAPI document says what the wire carries: every success inside the
|
|
116
|
+
envelope, each page as its own envelope, `page_size` only where it is
|
|
117
|
+
accepted, and the error envelope as every operation's `default` answer.
|
|
118
|
+
|
|
119
|
+
## Settings
|
|
120
|
+
|
|
121
|
+
One dictionary, `DRF_PARCEL`. A key the library does not know stops the
|
|
122
|
+
project at start-up rather than being ignored, and `manage.py check` reports
|
|
123
|
+
a value that cannot work. Every key, every shape and every error code is in
|
|
124
|
+
[the contract](docs/contract.md).
|
|
125
|
+
|
|
126
|
+
## Languages
|
|
127
|
+
|
|
128
|
+
Every sentence the library says is in the request's language. It ships
|
|
129
|
+
Persian; DRF's own messages come in DRF's languages.
|
|
130
|
+
|
|
131
|
+
## Supported
|
|
132
|
+
|
|
133
|
+
Python 3.11 to 3.14; Django 5.2, 6.0 and 6.1; Django REST framework 3.16 and
|
|
134
|
+
later; drf-spectacular 0.28 and later, for the schema.
|
|
135
|
+
|
|
136
|
+
## Working on it
|
|
137
|
+
|
|
138
|
+
Every check runs through [nox](https://nox.thea.codes), the same sessions
|
|
139
|
+
locally and in CI.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv tool install nox
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
nox -s lint types tests
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`tests` runs every supported Python and Django; `oldest` runs the oldest
|
|
150
|
+
release of everything supported; `package` builds the wheel and checks what
|
|
151
|
+
is in it. The suite runs on SQLite by default, and on PostgreSQL with
|
|
152
|
+
`DRF_PARCEL_TEST_DATABASE=postgres` and the standard `PG*` variables.
|
|
153
|
+
|
|
154
|
+
After changing a marked string, translate it in
|
|
155
|
+
`src/drf_parcel/locale/fa/LC_MESSAGES/django.po` and compile it:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
nox -s messages
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Licence
|
|
162
|
+
|
|
163
|
+
BSD 3-Clause - see [`LICENSE`](LICENSE).
|