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.
- {easyapi_django-0.31/easyapi_django.egg-info → easyapi_django-0.32}/PKG-INFO +10 -7
- {easyapi_django-0.31 → easyapi_django-0.32}/README.md +9 -6
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/base.py +39 -9
- {easyapi_django-0.31 → easyapi_django-0.32/easyapi_django.egg-info}/PKG-INFO +10 -7
- {easyapi_django-0.31 → easyapi_django-0.32}/pyproject.toml +1 -1
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_cache_scope.py +28 -12
- {easyapi_django-0.31 → easyapi_django-0.32}/LICENSE +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/__init__.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/auth.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/blocking.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/calc.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/calc_resource.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/client_ip.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/constants.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/dates.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/exception.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/filters.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/helpers.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/management/__init__.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/management/commands/__init__.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/management/commands/mcp_serve.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/__init__.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/bridge.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/protocol.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/resource.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/mcp/tools.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/middleware.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/openapi.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/orm/__init__.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/rate_limit.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/redis_config.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/routes.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/schemas.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/security.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/serializer.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/__init__.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/db_router.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/registry.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/tenant/tenant.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/util.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi/ws.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/SOURCES.txt +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/dependency_links.txt +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/requires.txt +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/easyapi_django.egg-info/top_level.txt +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/setup.cfg +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_api_key_resolver.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_auth.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_base_init.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_block.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_cache.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_cache_auto_account_scope.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_client_ip.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_dispatch_error_handling.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_exception_render.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_exports.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_filter_validation.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_filters.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_get_obj_m2m_only.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_helpers.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_index_route.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_mcp.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_mcp_middleware_chain.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_normalize_field.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_openapi.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_owner_field.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_redis_config.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_return_result_falsy.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_routes_gate.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_schemas.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_security.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_serializer.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_tenant_connection.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_tenant_registry.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_util.py +0 -0
- {easyapi_django-0.31 → easyapi_django-0.32}/tests/test_ws_channels.py +0 -0
- {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.
|
|
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
|
-
|
|
282
|
-
configured field is **missing** from the session payload, the
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
255
|
-
configured field is **missing** from the session payload, the
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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.
|
|
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
|
-
|
|
282
|
-
configured field is **missing** from the session payload, the
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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):
|
|
@@ -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
|
-
# ──
|
|
157
|
+
# ── lenient semantics (warn + skip fold, never 500) ────────────────
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|