easyapi-django 0.30__tar.gz → 0.31__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 (78) hide show
  1. {easyapi_django-0.30/easyapi_django.egg-info → easyapi_django-0.31}/PKG-INFO +1 -1
  2. easyapi_django-0.31/easyapi/exception.py +24 -0
  3. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/rate_limit.py +25 -6
  4. {easyapi_django-0.30 → easyapi_django-0.31/easyapi_django.egg-info}/PKG-INFO +1 -1
  5. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi_django.egg-info/SOURCES.txt +1 -0
  6. {easyapi_django-0.30 → easyapi_django-0.31}/pyproject.toml +1 -1
  7. easyapi_django-0.31/tests/test_exception_render.py +69 -0
  8. easyapi_django-0.30/easyapi/exception.py +0 -11
  9. {easyapi_django-0.30 → easyapi_django-0.31}/LICENSE +0 -0
  10. {easyapi_django-0.30 → easyapi_django-0.31}/README.md +0 -0
  11. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/__init__.py +0 -0
  12. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/auth.py +0 -0
  13. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/base.py +0 -0
  14. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/blocking.py +0 -0
  15. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/calc.py +0 -0
  16. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/calc_resource.py +0 -0
  17. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/client_ip.py +0 -0
  18. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/constants.py +0 -0
  19. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/dates.py +0 -0
  20. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/filters.py +0 -0
  21. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/helpers.py +0 -0
  22. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/management/__init__.py +0 -0
  23. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/management/commands/__init__.py +0 -0
  24. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/management/commands/mcp_serve.py +0 -0
  25. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/mcp/__init__.py +0 -0
  26. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/mcp/bridge.py +0 -0
  27. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/mcp/protocol.py +0 -0
  28. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/mcp/resource.py +0 -0
  29. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/mcp/tools.py +0 -0
  30. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/middleware.py +0 -0
  31. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/openapi.py +0 -0
  32. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/orm/__init__.py +0 -0
  33. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/redis_config.py +0 -0
  34. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/routes.py +0 -0
  35. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/schemas.py +0 -0
  36. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/security.py +0 -0
  37. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/serializer.py +0 -0
  38. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/tenant/__init__.py +0 -0
  39. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/tenant/db_router.py +0 -0
  40. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/tenant/registry.py +0 -0
  41. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/tenant/tenant.py +0 -0
  42. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/util.py +0 -0
  43. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi/ws.py +0 -0
  44. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi_django.egg-info/dependency_links.txt +0 -0
  45. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi_django.egg-info/requires.txt +0 -0
  46. {easyapi_django-0.30 → easyapi_django-0.31}/easyapi_django.egg-info/top_level.txt +0 -0
  47. {easyapi_django-0.30 → easyapi_django-0.31}/setup.cfg +0 -0
  48. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_api_key_resolver.py +0 -0
  49. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_auth.py +0 -0
  50. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_base_init.py +0 -0
  51. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_block.py +0 -0
  52. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_cache.py +0 -0
  53. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_cache_auto_account_scope.py +0 -0
  54. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_cache_scope.py +0 -0
  55. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_client_ip.py +0 -0
  56. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_dispatch_error_handling.py +0 -0
  57. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_exports.py +0 -0
  58. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_filter_validation.py +0 -0
  59. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_filters.py +0 -0
  60. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_get_obj_m2m_only.py +0 -0
  61. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_helpers.py +0 -0
  62. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_index_route.py +0 -0
  63. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_mcp.py +0 -0
  64. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_mcp_middleware_chain.py +0 -0
  65. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_normalize_field.py +0 -0
  66. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_openapi.py +0 -0
  67. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_owner_field.py +0 -0
  68. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_redis_config.py +0 -0
  69. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_return_result_falsy.py +0 -0
  70. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_routes_gate.py +0 -0
  71. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_schemas.py +0 -0
  72. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_security.py +0 -0
  73. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_serializer.py +0 -0
  74. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_tenant_connection.py +0 -0
  75. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_tenant_registry.py +0 -0
  76. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_util.py +0 -0
  77. {easyapi_django-0.30 → easyapi_django-0.31}/tests/test_ws_channels.py +0 -0
  78. {easyapi_django-0.30 → easyapi_django-0.31}/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.30
3
+ Version: 0.31
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
@@ -0,0 +1,24 @@
1
+ import logging
2
+
3
+ from django.http import JsonResponse
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class HTTPException(Exception):
9
+
10
+ def render(self, exception):
11
+ (status, detail) = exception.args
12
+ # 5xx is a server-side problem — emit a logged stack trace so the
13
+ # response body is not the only signal. Without this, every
14
+ # ``raise HTTPException(500, ...)`` was invisible to logs/Sentry
15
+ # and operators had to guess from black-box symptoms.
16
+ if isinstance(status, int) and status >= 500:
17
+ logger.exception(
18
+ 'HTTPException %s: %s', status, detail,
19
+ exc_info=exception,
20
+ )
21
+ return JsonResponse(
22
+ {'success': False, 'status': status, 'detail': detail},
23
+ status=status,
24
+ )
@@ -1,8 +1,12 @@
1
+ import logging
1
2
  import time
3
+
2
4
  from redis import StrictRedis
3
5
 
4
6
  from .redis_config import REDIS_SERVER, REDIS_DB, KEY_PREFIX
5
7
 
8
+ logger = logging.getLogger(__name__)
9
+
6
10
 
7
11
  class RateLimit(StrictRedis):
8
12
  def __init__(self):
@@ -30,7 +34,15 @@ class RateLimit(StrictRedis):
30
34
  return {"blocked": False}
31
35
 
32
36
  def track_pattern(self, identifier, action="abuse"):
33
- """Detect regular request patterns indicative of abuse."""
37
+ """Detect regular request patterns indicative of abuse.
38
+
39
+ Reports the signal but does NOT auto-write a block key. Frontends
40
+ retrying after a server-side error look identical to a bot when
41
+ you only consider request timing — auto-blocking that traffic
42
+ locked legitimate users out and was opaque to debug. Callers may
43
+ decide to escalate based on the returned ``suspicious`` flag plus
44
+ other signals (e.g. authenticated identity, response codes).
45
+ """
34
46
  pattern_key = f"{KEY_PREFIX}rate_limit:pattern:{action}:{identifier}"
35
47
  now = self.current_milli_time()
36
48
  last_req = self.get(f"{pattern_key}:last")
@@ -50,8 +62,11 @@ class RateLimit(StrictRedis):
50
62
  mean = sum(intervals) / len(intervals)
51
63
  variance = sum((x - mean) ** 2 for x in intervals) / len(intervals)
52
64
  if variance < 100:
53
- block_key = f"{KEY_PREFIX}block:{action}:{identifier}"
54
- self.setex(block_key, 300, "suspicious")
65
+ logger.warning(
66
+ 'rate_limit: suspicious request pattern from %s '
67
+ '(action=%s, mean_interval_ms=%.0f, variance=%.2f)',
68
+ identifier, action, mean, variance,
69
+ )
55
70
  return {"suspicious": True, "reason": "Regular request intervals"}
56
71
  return {"suspicious": False}
57
72
 
@@ -69,9 +84,13 @@ class RateLimit(StrictRedis):
69
84
  return {"rate_limited": True, "abuse": False, "blocked": True}
70
85
 
71
86
  if action in ["api", "login"]:
72
- pattern_result = self.track_pattern(identifier, "abuse")
73
- if pattern_result["suspicious"]:
74
- return {"rate_limited": True, "abuse": True, "reason": pattern_result["reason"]}
87
+ # Pattern detection is recorded for observability but no
88
+ # longer short-circuits to ``abuse=True``. Timing-only
89
+ # signals caught legitimate retry traffic (frontends
90
+ # retrying after server-side errors look identical to bots
91
+ # when judged by intervals alone). Real abuse is caught by
92
+ # the count thresholds below; pattern alone goes to logs.
93
+ self.track_pattern(identifier, "abuse")
75
94
 
76
95
  now = self.current_milli_time()
77
96
  type_to_check = action
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyapi_django
3
- Version: 0.30
3
+ Version: 0.31
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
@@ -50,6 +50,7 @@ tests/test_cache_auto_account_scope.py
50
50
  tests/test_cache_scope.py
51
51
  tests/test_client_ip.py
52
52
  tests/test_dispatch_error_handling.py
53
+ tests/test_exception_render.py
53
54
  tests/test_exports.py
54
55
  tests/test_filter_validation.py
55
56
  tests/test_filters.py
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
11
11
 
12
12
  [project]
13
13
  name = "easyapi_django"
14
- version = "0.30"
14
+ version = "0.31"
15
15
  authors = [
16
16
  { name="Stamatios Stamou Jr", email="bushier.outsets.0c@icloud.com" },
17
17
  ]
@@ -0,0 +1,69 @@
1
+ """HTTPException.render must log 5xx errors so Sentry / log aggregators
2
+ see them. Silent 5xx renders were the root of an opaque-error incident
3
+ in production."""
4
+ import json
5
+ import logging
6
+
7
+ import pytest
8
+
9
+ from easyapi.exception import HTTPException
10
+
11
+
12
+ def _render(status, detail='boom'):
13
+ exc = HTTPException(status, detail)
14
+ response = exc.render(exc)
15
+ body = json.loads(response.content.decode('utf-8'))
16
+ return response, body
17
+
18
+
19
+ def test_500_logs_exception(caplog):
20
+ with caplog.at_level(logging.ERROR, logger='easyapi.exception'):
21
+ response, body = _render(500, 'database unreachable')
22
+
23
+ assert response.status_code == 500
24
+ assert body == {'success': False, 'status': 500, 'detail': 'database unreachable'}
25
+
26
+ records = [r for r in caplog.records if r.name == 'easyapi.exception']
27
+ assert records, 'expected a log record for 500 render'
28
+ record = records[0]
29
+ assert record.levelno == logging.ERROR
30
+ assert '500' in record.getMessage()
31
+ assert 'database unreachable' in record.getMessage()
32
+ # exc_info attached so traceback is preserved for Sentry-style sinks
33
+ assert record.exc_info is not None
34
+
35
+
36
+ def test_503_also_logs(caplog):
37
+ with caplog.at_level(logging.ERROR, logger='easyapi.exception'):
38
+ _render(503, 'upstream down')
39
+ records = [r for r in caplog.records if r.name == 'easyapi.exception']
40
+ assert records, '5xx other than 500 must also log'
41
+
42
+
43
+ def test_400_does_not_log(caplog):
44
+ """4xx is client-side; no log noise — they're routine."""
45
+ with caplog.at_level(logging.DEBUG, logger='easyapi.exception'):
46
+ response, body = _render(400, 'bad input')
47
+ assert response.status_code == 400
48
+ records = [r for r in caplog.records if r.name == 'easyapi.exception']
49
+ assert not records
50
+
51
+
52
+ def test_403_does_not_log(caplog):
53
+ with caplog.at_level(logging.DEBUG, logger='easyapi.exception'):
54
+ _render(403, 'forbidden')
55
+ records = [r for r in caplog.records if r.name == 'easyapi.exception']
56
+ assert not records
57
+
58
+
59
+ def test_404_does_not_log(caplog):
60
+ with caplog.at_level(logging.DEBUG, logger='easyapi.exception'):
61
+ _render(404, 'not found')
62
+ records = [r for r in caplog.records if r.name == 'easyapi.exception']
63
+ assert not records
64
+
65
+
66
+ def test_response_body_unchanged_for_5xx():
67
+ """Logging is additive — must not change the wire response."""
68
+ response, body = _render(500, 'something')
69
+ assert body == {'success': False, 'status': 500, 'detail': 'something'}
@@ -1,11 +0,0 @@
1
- from django.http import JsonResponse
2
-
3
-
4
- class HTTPException(Exception):
5
-
6
- def render(self, exception):
7
- (status, detail) = exception.args
8
- return JsonResponse(
9
- {'success': False, 'status': status, 'detail': detail},
10
- status=status,
11
- )
File without changes
File without changes
File without changes