django-spec-rest 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 (277) hide show
  1. django_spec_rest-0.1.0/.github/CODEOWNERS +1 -0
  2. django_spec_rest-0.1.0/.github/SECURITY.md +9 -0
  3. django_spec_rest-0.1.0/.github/dependabot.yml +26 -0
  4. django_spec_rest-0.1.0/.github/workflows/release.yml +112 -0
  5. django_spec_rest-0.1.0/.github/workflows/tests.yml +485 -0
  6. django_spec_rest-0.1.0/.github/workflows/upstream-drift.yml +149 -0
  7. django_spec_rest-0.1.0/.gitignore +636 -0
  8. django_spec_rest-0.1.0/.pre-commit-config.yaml +80 -0
  9. django_spec_rest-0.1.0/CHANGELOG.md +112 -0
  10. django_spec_rest-0.1.0/CLAUDE.md +282 -0
  11. django_spec_rest-0.1.0/LICENSE +21 -0
  12. django_spec_rest-0.1.0/Makefile +86 -0
  13. django_spec_rest-0.1.0/PKG-INFO +200 -0
  14. django_spec_rest-0.1.0/README.md +158 -0
  15. django_spec_rest-0.1.0/django_spec_rest/__init__.py +79 -0
  16. django_spec_rest-0.1.0/django_spec_rest/dispatch/__init__.py +6 -0
  17. django_spec_rest-0.1.0/django_spec_rest/dispatch/kernel_edge.py +599 -0
  18. django_spec_rest-0.1.0/django_spec_rest/dispatch/kernel_refusals.py +141 -0
  19. django_spec_rest-0.1.0/django_spec_rest/dispatch/utils.py +28 -0
  20. django_spec_rest-0.1.0/django_spec_rest/openapi/__init__.py +30 -0
  21. django_spec_rest-0.1.0/django_spec_rest/openapi/_resolve.py +78 -0
  22. django_spec_rest-0.1.0/django_spec_rest/openapi/_to_serializer.py +40 -0
  23. django_spec_rest-0.1.0/django_spec_rest/openapi/enable_openapi.py +59 -0
  24. django_spec_rest-0.1.0/django_spec_rest/openapi/service_auto_schema.py +340 -0
  25. django_spec_rest-0.1.0/django_spec_rest/openapi/service_error_serializer.py +17 -0
  26. django_spec_rest-0.1.0/django_spec_rest/openapi/utils.py +39 -0
  27. django_spec_rest-0.1.0/django_spec_rest/py.typed +0 -0
  28. django_spec_rest-0.1.0/django_spec_rest/selectors/__init__.py +1 -0
  29. django_spec_rest-0.1.0/django_spec_rest/selectors/utils.py +254 -0
  30. django_spec_rest-0.1.0/django_spec_rest/service_specs/__init__.py +8 -0
  31. django_spec_rest-0.1.0/django_spec_rest/service_specs/base_serializer_context.py +52 -0
  32. django_spec_rest-0.1.0/django_spec_rest/service_specs/build_offline_context.py +204 -0
  33. django_spec_rest-0.1.0/django_spec_rest/service_specs/drf_invalid_arguments.py +60 -0
  34. django_spec_rest-0.1.0/django_spec_rest/service_specs/drf_not_authenticated.py +37 -0
  35. django_spec_rest-0.1.0/django_spec_rest/service_specs/drf_not_permitted.py +43 -0
  36. django_spec_rest-0.1.0/django_spec_rest/service_specs/drf_permission_check.py +210 -0
  37. django_spec_rest-0.1.0/django_spec_rest/service_specs/drf_principal_unavailable.py +45 -0
  38. django_spec_rest-0.1.0/django_spec_rest/service_specs/drf_service_validation_error.py +45 -0
  39. django_spec_rest-0.1.0/django_spec_rest/service_specs/kernel_spec_variant.py +142 -0
  40. django_spec_rest-0.1.0/django_spec_rest/service_specs/recording_spec.py +306 -0
  41. django_spec_rest-0.1.0/django_spec_rest/service_specs/register_drf_row_errors.py +38 -0
  42. django_spec_rest-0.1.0/django_spec_rest/service_specs/renderable_serializer_class.py +89 -0
  43. django_spec_rest-0.1.0/django_spec_rest/service_specs/seed_named_fields.py +25 -0
  44. django_spec_rest-0.1.0/django_spec_rest/service_specs/serializer_presenter.py +491 -0
  45. django_spec_rest-0.1.0/django_spec_rest/service_specs/serializer_validator.py +678 -0
  46. django_spec_rest-0.1.0/django_spec_rest/service_specs/translate_dispatch_errors.py +91 -0
  47. django_spec_rest-0.1.0/django_spec_rest/service_specs/translate_drf_refusals.py +125 -0
  48. django_spec_rest-0.1.0/django_spec_rest/service_specs/translating_drf_refusals.py +83 -0
  49. django_spec_rest-0.1.0/django_spec_rest/service_specs/utils.py +1318 -0
  50. django_spec_rest-0.1.0/django_spec_rest/types/__init__.py +29 -0
  51. django_spec_rest-0.1.0/django_spec_rest/types/field_marking.py +28 -0
  52. django_spec_rest-0.1.0/django_spec_rest/types/http_extras.py +49 -0
  53. django_spec_rest-0.1.0/django_spec_rest/types/json_schema_registry.py +57 -0
  54. django_spec_rest-0.1.0/django_spec_rest/types/offline_context.py +36 -0
  55. django_spec_rest-0.1.0/django_spec_rest/types/offline_http_request.py +92 -0
  56. django_spec_rest-0.1.0/django_spec_rest/types/offline_service_view.py +32 -0
  57. django_spec_rest-0.1.0/django_spec_rest/types/service_view.py +31 -0
  58. django_spec_rest-0.1.0/django_spec_rest/types/view_hooks.py +66 -0
  59. django_spec_rest-0.1.0/django_spec_rest/version.py +5 -0
  60. django_spec_rest-0.1.0/django_spec_rest/views/__init__.py +24 -0
  61. django_spec_rest-0.1.0/django_spec_rest/views/mutation/__init__.py +11 -0
  62. django_spec_rest-0.1.0/django_spec_rest/views/mutation/apply_response_finalizer.py +50 -0
  63. django_spec_rest-0.1.0/django_spec_rest/views/mutation/kernel_input_serializer_class.py +74 -0
  64. django_spec_rest-0.1.0/django_spec_rest/views/mutation/map_service_error.py +111 -0
  65. django_spec_rest-0.1.0/django_spec_rest/views/mutation/mutation_flow_mixin.py +108 -0
  66. django_spec_rest-0.1.0/django_spec_rest/views/mutation/resolve_success_status.py +37 -0
  67. django_spec_rest-0.1.0/django_spec_rest/views/mutation/service_create_view.py +58 -0
  68. django_spec_rest-0.1.0/django_spec_rest/views/mutation/service_delete_view.py +50 -0
  69. django_spec_rest-0.1.0/django_spec_rest/views/mutation/service_update_view.py +74 -0
  70. django_spec_rest-0.1.0/django_spec_rest/views/mutation/utils.py +294 -0
  71. django_spec_rest-0.1.0/django_spec_rest/views/query/__init__.py +11 -0
  72. django_spec_rest-0.1.0/django_spec_rest/views/query/selector_list_view.py +59 -0
  73. django_spec_rest-0.1.0/django_spec_rest/views/query/selector_retrieve_view.py +66 -0
  74. django_spec_rest-0.1.0/django_spec_rest/views/spec_options.py +157 -0
  75. django_spec_rest-0.1.0/django_spec_rest/views/spec_validation.py +451 -0
  76. django_spec_rest-0.1.0/django_spec_rest/views/utils.py +417 -0
  77. django_spec_rest-0.1.0/django_spec_rest/viewsets/__init__.py +28 -0
  78. django_spec_rest-0.1.0/django_spec_rest/viewsets/action_serializer_resolver.py +49 -0
  79. django_spec_rest-0.1.0/django_spec_rest/viewsets/decorators/__init__.py +9 -0
  80. django_spec_rest-0.1.0/django_spec_rest/viewsets/decorators/selector_action.py +101 -0
  81. django_spec_rest-0.1.0/django_spec_rest/viewsets/decorators/service_action.py +173 -0
  82. django_spec_rest-0.1.0/django_spec_rest/viewsets/selector_list_mixin.py +52 -0
  83. django_spec_rest-0.1.0/django_spec_rest/viewsets/selector_retrieve_mixin.py +61 -0
  84. django_spec_rest-0.1.0/django_spec_rest/viewsets/selector_viewset.py +29 -0
  85. django_spec_rest-0.1.0/django_spec_rest/viewsets/service_create_mixin.py +37 -0
  86. django_spec_rest-0.1.0/django_spec_rest/viewsets/service_destroy_mixin.py +41 -0
  87. django_spec_rest-0.1.0/django_spec_rest/viewsets/service_update_mixin.py +59 -0
  88. django_spec_rest-0.1.0/django_spec_rest/viewsets/service_viewset.py +38 -0
  89. django_spec_rest-0.1.0/django_spec_rest/viewsets/utils.py +307 -0
  90. django_spec_rest-0.1.0/docs/concepts.md +204 -0
  91. django_spec_rest-0.1.0/docs/index.md +63 -0
  92. django_spec_rest-0.1.0/docs/moving-from-drfs.md +284 -0
  93. django_spec_rest-0.1.0/docs/openapi.md +60 -0
  94. django_spec_rest-0.1.0/docs/quickstart.md +162 -0
  95. django_spec_rest-0.1.0/docs/recipes/extra-kwargs.md +77 -0
  96. django_spec_rest-0.1.0/docs/recipes/index.md +21 -0
  97. django_spec_rest-0.1.0/docs/recipes/off-http-inputs.md +57 -0
  98. django_spec_rest-0.1.0/docs/recipes/permissions.md +71 -0
  99. django_spec_rest-0.1.0/docs/recipes/selector-filtering.md +74 -0
  100. django_spec_rest-0.1.0/docs/recipes/serializer-context.md +90 -0
  101. django_spec_rest-0.1.0/docs/recipes/service-action.md +105 -0
  102. django_spec_rest-0.1.0/docs/reference/index.md +39 -0
  103. django_spec_rest-0.1.0/docs/reference/openapi.md +7 -0
  104. django_spec_rest-0.1.0/docs/reference/service-specs.md +57 -0
  105. django_spec_rest-0.1.0/docs/reference/types.md +30 -0
  106. django_spec_rest-0.1.0/docs/reference/views.md +26 -0
  107. django_spec_rest-0.1.0/docs/reference/viewsets.md +29 -0
  108. django_spec_rest-0.1.0/examples/README.md +76 -0
  109. django_spec_rest-0.1.0/examples/invoices/__init__.py +0 -0
  110. django_spec_rest-0.1.0/examples/invoices/apps.py +10 -0
  111. django_spec_rest-0.1.0/examples/invoices/migrations/0001_initial.py +47 -0
  112. django_spec_rest-0.1.0/examples/invoices/migrations/__init__.py +0 -0
  113. django_spec_rest-0.1.0/examples/invoices/models/__init__.py +29 -0
  114. django_spec_rest-0.1.0/examples/invoices/selectors/__init__.py +32 -0
  115. django_spec_rest-0.1.0/examples/invoices/serializers/__init__.py +40 -0
  116. django_spec_rest-0.1.0/examples/invoices/services/__init__.py +32 -0
  117. django_spec_rest-0.1.0/examples/invoices/tests/__init__.py +245 -0
  118. django_spec_rest-0.1.0/examples/invoices/urls.py +16 -0
  119. django_spec_rest-0.1.0/examples/invoices/views/__init__.py +118 -0
  120. django_spec_rest-0.1.0/examples/manage.py +18 -0
  121. django_spec_rest-0.1.0/examples/project/__init__.py +0 -0
  122. django_spec_rest-0.1.0/examples/project/settings.py +43 -0
  123. django_spec_rest-0.1.0/examples/project/urls.py +9 -0
  124. django_spec_rest-0.1.0/mkdocs.yml +107 -0
  125. django_spec_rest-0.1.0/pyproject.toml +243 -0
  126. django_spec_rest-0.1.0/scripts/check-changelog +148 -0
  127. django_spec_rest-0.1.0/scripts/release-publish.sh +246 -0
  128. django_spec_rest-0.1.0/tests/__init__.py +0 -0
  129. django_spec_rest-0.1.0/tests/conftest.py +1 -0
  130. django_spec_rest-0.1.0/tests/conftest_settings.py +42 -0
  131. django_spec_rest-0.1.0/tests/openapi/__init__.py +0 -0
  132. django_spec_rest-0.1.0/tests/openapi/test_enable_openapi.py +50 -0
  133. django_spec_rest-0.1.0/tests/openapi/test_resolve.py +112 -0
  134. django_spec_rest-0.1.0/tests/openapi/test_service_auto_schema.py +692 -0
  135. django_spec_rest-0.1.0/tests/openapi/test_to_serializer.py +45 -0
  136. django_spec_rest-0.1.0/tests/openapi/test_utils.py +53 -0
  137. django_spec_rest-0.1.0/tests/selectors/__init__.py +0 -0
  138. django_spec_rest-0.1.0/tests/selectors/test_kernel_query.py +122 -0
  139. django_spec_rest-0.1.0/tests/selectors/test_query_protocols.py +48 -0
  140. django_spec_rest-0.1.0/tests/selectors/test_queryset_shaping.py +214 -0
  141. django_spec_rest-0.1.0/tests/service_specs/__init__.py +0 -0
  142. django_spec_rest-0.1.0/tests/service_specs/served_status.py +31 -0
  143. django_spec_rest-0.1.0/tests/service_specs/test_a_presenter_s_refusal.py +201 -0
  144. django_spec_rest-0.1.0/tests/service_specs/test_a_provider_naming_a_field_twice.py +213 -0
  145. django_spec_rest-0.1.0/tests/service_specs/test_a_read_named_like_a_field.py +59 -0
  146. django_spec_rest-0.1.0/tests/service_specs/test_a_request_key_the_client_sends.py +160 -0
  147. django_spec_rest-0.1.0/tests/service_specs/test_a_request_scoped_filter_set_is_handed_the_request.py +79 -0
  148. django_spec_rest-0.1.0/tests/service_specs/test_a_seat_s_registry_is_checked_off_http.py +89 -0
  149. django_spec_rest-0.1.0/tests/service_specs/test_a_serializer_key_no_field_declares.py +216 -0
  150. django_spec_rest-0.1.0/tests/service_specs/test_a_serializer_presenter_renders_off_the_loop.py +116 -0
  151. django_spec_rest-0.1.0/tests/service_specs/test_a_service_presenting_its_return_as_rows.py +91 -0
  152. django_spec_rest-0.1.0/tests/service_specs/test_an_anonymous_client.py +73 -0
  153. django_spec_rest-0.1.0/tests/service_specs/test_base_serializer_context.py +121 -0
  154. django_spec_rest-0.1.0/tests/service_specs/test_build_offline_context.py +223 -0
  155. django_spec_rest-0.1.0/tests/service_specs/test_call_base_context.py +122 -0
  156. django_spec_rest-0.1.0/tests/service_specs/test_context_answer.py +52 -0
  157. django_spec_rest-0.1.0/tests/service_specs/test_drf_invalid_arguments.py +106 -0
  158. django_spec_rest-0.1.0/tests/service_specs/test_drf_not_authenticated.py +60 -0
  159. django_spec_rest-0.1.0/tests/service_specs/test_drf_not_permitted.py +75 -0
  160. django_spec_rest-0.1.0/tests/service_specs/test_drf_permission_check.py +498 -0
  161. django_spec_rest-0.1.0/tests/service_specs/test_drf_principal_unavailable.py +74 -0
  162. django_spec_rest-0.1.0/tests/service_specs/test_drf_refusals_on_the_kernel_route.py +447 -0
  163. django_spec_rest-0.1.0/tests/service_specs/test_drf_service_validation_error.py +64 -0
  164. django_spec_rest-0.1.0/tests/service_specs/test_edge_machinery_stays_out_of_the_pool.py +170 -0
  165. django_spec_rest-0.1.0/tests/service_specs/test_enforce_permissions.py +113 -0
  166. django_spec_rest-0.1.0/tests/service_specs/test_kernel_spec_variant.py +345 -0
  167. django_spec_rest-0.1.0/tests/service_specs/test_register_drf_row_errors.py +92 -0
  168. django_spec_rest-0.1.0/tests/service_specs/test_registered_fragments.py +407 -0
  169. django_spec_rest-0.1.0/tests/service_specs/test_registration_needs_no_database.py +174 -0
  170. django_spec_rest-0.1.0/tests/service_specs/test_registry_through_the_compiled_spec.py +176 -0
  171. django_spec_rest-0.1.0/tests/service_specs/test_renderable_serializer_class.py +77 -0
  172. django_spec_rest-0.1.0/tests/service_specs/test_serializer_context.py +435 -0
  173. django_spec_rest-0.1.0/tests/service_specs/test_serializer_presenter.py +897 -0
  174. django_spec_rest-0.1.0/tests/service_specs/test_serializer_validator.py +716 -0
  175. django_spec_rest-0.1.0/tests/service_specs/test_serializer_validator_unsendable.py +221 -0
  176. django_spec_rest-0.1.0/tests/service_specs/test_the_call_context_over_http.py +204 -0
  177. django_spec_rest-0.1.0/tests/service_specs/test_the_call_s_context_is_every_serializer_s.py +205 -0
  178. django_spec_rest-0.1.0/tests/service_specs/test_the_input_serializer_is_built_once_per_call.py +422 -0
  179. django_spec_rest-0.1.0/tests/service_specs/test_the_serializer_context_s_layers.py +353 -0
  180. django_spec_rest-0.1.0/tests/service_specs/test_translate_dispatch_errors.py +144 -0
  181. django_spec_rest-0.1.0/tests/service_specs/test_translating_drf_refusals.py +260 -0
  182. django_spec_rest-0.1.0/tests/service_specs/test_utils.py +684 -0
  183. django_spec_rest-0.1.0/tests/service_specs/test_what_the_edge_owes_a_kernel_spec.py +220 -0
  184. django_spec_rest-0.1.0/tests/test_dispatch_surface.py +133 -0
  185. django_spec_rest-0.1.0/tests/test_import_before_setup.py +58 -0
  186. django_spec_rest-0.1.0/tests/test_import_boundaries.py +290 -0
  187. django_spec_rest-0.1.0/tests/testapp/__init__.py +0 -0
  188. django_spec_rest-0.1.0/tests/testapp/apps.py +11 -0
  189. django_spec_rest-0.1.0/tests/testapp/models.py +183 -0
  190. django_spec_rest-0.1.0/tests/testapp/serializers.py +25 -0
  191. django_spec_rest-0.1.0/tests/types/__init__.py +0 -0
  192. django_spec_rest-0.1.0/tests/types/test_http_extras.py +81 -0
  193. django_spec_rest-0.1.0/tests/types/test_offline_http_request.py +91 -0
  194. django_spec_rest-0.1.0/tests/urls.py +5 -0
  195. django_spec_rest-0.1.0/tests/views/__init__.py +0 -0
  196. django_spec_rest-0.1.0/tests/views/server_value_calls.py +90 -0
  197. django_spec_rest-0.1.0/tests/views/test_a_deactivated_principal_is_refused.py +194 -0
  198. django_spec_rest-0.1.0/tests/views/test_a_filter_named_like_an_input_field.py +84 -0
  199. django_spec_rest-0.1.0/tests/views/test_a_lookup_read_named_like_a_field.py +153 -0
  200. django_spec_rest-0.1.0/tests/views/test_a_missing_argument_is_invalid_arguments.py +93 -0
  201. django_spec_rest-0.1.0/tests/views/test_a_nested_progress_reporter_is_refused_at_as_view.py +49 -0
  202. django_spec_rest-0.1.0/tests/views/test_a_nested_row_django_cannot_write.py +68 -0
  203. django_spec_rest-0.1.0/tests/views/test_a_parameter_named_like_an_input_needs_a_default_over_http.py +160 -0
  204. django_spec_rest-0.1.0/tests/views/test_a_route_capture_scopes_the_selector.py +51 -0
  205. django_spec_rest-0.1.0/tests/views/test_a_selectors_author_value_is_checked_against_its_annotation.py +105 -0
  206. django_spec_rest-0.1.0/tests/views/test_a_serializer_reading_its_context.py +654 -0
  207. django_spec_rest-0.1.0/tests/views/test_a_server_owned_name.py +104 -0
  208. django_spec_rest-0.1.0/tests/views/test_a_singular_relation_s_refusal_is_under_non_field_errors.py +313 -0
  209. django_spec_rest-0.1.0/tests/views/test_a_target_lookups_declared_reads.py +133 -0
  210. django_spec_rest-0.1.0/tests/views/test_a_target_lookups_typed_reads.py +159 -0
  211. django_spec_rest-0.1.0/tests/views/test_a_typed_provider_filling_a_lookup_read.py +131 -0
  212. django_spec_rest-0.1.0/tests/views/test_a_typed_provider_owning_a_wire_name.py +220 -0
  213. django_spec_rest-0.1.0/tests/views/test_a_typed_providers_key_stays_the_servers.py +183 -0
  214. django_spec_rest-0.1.0/tests/views/test_a_var_keyword_selector_reads_no_route_capture.py +123 -0
  215. django_spec_rest-0.1.0/tests/views/test_a_view_hook_reporting_progress.py +87 -0
  216. django_spec_rest-0.1.0/tests/views/test_additional_input_required_over_http.py +384 -0
  217. django_spec_rest-0.1.0/tests/views/test_affordances_over_http.py +164 -0
  218. django_spec_rest-0.1.0/tests/views/test_affordances_rendered_over_http.py +118 -0
  219. django_spec_rest-0.1.0/tests/views/test_an_undeclared_key_over_http.py +123 -0
  220. django_spec_rest-0.1.0/tests/views/test_apply_response_finalizer.py +79 -0
  221. django_spec_rest-0.1.0/tests/views/test_author_values_outrank_the_validator.py +265 -0
  222. django_spec_rest-0.1.0/tests/views/test_bulk_length_over_http.py +75 -0
  223. django_spec_rest-0.1.0/tests/views/test_bulk_mutations.py +274 -0
  224. django_spec_rest-0.1.0/tests/views/test_bulk_rows_over_http.py +211 -0
  225. django_spec_rest-0.1.0/tests/views/test_bundled_service_input.py +99 -0
  226. django_spec_rest-0.1.0/tests/views/test_callable_success_status.py +200 -0
  227. django_spec_rest-0.1.0/tests/views/test_create_model_view.py +41 -0
  228. django_spec_rest-0.1.0/tests/views/test_drf_refusals_restored_on_the_edge.py +320 -0
  229. django_spec_rest-0.1.0/tests/views/test_drf_validation_over_http.py +499 -0
  230. django_spec_rest-0.1.0/tests/views/test_errors_keyed_by_item_index.py +206 -0
  231. django_spec_rest-0.1.0/tests/views/test_hidden_inputs_over_http.py +324 -0
  232. django_spec_rest-0.1.0/tests/views/test_hook_values_for_model_fields.py +155 -0
  233. django_spec_rest-0.1.0/tests/views/test_instance_selector_views.py +199 -0
  234. django_spec_rest-0.1.0/tests/views/test_kernel_edge.py +459 -0
  235. django_spec_rest-0.1.0/tests/views/test_kernel_refusals.py +192 -0
  236. django_spec_rest-0.1.0/tests/views/test_kernel_views.py +841 -0
  237. django_spec_rest-0.1.0/tests/views/test_map_service_error.py +147 -0
  238. django_spec_rest-0.1.0/tests/views/test_permissions.py +100 -0
  239. django_spec_rest-0.1.0/tests/views/test_query_param_over_http.py +105 -0
  240. django_spec_rest-0.1.0/tests/views/test_resolve_progress_hook.py +78 -0
  241. django_spec_rest-0.1.0/tests/views/test_resolve_success_status.py +54 -0
  242. django_spec_rest-0.1.0/tests/views/test_resolved_data_context.py +265 -0
  243. django_spec_rest-0.1.0/tests/views/test_response_finalizer.py +230 -0
  244. django_spec_rest-0.1.0/tests/views/test_seed_named_validated_values.py +154 -0
  245. django_spec_rest-0.1.0/tests/views/test_selector_list_view.py +332 -0
  246. django_spec_rest-0.1.0/tests/views/test_selector_retrieve_view.py +411 -0
  247. django_spec_rest-0.1.0/tests/views/test_serializer_context.py +424 -0
  248. django_spec_rest-0.1.0/tests/views/test_serializer_context_runs_once.py +231 -0
  249. django_spec_rest-0.1.0/tests/views/test_serializer_in_pool.py +161 -0
  250. django_spec_rest-0.1.0/tests/views/test_server_value_over_a_second_route.py +540 -0
  251. django_spec_rest-0.1.0/tests/views/test_server_value_the_edge_put.py +454 -0
  252. django_spec_rest-0.1.0/tests/views/test_service_create_view.py +405 -0
  253. django_spec_rest-0.1.0/tests/views/test_service_delete_view.py +146 -0
  254. django_spec_rest-0.1.0/tests/views/test_service_update_view.py +480 -0
  255. django_spec_rest-0.1.0/tests/views/test_service_view.py +44 -0
  256. django_spec_rest-0.1.0/tests/views/test_spec_options.py +555 -0
  257. django_spec_rest-0.1.0/tests/views/test_spec_validation.py +241 -0
  258. django_spec_rest-0.1.0/tests/views/test_spec_validation_e2e.py +510 -0
  259. django_spec_rest-0.1.0/tests/views/test_the_query_string_by_the_kernel_s_rules.py +202 -0
  260. django_spec_rest-0.1.0/tests/views/test_url_kwarg_over_http.py +128 -0
  261. django_spec_rest-0.1.0/tests/views/test_utils.py +171 -0
  262. django_spec_rest-0.1.0/tests/views/test_view_call_context.py +265 -0
  263. django_spec_rest-0.1.0/tests/views/test_wire_named_errors.py +227 -0
  264. django_spec_rest-0.1.0/tests/viewsets/__init__.py +0 -0
  265. django_spec_rest-0.1.0/tests/viewsets/decorators/__init__.py +0 -0
  266. django_spec_rest-0.1.0/tests/viewsets/decorators/test_selector_action.py +187 -0
  267. django_spec_rest-0.1.0/tests/viewsets/decorators/test_service_action.py +431 -0
  268. django_spec_rest-0.1.0/tests/viewsets/test_action_serializer_resolver.py +51 -0
  269. django_spec_rest-0.1.0/tests/viewsets/test_instance_selector_spec.py +158 -0
  270. django_spec_rest-0.1.0/tests/viewsets/test_options_metadata.py +331 -0
  271. django_spec_rest-0.1.0/tests/viewsets/test_partial_update_spec.py +115 -0
  272. django_spec_rest-0.1.0/tests/viewsets/test_permissions.py +171 -0
  273. django_spec_rest-0.1.0/tests/viewsets/test_selector_retrieve_mixin.py +103 -0
  274. django_spec_rest-0.1.0/tests/viewsets/test_selector_viewset.py +167 -0
  275. django_spec_rest-0.1.0/tests/viewsets/test_service_viewset.py +518 -0
  276. django_spec_rest-0.1.0/tests/viewsets/test_utils.py +43 -0
  277. django_spec_rest-0.1.0/uv.lock +1537 -0
@@ -0,0 +1 @@
1
+ * @Artui
@@ -0,0 +1,9 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report security issues **privately** via GitHub's
6
+ [Report a vulnerability](../../security/advisories/new) button rather than opening a public issue.
7
+ I aim to acknowledge reports within 72 hours.
8
+
9
+ Supported: the latest released version on the default branch.
@@ -0,0 +1,26 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ # Dependabot already holds a new version for 3 days before proposing it; a
8
+ # week is the more useful threshold. A compromised release is typically
9
+ # yanked within days, so the gap between "published" and "found" is exactly
10
+ # the window an automatic upgrade walks into. This costs nothing in
11
+ # responsiveness that matters: cooldown covers version updates only, and
12
+ # security advisories are proposed immediately regardless.
13
+ cooldown:
14
+ default-days: 7
15
+ groups:
16
+ actions:
17
+ patterns: ["*"]
18
+ - package-ecosystem: "uv"
19
+ directory: "/"
20
+ schedule:
21
+ interval: "weekly"
22
+ cooldown:
23
+ default-days: 7
24
+ groups:
25
+ python:
26
+ patterns: ["*"]
@@ -0,0 +1,112 @@
1
+ name: release
2
+
3
+ # Main-triggered: every merge to `main` runs `release-publish-prepare`, which
4
+ # short-circuits to a no-op unless the version in source has been bumped past
5
+ # the most recent `vX.Y.Z` tag. The previous tag-trigger flow has been removed
6
+ # — `make release-bump` + merge to main is now the only path.
7
+ on:
8
+ push:
9
+ branches: [main]
10
+
11
+ concurrency:
12
+ group: release-main
13
+ cancel-in-progress: false
14
+
15
+ # Actions are pinned to a commit SHA rather than a tag: a tag is mutable, and an
16
+ # action owner (or anyone who takes over the account) can silently repoint one at
17
+ # new code that then runs with this workflow's token. The trailing version comment
18
+ # is not decoration -- Dependabot reads it to know which release the SHA is, and
19
+ # rewrites both together when a newer one ships.
20
+ jobs:
21
+ release:
22
+ name: release
23
+ runs-on: ubuntu-latest
24
+ environment:
25
+ name: pypi
26
+ url: https://pypi.org/p/django-spec-rest
27
+ permissions:
28
+ id-token: write # PyPI OIDC trusted publishing
29
+ contents: write # tag push + gh release create
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ with:
33
+ fetch-depth: 0
34
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
35
+ with:
36
+ enable-cache: true
37
+ - name: Install
38
+ run: uv sync --all-groups
39
+
40
+ - name: Prepare release (version check, test, build dist)
41
+ id: prepare
42
+ run: make release-publish-prepare
43
+
44
+ - name: Publish to PyPI
45
+ if: steps.prepare.outputs.released == 'true'
46
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
47
+ with:
48
+ # Idempotent on purpose. What gates a release is the git tag, not the
49
+ # index (see `prepare`), so re-running this job after a failure in a
50
+ # later step arrives here having already uploaded these files. Without
51
+ # this the upload is the step that makes that re-run impossible, and
52
+ # every recovery becomes manual — which is how one release lost its tag,
53
+ # its GitHub Release and its docs deploy to a single transient push
54
+ # failure, with the package already published.
55
+ # PyPI never lets a file be replaced, so skipping one cannot overwrite
56
+ # a published artefact.
57
+ skip-existing: true
58
+
59
+ - name: Tag + GitHub Release
60
+ if: steps.prepare.outputs.released == 'true'
61
+ env:
62
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63
+ run: make release-publish-finalize
64
+
65
+ # On release commits we own the single gh-pages push: embed the
66
+ # coverage badge JSON into the docs build so mkdocs gh-deploy carries
67
+ # it alongside the site, and tests.yml's coverage-badge job
68
+ # short-circuits to avoid a second push (and a second built-in
69
+ # "pages build and deployment" run).
70
+ - name: Render coverage badge for the docs site
71
+ if: steps.prepare.outputs.released == 'true' && hashFiles('mkdocs.yml') != ''
72
+ run: |
73
+ set -euo pipefail
74
+ # release-publish-prepare's pytest run wrote .coverage to the
75
+ # workspace root; convert to coverage.xml so the same parser
76
+ # we use in tests.yml can score it.
77
+ uv run coverage xml -o coverage.xml
78
+ python - <<'PY'
79
+ import json
80
+ import xml.etree.ElementTree as ET
81
+
82
+ tree = ET.parse("coverage.xml")
83
+ pct = round(float(tree.getroot().attrib["line-rate"]) * 100, 1)
84
+ if pct >= 95:
85
+ color = "brightgreen"
86
+ elif pct >= 85:
87
+ color = "green"
88
+ elif pct >= 70:
89
+ color = "yellow"
90
+ else:
91
+ color = "red"
92
+ payload = {
93
+ "schemaVersion": 1,
94
+ "label": "coverage",
95
+ "message": f"{pct}%",
96
+ "color": color,
97
+ }
98
+ # docs/ is mkdocs' source dir; non-.md files are copied verbatim
99
+ # into site/, which lands at the gh-pages root after gh-deploy.
100
+ with open("docs/coverage.json", "w") as fh:
101
+ json.dump(payload, fh)
102
+ fh.write("\n")
103
+ print(json.dumps(payload))
104
+ PY
105
+
106
+ - name: Deploy docs + coverage badge to gh-pages
107
+ if: steps.prepare.outputs.released == 'true' && hashFiles('mkdocs.yml') != ''
108
+ run: |
109
+ git config user.name 'github-actions[bot]'
110
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
111
+ DJANGO_SETTINGS_MODULE=tests.conftest_settings \
112
+ uv run mkdocs gh-deploy --force --clean --config-file mkdocs.yml
@@ -0,0 +1,485 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: tests-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ # Actions are pinned to a commit SHA rather than a tag: a tag is mutable, and an
13
+ # action owner (or anyone who takes over the account) can silently repoint one at
14
+ # new code that then runs with this workflow's token. The trailing version comment
15
+ # is not decoration -- Dependabot reads it to know which release the SHA is, and
16
+ # rewrites both together when a newer one ships.
17
+ jobs:
18
+ lint:
19
+ name: lint
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
24
+ with:
25
+ enable-cache: true
26
+ - name: Install
27
+ run: uv sync --all-groups
28
+ - name: ruff check
29
+ run: uv run ruff check .
30
+ - name: ruff format check
31
+ run: uv run ruff format --check --diff .
32
+ - name: ty
33
+ run: uv run ty check django_spec_rest
34
+
35
+ docs:
36
+ name: docs build
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
41
+ with:
42
+ enable-cache: true
43
+ - name: Install with docs group
44
+ run: uv sync --all-groups
45
+ - name: mkdocs build --strict
46
+ # mkdocstrings imports the package, which transitively imports DRF;
47
+ # DRF reads ``settings.REST_FRAMEWORK`` at import time, so we point at
48
+ # the test settings module to keep the build hermetic.
49
+ run: DJANGO_SETTINGS_MODULE=tests.conftest_settings uv run mkdocs build --strict
50
+
51
+ examples:
52
+ name: example project
53
+ runs-on: ubuntu-latest
54
+ steps:
55
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
56
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
57
+ with:
58
+ enable-cache: true
59
+ - name: Install
60
+ run: uv sync --all-groups
61
+ - name: Run the example project's tests
62
+ # examples/ is a Django project of its own: pytest collects none of its
63
+ # tests and the coverage gate measures none of it, so nothing else runs
64
+ # what it asserts.
65
+ run: make examples
66
+
67
+ floor:
68
+ name: lowest declared versions
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
72
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
73
+ with:
74
+ enable-cache: true
75
+ # A floor is a claim about the *oldest* version that works, and a lockfile
76
+ # can only ever check the newest. Development and CI both resolve the
77
+ # newest in-range dependency, so every gate passes while a consumer whose
78
+ # own constraints pull an older one gets a package that does not import.
79
+ #
80
+ # That has already happened in this ecosystem: a sibling declared
81
+ # pydantic-ai >=2 while importing a symbol that first exists in 2.16, and
82
+ # the only thing that found it was a downstream consumer resolving from
83
+ # scratch.
84
+ #
85
+ # `lowest-direct` pins every *declared* dependency to the bottom of its
86
+ # window while letting transitives resolve normally, so this asserts the
87
+ # floors mean what they say. A per-PR gate rather than a scheduled job,
88
+ # because a floor stops being true the moment someone imports a newer
89
+ # upstream symbol -- a code change, not the passage of time.
90
+ # Also the oldest supported interpreter: a floor claim covers Python
91
+ # too, and a dependency old enough to sit at its floor is the one most
92
+ # likely to use something a newer Python removed.
93
+ - name: Use the floor Python
94
+ run: uv python install 3.10
95
+ # `--refresh` on both resolutions below. This job's whole claim is "what a
96
+ # consumer resolving from scratch would get", and the runner's uv cache is
97
+ # shared across runs: without it the answer comes from whatever package
98
+ # list that cache happens to hold, which can be hours behind the index.
99
+ # Not hypothetical -- a sibling's gate failed a floor raise as
100
+ # unsatisfiable while the index had been serving the release for some
101
+ # time, two re-runs reused the same cache and failed identically, and a
102
+ # cacheless resolve of the same requirement succeeded at once. A stale
103
+ # listing makes this gate report the past, in whichever direction happens
104
+ # to be wrong: inventing a broken floor, or hiding a real one.
105
+ - name: Resolve every declared dependency at its floor
106
+ run: uv lock --resolution lowest-direct --python 3.10 --refresh
107
+ # --frozen and --no-sync are load-bearing, not tidiness. uv records the
108
+ # resolution mode in the lockfile and silently discards a lock resolved
109
+ # in a different one -- a plain `uv sync` here prints "Ignoring existing
110
+ # lockfile due to change in resolution mode: lowest-direct vs. highest"
111
+ # and re-resolves at the newest versions, so the job reports the oldest
112
+ # end while testing the newest. It did exactly that when first written.
113
+ - name: Install exactly what was locked
114
+ run: uv sync --frozen --all-groups --all-extras --python 3.10
115
+ - name: Name the versions that produced
116
+ run: uv pip list
117
+ - name: Test
118
+ run: uv run --no-sync pytest
119
+
120
+ # The step above installs every extra, and an extra can hold a shared
121
+ # dependency *above* the floor being claimed -- masking the lie rather
122
+ # than exposing it. That is not hypothetical: reverting a sibling's
123
+ # pydantic-ai floor to its old value and resolving with all extras kept
124
+ # that dependency at 2.18 because another extra required it, and the
125
+ # suite passed. The same revert on a plain base install resolved 2.0 and
126
+ # failed at import, which is exactly how the original defect reached a
127
+ # consumer.
128
+ #
129
+ # So the floor claim has to be checked on the install shape that has the
130
+ # fewest constraints: the package alone, no extras, no dev group.
131
+ - name: Import the package installed alone, at its floors
132
+ run: |
133
+ uv venv --python 3.10 .venv-floor
134
+ uv pip install --python .venv-floor/bin/python --resolution lowest-direct --refresh -e .
135
+ uv pip list --python .venv-floor/bin/python
136
+ .venv-floor/bin/python - <<'PY'
137
+ import django
138
+ from django.conf import settings
139
+
140
+ # DRF reads settings.REST_FRAMEWORK at import, so a bare import of
141
+ # this package cannot run unconfigured.
142
+ settings.configure(
143
+ SECRET_KEY="x",
144
+ INSTALLED_APPS=[
145
+ "django.contrib.contenttypes",
146
+ "django.contrib.auth",
147
+ "rest_framework",
148
+ ],
149
+ DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
150
+ )
151
+ django.setup()
152
+
153
+ import django_spec_rest
154
+ from django_spec_rest import ServiceCreateView, build_offline_context
155
+
156
+ # Touch the public surface rather than only the package root: an
157
+ # upstream symbol that moved would be imported by a leaf module, and
158
+ # importing the root alone can miss it.
159
+ assert callable(build_offline_context)
160
+ assert callable(ServiceCreateView)
161
+ print("BASE IMPORT OK", django.get_version())
162
+ PY
163
+
164
+ no-contenttypes:
165
+ name: without django.contrib.contenttypes
166
+ runs-on: ubuntu-latest
167
+ # The generic-relation kind is the only contact with
168
+ # ``django.contrib.contenttypes``, and that app is not guaranteed to be in
169
+ # a consumer's INSTALLED_APPS. The kind and its write are the kernel's
170
+ # (django-service-specs): ``GenericRelationSpec`` is the kernel's own
171
+ # object, and the content-type lookup and its gate live in the kernel's
172
+ # nested-writes module. A django-spec-rest install still runs them,
173
+ # so two things have to hold and neither is visible to the matrix,
174
+ # because the test settings install the app:
175
+ #
176
+ # 1. importing the package, and the kernel module holding the write,
177
+ # without it must work. ``ContentType`` is a Django *model*, and a
178
+ # project may import ``django_spec_rest`` before the app registry is
179
+ # ready (from an ``AppConfig`` module or an app's ``__init__``, or
180
+ # before ``django.setup()``) -- an eager import raises
181
+ # AppRegistryNotReady there even when the app *is* installed, and a
182
+ # RuntimeError about a model in no installed app once setup has run
183
+ # without it.
184
+ # 2. writing a generic relation without it must say so. Without the
185
+ # gate, the lookup fails with a LookupError about an app label, which
186
+ # names neither INSTALLED_APPS nor the remedy.
187
+ #
188
+ # There is no extra to install for this: contenttypes ships inside Django,
189
+ # so the opt-in is the INSTALLED_APPS entry and the only way to check the
190
+ # claim is to run without it.
191
+ steps:
192
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
193
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
194
+ with:
195
+ enable-cache: true
196
+ - name: Install
197
+ run: uv sync --all-groups
198
+ - name: Import and drive the gate with the app absent
199
+ run: |
200
+ uv run --no-sync python - <<'PY'
201
+ import django
202
+ from django.conf import settings
203
+
204
+ # No django.contrib.contenttypes, and no django.contrib.auth either
205
+ # -- auth depends on it, so leaving auth in would install the very
206
+ # thing this job removes.
207
+ settings.configure(
208
+ SECRET_KEY="x",
209
+ INSTALLED_APPS=["rest_framework"],
210
+ DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
211
+ )
212
+ django.setup()
213
+
214
+ import sys
215
+
216
+ import django_spec_rest
217
+ from django_service_specs.mutations import nested_writes
218
+ from django_service_specs.relations import generic_relation_spec
219
+ from django_service_specs import GenericRelationSpec
220
+
221
+ assert "django.contrib.contenttypes.models" not in sys.modules, (
222
+ "the package or the kernel's nested writes imported the "
223
+ "contenttypes models eagerly"
224
+ )
225
+ assert GenericRelationSpec.write_phase.name == "GENERIC"
226
+ assert GenericRelationSpec is generic_relation_spec.GenericRelationSpec
227
+
228
+ from django.core.exceptions import ImproperlyConfigured
229
+
230
+ # The gate is private to the kernel, so it is reached by its module:
231
+ # a move there fails this job at the attribute, loudly, rather than
232
+ # leaving the claim unchecked.
233
+ _content_type_for = nested_writes._content_type_for
234
+
235
+ try:
236
+ # ``None`` on purpose: the gate refuses before it ever looks at
237
+ # the instance, which is exactly the claim being checked.
238
+ _content_type_for(None)
239
+ except ImproperlyConfigured as exc:
240
+ assert "django.contrib.contenttypes" in str(exc), exc
241
+ assert "INSTALLED_APPS" in str(exc), exc
242
+ else:
243
+ raise SystemExit("the generic-relation gate did not fire")
244
+
245
+ print("NO-CONTENTTYPES OK", django.get_version())
246
+ PY
247
+
248
+ test:
249
+ name: ${{ matrix.python-version }} / Django ${{ matrix.django-version }}
250
+ runs-on: ubuntu-latest
251
+ strategy:
252
+ fail-fast: false
253
+ matrix:
254
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
255
+ django-version: ["4.2", "5.0", "5.1", "5.2", "6.0", "6.1"]
256
+ exclude:
257
+ # Django 4.2 supports Python 3.8–3.12
258
+ - python-version: "3.13"
259
+ django-version: "4.2"
260
+ - python-version: "3.14"
261
+ django-version: "4.2"
262
+ # Django 5.0 supports Python 3.10–3.12
263
+ - python-version: "3.13"
264
+ django-version: "5.0"
265
+ - python-version: "3.14"
266
+ django-version: "5.0"
267
+ # Django 5.1 supports Python 3.10–3.13
268
+ - python-version: "3.14"
269
+ django-version: "5.1"
270
+ # Django 6.0 supports Python 3.12+
271
+ - python-version: "3.10"
272
+ django-version: "6.0"
273
+ - python-version: "3.11"
274
+ django-version: "6.0"
275
+ # Django 6.1 supports Python 3.12+
276
+ - python-version: "3.10"
277
+ django-version: "6.1"
278
+ - python-version: "3.11"
279
+ django-version: "6.1"
280
+
281
+ steps:
282
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
283
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
284
+ with:
285
+ enable-cache: true
286
+ - name: Install Python ${{ matrix.python-version }}
287
+ run: uv python install ${{ matrix.python-version }}
288
+ - name: Sync dev deps
289
+ run: uv sync --all-groups --python ${{ matrix.python-version }}
290
+ - name: Pin Django ${{ matrix.django-version }}
291
+ run: uv pip install --reinstall-package django "django~=${{ matrix.django-version }}.0"
292
+ - name: pytest
293
+ run: |
294
+ uv run --no-sync pytest \
295
+ --cov-report=xml:coverage.xml \
296
+ --cov-report=html:htmlcov
297
+ - name: Upload coverage artifact
298
+ if: always()
299
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
300
+ with:
301
+ name: coverage-py${{ matrix.python-version }}-django${{ matrix.django-version }}
302
+ path: |
303
+ coverage.xml
304
+ htmlcov/
305
+ if-no-files-found: ignore
306
+
307
+ coverage-badge:
308
+ # Publishes a shields.io-compatible coverage.json to gh-pages so the README
309
+ # badge reflects reality. Runs only on push to main, after the matrix has
310
+ # produced an artifact we can read from.
311
+ name: coverage badge
312
+ needs: test
313
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
314
+ runs-on: ubuntu-latest
315
+ permissions:
316
+ contents: write
317
+ steps:
318
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
319
+ with:
320
+ # Need the parent commit so the gate step below can diff
321
+ # version.py against ${{ github.event.before }}. The default
322
+ # shallow checkout (depth 1) would make `git show "$before:…"`
323
+ # fail with exit 128 and turn the whole job red.
324
+ fetch-depth: 2
325
+ # release.yml runs in parallel on the same push and owns gh-pages
326
+ # publishing on release commits (it bundles coverage.json into the
327
+ # mkdocs gh-deploy). If we also push here we get a second built-in
328
+ # "pages build and deployment" run that the first one cancels. The
329
+ # version-diff against ${{ github.event.before }} is the reliable
330
+ # signal — release.yml's tag isn't pushed until after PyPI publish,
331
+ # so a tag-existence check would race.
332
+ - name: Skip when release.yml owns this push
333
+ id: gate
334
+ run: |
335
+ set -euo pipefail
336
+ before="${{ github.event.before }}"
337
+ if [[ -z "$before" || "$before" =~ ^0+$ ]]; then
338
+ # First push to main on a brand-new repo — no prior state to diff.
339
+ echo "skip=false" >> "$GITHUB_OUTPUT"
340
+ exit 0
341
+ fi
342
+ # `before` may not be in the local object DB if the push
343
+ # contained more than one commit (fetch-depth: 2 only guarantees
344
+ # HEAD + 1 parent). Try to fetch just that commit; if it's not
345
+ # reachable, fall back to "don't skip" — better to double-push
346
+ # to gh-pages once than to turn the pipeline red.
347
+ if ! git cat-file -e "$before^{commit}" 2>/dev/null; then
348
+ git fetch --depth=1 origin "$before" 2>/dev/null || true
349
+ fi
350
+ old=""
351
+ if git cat-file -e "$before:django_spec_rest/version.py" 2>/dev/null; then
352
+ old="$(git show "$before:django_spec_rest/version.py" \
353
+ | awk -F'"' '/^__version__/ { print $2; exit }')"
354
+ fi
355
+ new="$(awk -F'"' '/^__version__/ { print $2; exit }' \
356
+ django_spec_rest/version.py)"
357
+ if [[ -n "$old" && "$old" != "$new" ]]; then
358
+ echo "Version bumped $old → $new; release.yml will deploy gh-pages."
359
+ echo "skip=true" >> "$GITHUB_OUTPUT"
360
+ else
361
+ echo "skip=false" >> "$GITHUB_OUTPUT"
362
+ fi
363
+ - name: Download canonical coverage artifact
364
+ if: steps.gate.outputs.skip != 'true'
365
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
366
+ with:
367
+ # Pin to one cell of the matrix — coverage is the same shape across
368
+ # all cells and we only need one number.
369
+ name: coverage-py3.13-django5.2
370
+ path: coverage-artifact/
371
+ - name: Render coverage.json
372
+ if: steps.gate.outputs.skip != 'true'
373
+ run: |
374
+ python - <<'PY'
375
+ import json
376
+ import xml.etree.ElementTree as ET
377
+
378
+ tree = ET.parse("coverage-artifact/coverage.xml")
379
+ pct = round(float(tree.getroot().attrib["line-rate"]) * 100, 1)
380
+ if pct >= 95:
381
+ color = "brightgreen"
382
+ elif pct >= 85:
383
+ color = "green"
384
+ elif pct >= 70:
385
+ color = "yellow"
386
+ else:
387
+ color = "red"
388
+ payload = {
389
+ "schemaVersion": 1,
390
+ "label": "coverage",
391
+ "message": f"{pct}%",
392
+ "color": color,
393
+ }
394
+ with open("coverage.json", "w") as fh:
395
+ json.dump(payload, fh)
396
+ fh.write("\n")
397
+ print(json.dumps(payload))
398
+ PY
399
+ - name: Publish coverage.json to gh-pages
400
+ if: steps.gate.outputs.skip != 'true'
401
+ env:
402
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
403
+ run: |
404
+ set -euo pipefail
405
+ git config user.name 'github-actions[bot]'
406
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
407
+
408
+ tmp="$(mktemp -d)"
409
+ cp coverage.json "$tmp/coverage.json"
410
+
411
+ git fetch origin gh-pages --depth=1 2>/dev/null || true
412
+ if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then
413
+ git worktree add gh-pages origin/gh-pages
414
+ cp "$tmp/coverage.json" gh-pages/coverage.json
415
+ cd gh-pages
416
+ if [[ -n "$(git status --porcelain coverage.json)" ]]; then
417
+ git add coverage.json
418
+ git commit -m "coverage badge: update from ${GITHUB_SHA::7}"
419
+ git push origin HEAD:gh-pages
420
+ else
421
+ echo "coverage.json unchanged, skipping push"
422
+ fi
423
+ else
424
+ # First run before docs deploy. Lay down an orphan gh-pages with
425
+ # just the badge file; the docs deploy will overwrite the rest.
426
+ git worktree add --detach gh-pages
427
+ cd gh-pages
428
+ git checkout --orphan gh-pages
429
+ git rm -rf . >/dev/null 2>&1 || true
430
+ cp "$tmp/coverage.json" coverage.json
431
+ git add coverage.json
432
+ git commit -m "coverage badge: initial publish from ${GITHUB_SHA::7}"
433
+ git push origin gh-pages
434
+ fi
435
+
436
+ secrets:
437
+ name: secret scan
438
+ runs-on: ubuntu-latest
439
+ steps:
440
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
441
+ with:
442
+ fetch-depth: 0
443
+ - name: Install gitleaks
444
+ run: |
445
+ curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz | tar -xz gitleaks
446
+ sudo mv gitleaks /usr/local/bin/
447
+ - name: Scan repo + history
448
+ run: gitleaks git --redact --no-banner .
449
+
450
+ tests-passed:
451
+ name: tests
452
+ # always(), never !cancelled(): a skipped job reports a skipped check, and
453
+ # branch protection counts a skipped required check as passing.
454
+ if: always()
455
+ needs: [lint, docs, floor, no-contenttypes, test]
456
+ runs-on: ubuntu-latest
457
+ steps:
458
+ # Pass on an exact `success` from every job above, rather than failing on
459
+ # a list of bad results: such a list passes whatever it does not name, and
460
+ # GitHub reports a result it does not document. A job no hosted runner
461
+ # ever picks up ("The job was not acquired by Runner of type hosted even
462
+ # after multiple attempts") shows as cancelled on its own check but
463
+ # reaches `needs` as `abandoned`, and a gate that failed only on `failure`
464
+ # and `cancelled` went green over exactly that while the jobs it guards
465
+ # never ran. A matrix reaches `needs` as one result, and one cell that did
466
+ # not succeed is enough to make it something other than `success`.
467
+ #
468
+ # None of the jobs above has an `if:`, so `skipped` can only mean one of
469
+ # them never ran, and it fails like any other result. A job that gains a
470
+ # legitimate skip gets an exception naming it, never `skipped` accepted
471
+ # from all of them.
472
+ #
473
+ # The results come in through `env` rather than being spliced into the
474
+ # script, and every one is printed, so a red gate says which job and what
475
+ # GitHub reported for it.
476
+ - name: Verify every required job succeeded
477
+ env:
478
+ NEEDS: ${{ toJSON(needs) }}
479
+ run: |
480
+ echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
481
+ not_ok="$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result != "success") | "\(.key)=\(.value.result)"] | join(" ")')"
482
+ if [ -n "$not_ok" ]; then
483
+ echo "::error::required jobs did not succeed: $not_ok"
484
+ exit 1
485
+ fi