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.
Files changed (158) hide show
  1. drf_parcel-0.1.0/.gitignore +31 -0
  2. drf_parcel-0.1.0/CHANGELOG.fa.md +49 -0
  3. drf_parcel-0.1.0/CHANGELOG.md +48 -0
  4. drf_parcel-0.1.0/LICENSE +28 -0
  5. drf_parcel-0.1.0/PKG-INFO +198 -0
  6. drf_parcel-0.1.0/README.fa.md +163 -0
  7. drf_parcel-0.1.0/README.md +163 -0
  8. drf_parcel-0.1.0/docs/changelog.fa.md +3 -0
  9. drf_parcel-0.1.0/docs/changelog.md +3 -0
  10. drf_parcel-0.1.0/docs/contract.fa.md +540 -0
  11. drf_parcel-0.1.0/docs/contract.md +542 -0
  12. drf_parcel-0.1.0/docs/explanation/alternatives.fa.md +30 -0
  13. drf_parcel-0.1.0/docs/explanation/alternatives.md +31 -0
  14. drf_parcel-0.1.0/docs/explanation/how-an-answer-is-made.fa.md +46 -0
  15. drf_parcel-0.1.0/docs/explanation/how-an-answer-is-made.md +46 -0
  16. drf_parcel-0.1.0/docs/explanation/index.fa.md +10 -0
  17. drf_parcel-0.1.0/docs/explanation/index.md +10 -0
  18. drf_parcel-0.1.0/docs/explanation/why-one-envelope.fa.md +41 -0
  19. drf_parcel-0.1.0/docs/explanation/why-one-envelope.md +42 -0
  20. drf_parcel-0.1.0/docs/faq.fa.md +59 -0
  21. drf_parcel-0.1.0/docs/faq.md +60 -0
  22. drf_parcel-0.1.0/docs/getting-started/installation.fa.md +61 -0
  23. drf_parcel-0.1.0/docs/getting-started/installation.md +62 -0
  24. drf_parcel-0.1.0/docs/getting-started/quickstart.fa.md +106 -0
  25. drf_parcel-0.1.0/docs/getting-started/quickstart.md +107 -0
  26. drf_parcel-0.1.0/docs/hooks.py +16 -0
  27. drf_parcel-0.1.0/docs/how-to/adopt.fa.md +85 -0
  28. drf_parcel-0.1.0/docs/how-to/adopt.md +85 -0
  29. drf_parcel-0.1.0/docs/how-to/choose-a-page.fa.md +64 -0
  30. drf_parcel-0.1.0/docs/how-to/choose-a-page.md +66 -0
  31. drf_parcel-0.1.0/docs/how-to/database-constraints.fa.md +20 -0
  32. drf_parcel-0.1.0/docs/how-to/database-constraints.md +20 -0
  33. drf_parcel-0.1.0/docs/how-to/frontend.fa.md +77 -0
  34. drf_parcel-0.1.0/docs/how-to/frontend.md +79 -0
  35. drf_parcel-0.1.0/docs/how-to/index.fa.md +17 -0
  36. drf_parcel-0.1.0/docs/how-to/index.md +17 -0
  37. drf_parcel-0.1.0/docs/how-to/leave-a-view-out.fa.md +34 -0
  38. drf_parcel-0.1.0/docs/how-to/leave-a-view-out.md +33 -0
  39. drf_parcel-0.1.0/docs/how-to/own-errors.fa.md +71 -0
  40. drf_parcel-0.1.0/docs/how-to/own-errors.md +69 -0
  41. drf_parcel-0.1.0/docs/how-to/own-schema-or-handler.fa.md +45 -0
  42. drf_parcel-0.1.0/docs/how-to/own-schema-or-handler.md +46 -0
  43. drf_parcel-0.1.0/docs/how-to/rename-error-codes.fa.md +33 -0
  44. drf_parcel-0.1.0/docs/how-to/rename-error-codes.md +33 -0
  45. drf_parcel-0.1.0/docs/how-to/report-bugs.fa.md +40 -0
  46. drf_parcel-0.1.0/docs/how-to/report-bugs.md +40 -0
  47. drf_parcel-0.1.0/docs/how-to/testing.fa.md +59 -0
  48. drf_parcel-0.1.0/docs/how-to/testing.md +58 -0
  49. drf_parcel-0.1.0/docs/index.fa.md +69 -0
  50. drf_parcel-0.1.0/docs/index.md +68 -0
  51. drf_parcel-0.1.0/docs/project/contributing.fa.md +39 -0
  52. drf_parcel-0.1.0/docs/project/contributing.md +39 -0
  53. drf_parcel-0.1.0/docs/project/security.fa.md +21 -0
  54. drf_parcel-0.1.0/docs/project/security.md +21 -0
  55. drf_parcel-0.1.0/docs/project/versions.fa.md +22 -0
  56. drf_parcel-0.1.0/docs/project/versions.md +23 -0
  57. drf_parcel-0.1.0/docs/reference/api/envelope.fa.md +5 -0
  58. drf_parcel-0.1.0/docs/reference/api/envelope.md +3 -0
  59. drf_parcel-0.1.0/docs/reference/api/exceptions.fa.md +5 -0
  60. drf_parcel-0.1.0/docs/reference/api/exceptions.md +3 -0
  61. drf_parcel-0.1.0/docs/reference/api/handlers.fa.md +5 -0
  62. drf_parcel-0.1.0/docs/reference/api/handlers.md +3 -0
  63. drf_parcel-0.1.0/docs/reference/api/pagination.fa.md +5 -0
  64. drf_parcel-0.1.0/docs/reference/api/pagination.md +3 -0
  65. drf_parcel-0.1.0/docs/reference/api/responses.fa.md +5 -0
  66. drf_parcel-0.1.0/docs/reference/api/responses.md +3 -0
  67. drf_parcel-0.1.0/docs/reference/api/schema.fa.md +5 -0
  68. drf_parcel-0.1.0/docs/reference/api/schema.md +3 -0
  69. drf_parcel-0.1.0/docs/reference/checks.fa.md +21 -0
  70. drf_parcel-0.1.0/docs/reference/checks.md +21 -0
  71. drf_parcel-0.1.0/docs/reference/error-codes.fa.md +39 -0
  72. drf_parcel-0.1.0/docs/reference/error-codes.md +38 -0
  73. drf_parcel-0.1.0/docs/reference/index.fa.md +11 -0
  74. drf_parcel-0.1.0/docs/reference/index.md +11 -0
  75. drf_parcel-0.1.0/docs/reference/settings.fa.md +73 -0
  76. drf_parcel-0.1.0/docs/reference/settings.md +73 -0
  77. drf_parcel-0.1.0/docs/stylesheets/extra.css +20 -0
  78. drf_parcel-0.1.0/docs/tutorial/01-the-envelope.fa.md +59 -0
  79. drf_parcel-0.1.0/docs/tutorial/01-the-envelope.md +61 -0
  80. drf_parcel-0.1.0/docs/tutorial/02-pages.fa.md +66 -0
  81. drf_parcel-0.1.0/docs/tutorial/02-pages.md +66 -0
  82. drf_parcel-0.1.0/docs/tutorial/03-errors.fa.md +86 -0
  83. drf_parcel-0.1.0/docs/tutorial/03-errors.md +83 -0
  84. drf_parcel-0.1.0/docs/tutorial/04-schema-and-languages.fa.md +45 -0
  85. drf_parcel-0.1.0/docs/tutorial/04-schema-and-languages.md +44 -0
  86. drf_parcel-0.1.0/docs/tutorial/index.fa.md +30 -0
  87. drf_parcel-0.1.0/docs/tutorial/index.md +29 -0
  88. drf_parcel-0.1.0/example/README.fa.md +92 -0
  89. drf_parcel-0.1.0/example/README.md +93 -0
  90. drf_parcel-0.1.0/example/config/__init__.py +0 -0
  91. drf_parcel-0.1.0/example/config/settings.py +85 -0
  92. drf_parcel-0.1.0/example/config/urls.py +9 -0
  93. drf_parcel-0.1.0/example/manage.py +11 -0
  94. drf_parcel-0.1.0/example/shop/__init__.py +0 -0
  95. drf_parcel-0.1.0/example/shop/api.py +120 -0
  96. drf_parcel-0.1.0/example/shop/apps.py +5 -0
  97. drf_parcel-0.1.0/example/shop/errors.py +42 -0
  98. drf_parcel-0.1.0/example/shop/management/__init__.py +0 -0
  99. drf_parcel-0.1.0/example/shop/management/commands/__init__.py +0 -0
  100. drf_parcel-0.1.0/example/shop/management/commands/example_data.py +32 -0
  101. drf_parcel-0.1.0/example/shop/migrations/0001_initial.py +61 -0
  102. drf_parcel-0.1.0/example/shop/migrations/__init__.py +0 -0
  103. drf_parcel-0.1.0/example/shop/models.py +32 -0
  104. drf_parcel-0.1.0/example/shop/reporting.py +16 -0
  105. drf_parcel-0.1.0/example/shop/services.py +37 -0
  106. drf_parcel-0.1.0/example/shop/tests.py +187 -0
  107. drf_parcel-0.1.0/example/shop/urls.py +13 -0
  108. drf_parcel-0.1.0/pyproject.toml +137 -0
  109. drf_parcel-0.1.0/src/drf_parcel/__init__.py +0 -0
  110. drf_parcel-0.1.0/src/drf_parcel/apps.py +15 -0
  111. drf_parcel-0.1.0/src/drf_parcel/checks.py +167 -0
  112. drf_parcel-0.1.0/src/drf_parcel/conf.py +106 -0
  113. drf_parcel-0.1.0/src/drf_parcel/envelope.py +99 -0
  114. drf_parcel-0.1.0/src/drf_parcel/exceptions.py +109 -0
  115. drf_parcel-0.1.0/src/drf_parcel/handlers.py +270 -0
  116. drf_parcel-0.1.0/src/drf_parcel/integrity.py +75 -0
  117. drf_parcel-0.1.0/src/drf_parcel/locale/fa/LC_MESSAGES/django.mo +0 -0
  118. drf_parcel-0.1.0/src/drf_parcel/locale/fa/LC_MESSAGES/django.po +79 -0
  119. drf_parcel-0.1.0/src/drf_parcel/messages.py +54 -0
  120. drf_parcel-0.1.0/src/drf_parcel/pagination.py +271 -0
  121. drf_parcel-0.1.0/src/drf_parcel/py.typed +0 -0
  122. drf_parcel-0.1.0/src/drf_parcel/responses.py +196 -0
  123. drf_parcel-0.1.0/src/drf_parcel/schema.py +244 -0
  124. drf_parcel-0.1.0/tests/__init__.py +0 -0
  125. drf_parcel-0.1.0/tests/conftest.py +31 -0
  126. drf_parcel-0.1.0/tests/project/__init__.py +0 -0
  127. drf_parcel-0.1.0/tests/project/documented.py +122 -0
  128. drf_parcel-0.1.0/tests/project/failures.py +154 -0
  129. drf_parcel-0.1.0/tests/project/handlers.py +15 -0
  130. drf_parcel-0.1.0/tests/project/settings.py +69 -0
  131. drf_parcel-0.1.0/tests/project/shelf/__init__.py +0 -0
  132. drf_parcel-0.1.0/tests/project/shelf/models.py +23 -0
  133. drf_parcel-0.1.0/tests/project/shelf/views.py +87 -0
  134. drf_parcel-0.1.0/tests/project/urls.py +36 -0
  135. drf_parcel-0.1.0/tests/project/views.py +175 -0
  136. drf_parcel-0.1.0/tests/test_a_bug_never_tells_its_secrets.py +103 -0
  137. drf_parcel-0.1.0/tests/test_a_constraint_the_database_refuses.py +110 -0
  138. drf_parcel-0.1.0/tests/test_a_delete_can_be_read.py +47 -0
  139. drf_parcel-0.1.0/tests/test_a_failure_undoes_its_write.py +25 -0
  140. drf_parcel-0.1.0/tests/test_a_form_learns_every_field_that_is_wrong.py +115 -0
  141. drf_parcel-0.1.0/tests/test_a_list_comes_a_page_at_a_time.py +95 -0
  142. drf_parcel-0.1.0/tests/test_a_misconfigured_setting_is_caught_at_start.py +140 -0
  143. drf_parcel-0.1.0/tests/test_a_misspelt_setting_stops_the_start.py +46 -0
  144. drf_parcel-0.1.0/tests/test_a_page_never_holds_more_than_the_ceiling.py +100 -0
  145. drf_parcel-0.1.0/tests/test_a_projects_own_error_answers_in_the_envelope.py +55 -0
  146. drf_parcel-0.1.0/tests/test_a_success_says_what_happened.py +70 -0
  147. drf_parcel-0.1.0/tests/test_a_view_keeps_its_headers_and_cookies.py +20 -0
  148. drf_parcel-0.1.0/tests/test_a_view_without_the_mixin.py +25 -0
  149. drf_parcel-0.1.0/tests/test_an_answer_is_wrapped_once.py +44 -0
  150. drf_parcel-0.1.0/tests/test_an_endless_feed_is_read_by_cursor.py +101 -0
  151. drf_parcel-0.1.0/tests/test_errors_work_without_the_librarys_handler.py +17 -0
  152. drf_parcel-0.1.0/tests/test_every_answer_has_the_same_top_level.py +56 -0
  153. drf_parcel-0.1.0/tests/test_every_failure_has_its_code.py +138 -0
  154. drf_parcel-0.1.0/tests/test_the_envelope_holds_its_own_rules.py +40 -0
  155. drf_parcel-0.1.0/tests/test_the_library_speaks_persian.py +87 -0
  156. drf_parcel-0.1.0/tests/test_the_schema_is_an_extra.py +41 -0
  157. drf_parcel-0.1.0/tests/test_the_schema_says_what_the_wire_carries.py +238 -0
  158. 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.
@@ -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).