easyapi-django 0.31__tar.gz → 0.32__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 (77) hide show
  1. {easyapi_django-0.31/easyapi_django.egg-info → easyapi_django-0.32}/PKG-INFO +10 -7
  2. {easyapi_django-0.31 → easyapi_django-0.32}/README.md +9 -6
  3. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/base.py +39 -9
  4. {easyapi_django-0.31 → easyapi_django-0.32/easyapi_django.egg-info}/PKG-INFO +10 -7
  5. {easyapi_django-0.31 → easyapi_django-0.32}/pyproject.toml +1 -1
  6. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_cache_scope.py +28 -12
  7. {easyapi_django-0.31 → easyapi_django-0.32}/LICENSE +0 -0
  8. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/__init__.py +0 -0
  9. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/auth.py +0 -0
  10. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/blocking.py +0 -0
  11. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/calc.py +0 -0
  12. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/calc_resource.py +0 -0
  13. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/client_ip.py +0 -0
  14. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/constants.py +0 -0
  15. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/dates.py +0 -0
  16. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/exception.py +0 -0
  17. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/filters.py +0 -0
  18. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/helpers.py +0 -0
  19. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/management/__init__.py +0 -0
  20. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/management/commands/__init__.py +0 -0
  21. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/management/commands/mcp_serve.py +0 -0
  22. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/__init__.py +0 -0
  23. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/bridge.py +0 -0
  24. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/protocol.py +0 -0
  25. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/resource.py +0 -0
  26. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/tools.py +0 -0
  27. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/middleware.py +0 -0
  28. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/openapi.py +0 -0
  29. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/orm/__init__.py +0 -0
  30. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/rate_limit.py +0 -0
  31. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/redis_config.py +0 -0
  32. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/routes.py +0 -0
  33. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/schemas.py +0 -0
  34. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/security.py +0 -0
  35. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/serializer.py +0 -0
  36. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/__init__.py +0 -0
  37. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/db_router.py +0 -0
  38. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/registry.py +0 -0
  39. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/tenant.py +0 -0
  40. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/util.py +0 -0
  41. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/ws.py +0 -0
  42. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/SOURCES.txt +0 -0
  43. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/dependency_links.txt +0 -0
  44. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/requires.txt +0 -0
  45. {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/top_level.txt +0 -0
  46. {easyapi_django-0.31 → easyapi_django-0.32}/setup.cfg +0 -0
  47. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_api_key_resolver.py +0 -0
  48. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_auth.py +0 -0
  49. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_base_init.py +0 -0
  50. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_block.py +0 -0
  51. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_cache.py +0 -0
  52. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_cache_auto_account_scope.py +0 -0
  53. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_client_ip.py +0 -0
  54. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_dispatch_error_handling.py +0 -0
  55. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_exception_render.py +0 -0
  56. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_exports.py +0 -0
  57. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_filter_validation.py +0 -0
  58. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_filters.py +0 -0
  59. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_get_obj_m2m_only.py +0 -0
  60. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_helpers.py +0 -0
  61. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_index_route.py +0 -0
  62. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_mcp.py +0 -0
  63. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_mcp_middleware_chain.py +0 -0
  64. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_normalize_field.py +0 -0
  65. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_openapi.py +0 -0
  66. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_owner_field.py +0 -0
  67. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_redis_config.py +0 -0
  68. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_return_result_falsy.py +0 -0
  69. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_routes_gate.py +0 -0
  70. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_schemas.py +0 -0
  71. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_security.py +0 -0
  72. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_serializer.py +0 -0
  73. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_tenant_connection.py +0 -0
  74. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_tenant_registry.py +0 -0
  75. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_util.py +0 -0
  76. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_ws_channels.py +0 -0
  77. {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_ws_optional.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyapi_django
3
- Version: 0.31
3
+ Version: 0.32
4
4
  Summary: A simple rest api generator for django based on models
5
5
  Author-email: Stamatios Stamou Jr <bushier.outsets.0c@icloud.com>
6
6
  License: MIT
@@ -278,12 +278,15 @@ class TaskResource(BaseResource):
278
278
  cache_scope_fields = ['space_id', ('account', 'plan_id')]
279
279
  ```
280
280
 
281
- Strict by default: if the request has authenticated context but a
282
- configured field is **missing** from the session payload, the request
283
- fails with `HTTPException(500)` instead of silently merging caches
284
- across scopes. Anonymous requests skip the fold. `None`, `0` and `''`
285
- count as present (a real value). Use `before_cache` for the rare case
286
- that needs context outside `self.user` / `self.account`:
281
+ Lenient by default: if the request has authenticated context but a
282
+ configured field is **missing** from the session payload, the framework
283
+ logs a `WARNING` (logger `easyapi.base`) and **skips the scope fold** —
284
+ the request still serves, the cache may be shared across scopes until
285
+ the operator updates the session payload. Anonymous requests also skip
286
+ the fold. `None`, `0` and `''` count as present (a real value).
287
+
288
+ Use `before_cache` for the rare case that needs context outside
289
+ `self.user` / `self.account`:
287
290
 
288
291
  ```python
289
292
  async def before_cache(self, request):
@@ -251,12 +251,15 @@ class TaskResource(BaseResource):
251
251
  cache_scope_fields = ['space_id', ('account', 'plan_id')]
252
252
  ```
253
253
 
254
- Strict by default: if the request has authenticated context but a
255
- configured field is **missing** from the session payload, the request
256
- fails with `HTTPException(500)` instead of silently merging caches
257
- across scopes. Anonymous requests skip the fold. `None`, `0` and `''`
258
- count as present (a real value). Use `before_cache` for the rare case
259
- that needs context outside `self.user` / `self.account`:
254
+ Lenient by default: if the request has authenticated context but a
255
+ configured field is **missing** from the session payload, the framework
256
+ logs a `WARNING` (logger `easyapi.base`) and **skips the scope fold** —
257
+ the request still serves, the cache may be shared across scopes until
258
+ the operator updates the session payload. Anonymous requests also skip
259
+ the fold. `None`, `0` and `''` count as present (a real value).
260
+
261
+ Use `before_cache` for the rare case that needs context outside
262
+ `self.user` / `self.account`:
260
263
 
261
264
  ```python
262
265
  async def before_cache(self, request):
@@ -393,18 +393,21 @@ class BaseResource(View):
393
393
  for source, field in self.cache_scope_fields:
394
394
  container = sources.get(source)
395
395
  if container is None:
396
- raise HTTPException(
397
- 500,
398
- f"Cache scope source {source!r} missing from session "
399
- f"for {self.__class__.__name__}",
396
+ logger.warning(
397
+ "cache scope source %r missing from session for %s; "
398
+ "skipping scope fold (cache may be shared across "
399
+ "scopes — populate the session payload to fix)",
400
+ source, self.__class__.__name__,
400
401
  )
402
+ return None
401
403
  if field not in container:
402
- raise HTTPException(
403
- 500,
404
- f"Cache scope field {field!r} missing from "
405
- f"{source} session payload for "
406
- f"{self.__class__.__name__}",
404
+ logger.warning(
405
+ "cache scope field %r missing from %s session payload "
406
+ "for %s; skipping scope fold (cache may be shared "
407
+ "across scopes — populate the session payload to fix)",
408
+ field, source, self.__class__.__name__,
407
409
  )
410
+ return None
408
411
  parts.append(f'{source}.{field}={container[field]!r}')
409
412
 
410
413
  digest = hashlib.md5(':'.join(parts).encode('utf-8')).hexdigest()[:16]
@@ -514,6 +517,33 @@ class BaseResource(View):
514
517
  )
515
518
 
516
519
  async def dispatch(self, request, *args, **kwargs) -> None:
520
+ """Outer wrapper — guarantees unhandled exceptions never reach
521
+ the wire silently. ``HTTPException`` re-raises (rendered by
522
+ :class:`ExceptionMiddleware`); anything else gets logged with
523
+ full traceback and returns a sanitized JSON 500.
524
+
525
+ Without this net, exceptions from ``_authenticate``, ``aset_tenant``,
526
+ ``_serve_cache``, ``pre_process``, ``_parse_body``, ``build_filters``
527
+ or ``serialize`` bubbled to Django's default async handler with no
528
+ easyapi-side log — Sentry stayed empty and operators were blind.
529
+ """
530
+ try:
531
+ return await self._dispatch(request, *args, **kwargs)
532
+ except HTTPException:
533
+ raise
534
+ except Exception:
535
+ logger.exception(
536
+ 'Unhandled error in dispatch %s %s',
537
+ request.method, request.path,
538
+ )
539
+ if getattr(django_settings, 'DEBUG', False):
540
+ raise
541
+ return JsonResponse(
542
+ {'success': False, 'status': 500, 'detail': 'Internal error'},
543
+ status=500,
544
+ )
545
+
546
+ async def _dispatch(self, request, *args, **kwargs) -> None:
517
547
  self.identifier = get_client_ip(request)
518
548
 
519
549
  await self._enforce_rate_limit(request)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyapi_django
3
- Version: 0.31
3
+ Version: 0.32
4
4
  Summary: A simple rest api generator for django based on models
5
5
  Author-email: Stamatios Stamou Jr <bushier.outsets.0c@icloud.com>
6
6
  License: MIT
@@ -278,12 +278,15 @@ class TaskResource(BaseResource):
278
278
  cache_scope_fields = ['space_id', ('account', 'plan_id')]
279
279
  ```
280
280
 
281
- Strict by default: if the request has authenticated context but a
282
- configured field is **missing** from the session payload, the request
283
- fails with `HTTPException(500)` instead of silently merging caches
284
- across scopes. Anonymous requests skip the fold. `None`, `0` and `''`
285
- count as present (a real value). Use `before_cache` for the rare case
286
- that needs context outside `self.user` / `self.account`:
281
+ Lenient by default: if the request has authenticated context but a
282
+ configured field is **missing** from the session payload, the framework
283
+ logs a `WARNING` (logger `easyapi.base`) and **skips the scope fold** —
284
+ the request still serves, the cache may be shared across scopes until
285
+ the operator updates the session payload. Anonymous requests also skip
286
+ the fold. `None`, `0` and `''` count as present (a real value).
287
+
288
+ Use `before_cache` for the rare case that needs context outside
289
+ `self.user` / `self.account`:
287
290
 
288
291
  ```python
289
292
  async def before_cache(self, request):
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
11
11
 
12
12
  [project]
13
13
  name = "easyapi_django"
14
- version = "0.31"
14
+ version = "0.32"
15
15
  authors = [
16
16
  { name="Stamatios Stamou Jr", email="bushier.outsets.0c@icloud.com" },
17
17
  ]
@@ -154,28 +154,44 @@ def test_multiple_dimensions_combine():
154
154
  assert a._build_cache_key(_request(), None) != b._build_cache_key(_request(), None)
155
155
 
156
156
 
157
- # ── strict semantics ───────────────────────────────────────────────
157
+ # ── lenient semantics (warn + skip fold, never 500) ────────────────
158
158
 
159
159
 
160
- def test_authenticated_user_missing_field_raises_500():
160
+ def test_missing_field_logs_warning_and_skips_fold(caplog):
161
+ """Missing scope field on authenticated request must not block the
162
+ request — silent leak between scopes is preferable to taking the
163
+ site down. Operators see a warning to fix the session payload."""
164
+ import logging as _logging
165
+
161
166
  class _R(_Resource):
162
167
  cache_scope_fields = ['space_id']
163
168
  res = _make(_R, user={'id': 1}) # space_id NOT in user
164
- with pytest.raises(HTTPException) as exc:
165
- res._build_cache_key(_request(), None)
166
- assert exc.value.args[0] == 500
167
- assert 'space_id' in exc.value.args[1]
168
169
 
170
+ with caplog.at_level(_logging.WARNING, logger='easyapi.base'):
171
+ key = res._build_cache_key(_request(), None)
172
+
173
+ assert 'scope=' not in key # fold skipped
174
+ records = [r for r in caplog.records if r.name == 'easyapi.base']
175
+ assert records, 'expected a warning when scope field is missing'
176
+ msg = records[0].getMessage()
177
+ assert 'space_id' in msg
178
+ assert '_R' in msg or 'class' in msg.lower()
179
+
180
+
181
+ def test_missing_account_source_logs_warning_and_skips_fold(caplog):
182
+ import logging as _logging
169
183
 
170
- def test_authenticated_user_missing_account_source_raises_500():
171
- """User present, account expected but absent — refuses to cache."""
172
184
  class _R(_Resource):
173
185
  cache_scope_fields = [('account', 'plan_id')]
174
186
  res = _make(_R, user={'id': 1}, account=None)
175
- with pytest.raises(HTTPException) as exc:
176
- res._build_cache_key(_request(), None)
177
- assert exc.value.args[0] == 500
178
- assert 'account' in exc.value.args[1]
187
+
188
+ with caplog.at_level(_logging.WARNING, logger='easyapi.base'):
189
+ key = res._build_cache_key(_request(), None)
190
+
191
+ assert 'scope=' not in key
192
+ records = [r for r in caplog.records if r.name == 'easyapi.base']
193
+ assert records
194
+ assert 'account' in records[0].getMessage()
179
195
 
180
196
 
181
197
  # ── coexistence with session_cache ─────────────────────────────────
File without changes
File without changes