easyapi-django 0.34__tar.gz → 0.35__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.34/easyapi_django.egg-info → easyapi_django-0.35}/PKG-INFO +1 -1
  2. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/base.py +9 -2
  3. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/redis_config.py +49 -18
  4. {easyapi_django-0.34 → easyapi_django-0.35/easyapi_django.egg-info}/PKG-INFO +1 -1
  5. {easyapi_django-0.34 → easyapi_django-0.35}/pyproject.toml +1 -1
  6. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_redis_config.py +25 -3
  7. {easyapi_django-0.34 → easyapi_django-0.35}/LICENSE +0 -0
  8. {easyapi_django-0.34 → easyapi_django-0.35}/README.md +0 -0
  9. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/__init__.py +0 -0
  10. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/auth.py +0 -0
  11. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/blocking.py +0 -0
  12. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/calc.py +0 -0
  13. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/calc_resource.py +0 -0
  14. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/client_ip.py +0 -0
  15. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/constants.py +0 -0
  16. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/dates.py +0 -0
  17. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/exception.py +0 -0
  18. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/filters.py +0 -0
  19. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/helpers.py +0 -0
  20. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/management/__init__.py +0 -0
  21. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/management/commands/__init__.py +0 -0
  22. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/management/commands/mcp_serve.py +0 -0
  23. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/mcp/__init__.py +0 -0
  24. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/mcp/bridge.py +0 -0
  25. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/mcp/protocol.py +0 -0
  26. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/mcp/resource.py +0 -0
  27. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/mcp/tools.py +0 -0
  28. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/middleware.py +0 -0
  29. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/openapi.py +0 -0
  30. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/orm/__init__.py +0 -0
  31. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/rate_limit.py +0 -0
  32. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/routes.py +0 -0
  33. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/schemas.py +0 -0
  34. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/security.py +0 -0
  35. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/serializer.py +0 -0
  36. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/tenant/__init__.py +0 -0
  37. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/tenant/db_router.py +0 -0
  38. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/tenant/registry.py +0 -0
  39. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/tenant/tenant.py +0 -0
  40. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/util.py +0 -0
  41. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi/ws.py +0 -0
  42. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi_django.egg-info/SOURCES.txt +0 -0
  43. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi_django.egg-info/dependency_links.txt +0 -0
  44. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi_django.egg-info/requires.txt +0 -0
  45. {easyapi_django-0.34 → easyapi_django-0.35}/easyapi_django.egg-info/top_level.txt +0 -0
  46. {easyapi_django-0.34 → easyapi_django-0.35}/setup.cfg +0 -0
  47. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_api_key_resolver.py +0 -0
  48. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_auth.py +0 -0
  49. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_base_init.py +0 -0
  50. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_block.py +0 -0
  51. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_cache.py +0 -0
  52. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_cache_auto_account_scope.py +0 -0
  53. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_cache_scope.py +0 -0
  54. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_client_ip.py +0 -0
  55. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_dispatch_error_handling.py +0 -0
  56. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_exception_render.py +0 -0
  57. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_exports.py +0 -0
  58. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_filter_validation.py +0 -0
  59. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_filters.py +0 -0
  60. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_get_obj_m2m_only.py +0 -0
  61. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_helpers.py +0 -0
  62. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_index_route.py +0 -0
  63. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_mcp.py +0 -0
  64. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_mcp_middleware_chain.py +0 -0
  65. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_normalize_field.py +0 -0
  66. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_openapi.py +0 -0
  67. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_owner_field.py +0 -0
  68. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_return_result_falsy.py +0 -0
  69. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_routes_gate.py +0 -0
  70. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_schemas.py +0 -0
  71. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_security.py +0 -0
  72. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_serializer.py +0 -0
  73. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_tenant_connection.py +0 -0
  74. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_tenant_registry.py +0 -0
  75. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_util.py +0 -0
  76. {easyapi_django-0.34 → easyapi_django-0.35}/tests/test_ws_channels.py +0 -0
  77. {easyapi_django-0.34 → easyapi_django-0.35}/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.34
3
+ Version: 0.35
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
@@ -482,8 +482,15 @@ class BaseResource(View):
482
482
 
483
483
  label = self.model._meta.label_lower if self.model else 'unknown'
484
484
  outcome = 'hits' if response else 'misses'
485
- await redis.incr(f'{KEY_PREFIX}cache_stats:{outcome}')
486
- await redis.incr(f'{KEY_PREFIX}cache_stats:{outcome}:{label}')
485
+ # Per-model counters live in a single hash so ``get_cache_stats``
486
+ # can read them with one HGETALL instead of SCAN over the whole
487
+ # keyspace (the original ``cache_stats:hits:<label>`` layout
488
+ # cost 10+ seconds on production-sized Redis).
489
+ from .redis_config import _BY_MODEL_KEY, cache_stats_field
490
+ pipe = redis.pipeline()
491
+ pipe.incr(f'{KEY_PREFIX}cache_stats:{outcome}')
492
+ pipe.hincrby(_BY_MODEL_KEY, cache_stats_field(outcome, label), 1)
493
+ await pipe.execute()
487
494
 
488
495
  if response:
489
496
  return HttpResponse(response, content_type='application/json')
@@ -68,26 +68,56 @@ def get_redis():
68
68
  return client
69
69
 
70
70
 
71
+ # Cache hit/miss counters live in two places now:
72
+ #
73
+ # {KEY_PREFIX}cache_stats:hits global hit counter (INCR)
74
+ # {KEY_PREFIX}cache_stats:misses global miss counter (INCR)
75
+ # {KEY_PREFIX}cache_stats:by_model HASH keyed by ``hits:<label>`` /
76
+ # ``misses:<label>`` (HINCRBY)
77
+ #
78
+ # Pre-0.30 deploys used per-model keys (``cache_stats:hits:<label>``) and
79
+ # discovered them via ``SCAN``. With a large keyspace SCAN walked every
80
+ # key in the database, costing 10+ seconds for a single ``cachestats``
81
+ # call. The HASH variant reads everything in one ``HGETALL`` (O(M) where
82
+ # M = number of distinct models). Counters reset across the upgrade —
83
+ # they were observability-only.
84
+ _BY_MODEL_KEY = f'{KEY_PREFIX}cache_stats:by_model'
85
+
86
+
87
+ def cache_stats_field(outcome, label):
88
+ """Hash field for ``cache_stats:by_model``. ``outcome`` is ``hits`` or
89
+ ``misses``; ``label`` is the model's ``app.model`` lowercased name."""
90
+ return f'{outcome}:{label}'
91
+
92
+
71
93
  async def get_cache_stats():
72
94
  redis = get_redis()
73
- hits = int(await redis.get(f'{KEY_PREFIX}cache_stats:hits') or 0)
74
- misses = int(await redis.get(f'{KEY_PREFIX}cache_stats:misses') or 0)
95
+ pipe = redis.pipeline()
96
+ pipe.get(f'{KEY_PREFIX}cache_stats:hits')
97
+ pipe.get(f'{KEY_PREFIX}cache_stats:misses')
98
+ pipe.hgetall(_BY_MODEL_KEY)
99
+ raw_hits, raw_misses, raw_by_model = await pipe.execute()
100
+
101
+ hits = int(raw_hits or 0)
102
+ misses = int(raw_misses or 0)
75
103
  total = hits + misses
76
104
  ratio = hits / total if total else 0.0
77
105
 
78
- pattern = f'{KEY_PREFIX}cache_stats:hits:*'
79
106
  by_model = {}
80
- async for key in redis.scan_iter(match=pattern):
81
- label = key.split(':hits:', 1)[1]
82
- h = int(await redis.get(key) or 0)
83
- m = int(await redis.get(f'{KEY_PREFIX}cache_stats:misses:{label}') or 0)
84
- t = h + m
85
- by_model[label] = {
86
- 'hits': h,
87
- 'misses': m,
88
- 'total': t,
89
- 'ratio': h / t if t else 0.0,
90
- }
107
+ for field, value in (raw_by_model or {}).items():
108
+ if ':' not in field:
109
+ continue
110
+ kind, label = field.split(':', 1)
111
+ if kind not in ('hits', 'misses'):
112
+ continue
113
+ bucket = by_model.setdefault(label, {'hits': 0, 'misses': 0})
114
+ bucket[kind] = int(value)
115
+
116
+ for label, bucket in by_model.items():
117
+ bucket['total'] = bucket['hits'] + bucket['misses']
118
+ bucket['ratio'] = (
119
+ bucket['hits'] / bucket['total'] if bucket['total'] else 0.0
120
+ )
91
121
 
92
122
  return {
93
123
  'hits': hits,
@@ -100,7 +130,8 @@ async def get_cache_stats():
100
130
 
101
131
  async def reset_cache_stats():
102
132
  redis = get_redis()
103
- pattern = f'{KEY_PREFIX}cache_stats:*'
104
- keys = [k async for k in redis.scan_iter(match=pattern)]
105
- if keys:
106
- await redis.delete(*keys)
133
+ pipe = redis.pipeline()
134
+ pipe.delete(f'{KEY_PREFIX}cache_stats:hits')
135
+ pipe.delete(f'{KEY_PREFIX}cache_stats:misses')
136
+ pipe.delete(_BY_MODEL_KEY)
137
+ await pipe.execute()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyapi_django
3
- Version: 0.34
3
+ Version: 0.35
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
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
11
11
 
12
12
  [project]
13
13
  name = "easyapi_django"
14
- version = "0.34"
14
+ version = "0.35"
15
15
  authors = [
16
16
  { name="Stamatios Stamou Jr", email="bushier.outsets.0c@icloud.com" },
17
17
  ]
@@ -77,8 +77,12 @@ async def test_cache_stats_counts(fake_redis):
77
77
  await redis.incr(f'{KEY_PREFIX}cache_stats:hits')
78
78
  await redis.incr(f'{KEY_PREFIX}cache_stats:hits')
79
79
  await redis.incr(f'{KEY_PREFIX}cache_stats:misses')
80
- await redis.incr(f'{KEY_PREFIX}cache_stats:hits:testapp.space')
81
- await redis.incr(f'{KEY_PREFIX}cache_stats:misses:testapp.space')
80
+ # Per-model counters live in a single hash now (HGETALL on read,
81
+ # HINCRBY on write) so ``get_cache_stats`` doesn't SCAN the whole
82
+ # keyspace when Redis has millions of unrelated keys.
83
+ by_model_key = f'{KEY_PREFIX}cache_stats:by_model'
84
+ await redis.hincrby(by_model_key, 'hits:testapp.space', 1)
85
+ await redis.hincrby(by_model_key, 'misses:testapp.space', 1)
82
86
 
83
87
  stats = await get_cache_stats()
84
88
  assert stats['hits'] == 3
@@ -93,11 +97,29 @@ async def test_cache_stats_counts(fake_redis):
93
97
  }
94
98
 
95
99
 
100
+ @pytest.mark.asyncio
101
+ async def test_cache_stats_handles_only_misses(fake_redis):
102
+ """Resource that always misses should still appear with hits=0."""
103
+ await reset_cache_stats()
104
+ redis = get_redis()
105
+ by_model_key = f'{KEY_PREFIX}cache_stats:by_model'
106
+ await redis.hincrby(by_model_key, 'misses:testapp.space', 5)
107
+
108
+ stats = await get_cache_stats()
109
+ assert stats['by_model']['testapp.space'] == {
110
+ 'hits': 0,
111
+ 'misses': 5,
112
+ 'total': 5,
113
+ 'ratio': 0.0,
114
+ }
115
+
116
+
96
117
  @pytest.mark.asyncio
97
118
  async def test_reset_cache_stats(fake_redis):
98
119
  redis = get_redis()
120
+ by_model_key = f'{KEY_PREFIX}cache_stats:by_model'
99
121
  await redis.incr(f'{KEY_PREFIX}cache_stats:hits')
100
- await redis.incr(f'{KEY_PREFIX}cache_stats:misses:testapp.space')
122
+ await redis.hincrby(by_model_key, 'misses:testapp.space', 3)
101
123
 
102
124
  await reset_cache_stats()
103
125
 
File without changes
File without changes
File without changes