identity-plan-kit 0.2.6__tar.gz → 0.2.8__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 (176) hide show
  1. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/PKG-INFO +1 -1
  2. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/pyproject.toml +2 -2
  3. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/__init__.py +12 -0
  4. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dependencies.py +84 -1
  5. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/exceptions.py +21 -0
  6. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/repositories/user_repo.py +14 -6
  7. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/services/auth_service.py +9 -2
  8. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/__init__.py +8 -0
  9. identity_plan_kit-0.2.8/src/identity_plan_kit/plans/cache/user_plan_cache.py +205 -0
  10. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/services/plan_service.py +108 -13
  11. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/domain/exceptions.py +11 -1
  12. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/error_formatter.py +10 -8
  13. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/exception_handlers.py +93 -2
  14. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/exceptions.py +13 -5
  15. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/uv.lock +1 -1
  16. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/.gitignore +0 -0
  17. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/Makefile +0 -0
  18. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/README.md +0 -0
  19. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/env.py +0 -0
  20. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/script.py.mako +0 -0
  21. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/versions/20250124_000000_initial_schema.py +0 -0
  22. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/versions/20250127_000000_add_password_hash.py +0 -0
  23. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic.ini +0 -0
  24. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/__init__.py +0 -0
  25. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/fastapi_integration.py +0 -0
  26. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/sample_alembic_env.py +0 -0
  27. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/webhook_integration.py +0 -0
  28. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/admin_usage.py +0 -0
  29. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/basic_usage.py +0 -0
  30. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/custom_error_formats.py +0 -0
  31. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/extending_models.html +0 -0
  32. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/extension_example.py +0 -0
  33. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/.env.production.example +0 -0
  34. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/Dockerfile +0 -0
  35. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/docker-compose.yml +0 -0
  36. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/nginx.conf +0 -0
  37. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/production_setup.py +0 -0
  38. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/prometheus.yml +0 -0
  39. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/README.md +0 -0
  40. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/__init__.py +0 -0
  41. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/config.py +0 -0
  42. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/locustfile.py +0 -0
  43. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_auth.py +0 -0
  44. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_database_stress.py +0 -0
  45. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_mixed_scenarios.py +0 -0
  46. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_plans_quota.py +0 -0
  47. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_quota_direct.py +0 -0
  48. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_rbac_cache.py +0 -0
  49. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/utils.py +0 -0
  50. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/__init__.py +0 -0
  51. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/admin/__init__.py +0 -0
  52. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/admin/auth.py +0 -0
  53. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/admin/views.py +0 -0
  54. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/__init__.py +0 -0
  55. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/entities.py +0 -0
  56. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dto/__init__.py +0 -0
  57. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dto/requests.py +0 -0
  58. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dto/responses.py +0 -0
  59. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/handlers/__init__.py +0 -0
  60. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/handlers/oauth_routes.py +0 -0
  61. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/__init__.py +0 -0
  62. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/refresh_token.py +0 -0
  63. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/user.py +0 -0
  64. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/user_provider.py +0 -0
  65. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/repositories/__init__.py +0 -0
  66. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/repositories/token_repo.py +0 -0
  67. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/services/__init__.py +0 -0
  68. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/services/oauth_service.py +0 -0
  69. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/uow.py +0 -0
  70. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/cli.py +0 -0
  71. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/config.py +0 -0
  72. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/kit.py +0 -0
  73. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/migrations.py +0 -0
  74. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/cache/__init__.py +0 -0
  75. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/cache/plan_cache.py +0 -0
  76. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dependencies.py +0 -0
  77. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/domain/__init__.py +0 -0
  78. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/domain/entities.py +0 -0
  79. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/domain/exceptions.py +0 -0
  80. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dto/__init__.py +0 -0
  81. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dto/responses.py +0 -0
  82. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dto/usage.py +0 -0
  83. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/handlers/__init__.py +0 -0
  84. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/handlers/plan_routes.py +0 -0
  85. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/__init__.py +0 -0
  86. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/feature.py +0 -0
  87. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/feature_usage.py +0 -0
  88. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/plan.py +0 -0
  89. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/plan_limit.py +0 -0
  90. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/plan_permission.py +0 -0
  91. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/user_plan.py +0 -0
  92. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/repositories/__init__.py +0 -0
  93. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/repositories/plan_repo.py +0 -0
  94. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/repositories/usage_repo.py +0 -0
  95. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/services/__init__.py +0 -0
  96. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/uow.py +0 -0
  97. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/__init__.py +0 -0
  98. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/cache/__init__.py +0 -0
  99. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/cache/permission_cache.py +0 -0
  100. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/dependencies.py +0 -0
  101. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/domain/__init__.py +0 -0
  102. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/domain/entities.py +0 -0
  103. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/__init__.py +0 -0
  104. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/permission.py +0 -0
  105. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/role.py +0 -0
  106. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/role_permission.py +0 -0
  107. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/repositories/__init__.py +0 -0
  108. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/repositories/rbac_repo.py +0 -0
  109. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/services/__init__.py +0 -0
  110. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/services/rbac_service.py +0 -0
  111. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/uow.py +0 -0
  112. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/__init__.py +0 -0
  113. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/audit.py +0 -0
  114. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/circuit_breaker.py +0 -0
  115. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/cleanup_scheduler.py +0 -0
  116. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/database.py +0 -0
  117. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/graceful_shutdown.py +0 -0
  118. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/health.py +0 -0
  119. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/http_utils.py +0 -0
  120. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/lockout.py +0 -0
  121. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/logging.py +0 -0
  122. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/metrics.py +0 -0
  123. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/models.py +0 -0
  124. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/rate_limiter.py +0 -0
  125. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/request_id.py +0 -0
  126. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/schemas.py +0 -0
  127. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/security.py +0 -0
  128. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/state_store.py +0 -0
  129. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/uow.py +0 -0
  130. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/uuid7.py +0 -0
  131. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/__init__.py +0 -0
  132. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/__init__.py +0 -0
  133. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_auth_service.py +0 -0
  134. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_circuit_breaker.py +0 -0
  135. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_csrf_state.py +0 -0
  136. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_dependencies.py +0 -0
  137. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_domain_entities.py +0 -0
  138. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_http_utils.py +0 -0
  139. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_lockout.py +0 -0
  140. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_lockout_concurrent.py +0 -0
  141. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_oauth_service_resilience.py +0 -0
  142. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_security_comprehensive.py +0 -0
  143. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_token_security.py +0 -0
  144. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/conftest.py +0 -0
  145. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/__init__.py +0 -0
  146. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/conftest.py +0 -0
  147. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_limit_merge.py +0 -0
  148. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_plan_operations.py +0 -0
  149. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_token_operations.py +0 -0
  150. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_user_creation.py +0 -0
  151. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_user_deactivation.py +0 -0
  152. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_limit_boundaries.py +0 -0
  153. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_plan_cache_race.py +0 -0
  154. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_quota_concurrency.py +0 -0
  155. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_token_cleanup.py +0 -0
  156. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_token_repository.py +0 -0
  157. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_usage_periods.py +0 -0
  158. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_user_repository.py +0 -0
  159. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/__init__.py +0 -0
  160. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_domain_entities.py +0 -0
  161. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_period_edge_cases.py +0 -0
  162. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_plan_cache_concurrent.py +0 -0
  163. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_plan_service.py +0 -0
  164. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/__init__.py +0 -0
  165. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_permission_cache.py +0 -0
  166. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_permission_cache_concurrent.py +0 -0
  167. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_rbac_cache_stampede.py +0 -0
  168. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_rbac_service.py +0 -0
  169. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_audit_pii_masking.py +0 -0
  170. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_circuit_breaker_concurrent.py +0 -0
  171. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_cleanup_scheduler_concurrent.py +0 -0
  172. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_config_validation.py +0 -0
  173. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_http_utils_security.py +0 -0
  174. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_rate_limiter.py +0 -0
  175. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_rate_limiter_concurrent.py +0 -0
  176. {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_state_store_concurrent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: identity-plan-kit
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Modern FastAPI library for authentication, RBAC, subscription plans, and usage tracking
5
5
  Author-email: harut <harut.avetisyan2002@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "identity-plan-kit"
3
- version = "0.2.6"
3
+ version = "0.2.8"
4
4
  description = "Modern FastAPI library for authentication, RBAC, subscription plans, and usage tracking"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -140,7 +140,7 @@ testpaths = ["tests"]
140
140
  addopts = "-v --tb=short"
141
141
 
142
142
  [tool.bumpversion]
143
- current_version = "0.2.6"
143
+ current_version = "0.2.8"
144
144
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
145
145
  serialize = ["{major}.{minor}.{patch}"]
146
146
  tag = true
@@ -5,8 +5,14 @@ from identity_plan_kit.auth.domain.entities import RefreshToken, User, UserProvi
5
5
  from identity_plan_kit.auth.domain.exceptions import (
6
6
  AuthError,
7
7
  InvalidCredentialsError,
8
+ OAuthError,
8
9
  PasswordValidationError,
10
+ ProviderNotConfiguredError,
11
+ RefreshTokenExpiredError,
12
+ RefreshTokenInvalidError,
13
+ RefreshTokenMissingError,
9
14
  TokenExpiredError,
15
+ TokenInvalidError,
10
16
  UserInactiveError,
11
17
  UserNotFoundError,
12
18
  )
@@ -22,12 +28,18 @@ __all__ = [
22
28
  # Dependencies
23
29
  "CurrentUser",
24
30
  "InvalidCredentialsError",
31
+ "OAuthError",
25
32
  "OptionalUser",
26
33
  "PasswordValidationError",
34
+ "ProviderNotConfiguredError",
27
35
  "RefreshToken",
36
+ "RefreshTokenExpiredError",
37
+ "RefreshTokenInvalidError",
38
+ "RefreshTokenMissingError",
28
39
  # Repositories (for direct use with external sessions)
29
40
  "RefreshTokenRepository",
30
41
  "TokenExpiredError",
42
+ "TokenInvalidError",
31
43
  # Entities
32
44
  "User",
33
45
  "UserInactiveError",
@@ -1,6 +1,7 @@
1
1
  """Auth FastAPI dependencies."""
2
2
 
3
- from typing import Annotated
3
+ from collections.abc import Callable, Coroutine
4
+ from typing import Annotated, Any
4
5
 
5
6
  from fastapi import Cookie, Depends, HTTPException, Request, status
6
7
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -21,6 +22,84 @@ logger = get_logger(__name__)
21
22
  bearer_scheme = HTTPBearer(auto_error=False)
22
23
 
23
24
 
25
+ def current_user(
26
+ include_role: bool = True,
27
+ ) -> Callable[..., Coroutine[Any, Any, User]]:
28
+ """
29
+ Create a parameterized current user dependency.
30
+
31
+ Use this when you need to control whether role is loaded:
32
+
33
+ Example::
34
+
35
+ # Skip role loading for better performance
36
+ @router.get("/generate")
37
+ async def generate(
38
+ user: Annotated[User, Depends(current_user(include_role=False))],
39
+ ):
40
+ ...
41
+
42
+ Args:
43
+ include_role: If True, eagerly load the user's role (default: True).
44
+ Set to False to skip the role query when role info is not needed.
45
+
46
+ Returns:
47
+ FastAPI dependency function
48
+ """
49
+
50
+ async def _get_user(
51
+ request: Request,
52
+ credentials: Annotated[
53
+ HTTPAuthorizationCredentials | None,
54
+ Depends(bearer_scheme),
55
+ ] = None,
56
+ access_token: Annotated[str | None, Cookie(alias="access_token")] = None,
57
+ ) -> User:
58
+ token: str | None = None
59
+ if credentials:
60
+ token = credentials.credentials
61
+ elif access_token:
62
+ token = access_token
63
+
64
+ if not token:
65
+ raise HTTPException(
66
+ status_code=status.HTTP_401_UNAUTHORIZED,
67
+ detail="Not authenticated",
68
+ headers={"WWW-Authenticate": "Bearer"},
69
+ )
70
+
71
+ kit = request.app.state.identity_plan_kit
72
+ auth_service = kit.auth_service
73
+
74
+ try:
75
+ return await auth_service.get_user_from_token(token, include_role=include_role)
76
+ except TokenExpiredError:
77
+ raise HTTPException(
78
+ status_code=status.HTTP_401_UNAUTHORIZED,
79
+ detail="Token has expired",
80
+ headers={"WWW-Authenticate": "Bearer"},
81
+ ) from None
82
+ except (TokenInvalidError, AuthError):
83
+ raise HTTPException(
84
+ status_code=status.HTTP_401_UNAUTHORIZED,
85
+ detail="Invalid token",
86
+ headers={"WWW-Authenticate": "Bearer"},
87
+ ) from None
88
+ except UserNotFoundError:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_401_UNAUTHORIZED,
91
+ detail="User not found",
92
+ headers={"WWW-Authenticate": "Bearer"},
93
+ ) from None
94
+ except UserInactiveError:
95
+ raise HTTPException(
96
+ status_code=status.HTTP_403_FORBIDDEN,
97
+ detail="User account is inactive",
98
+ ) from None
99
+
100
+ return _get_user
101
+
102
+
24
103
  async def get_current_user(
25
104
  request: Request,
26
105
  credentials: Annotated[
@@ -137,3 +216,7 @@ async def get_optional_user(
137
216
  # Type aliases for dependency injection
138
217
  CurrentUser = Annotated[User, Depends(get_current_user)]
139
218
  OptionalUser = Annotated[User | None, Depends(get_optional_user)]
219
+
220
+ # Optimized variant that skips role loading (saves 1 DB query)
221
+ # Use when role info is not needed for the endpoint
222
+ CurrentUserNoRole = Annotated[User, Depends(current_user(include_role=False))]
@@ -35,6 +35,27 @@ class TokenInvalidError(AuthError):
35
35
  message = "Invalid token"
36
36
 
37
37
 
38
+ class RefreshTokenMissingError(AuthError):
39
+ """Refresh token not provided."""
40
+
41
+ code = "REFRESH_TOKEN_MISSING"
42
+ message = "Refresh token not provided"
43
+
44
+
45
+ class RefreshTokenInvalidError(AuthError):
46
+ """Refresh token is invalid."""
47
+
48
+ code = "REFRESH_TOKEN_INVALID"
49
+ message = "Invalid refresh token"
50
+
51
+
52
+ class RefreshTokenExpiredError(AuthError):
53
+ """Refresh token has expired."""
54
+
55
+ code = "REFRESH_TOKEN_EXPIRED"
56
+ message = "Refresh token has expired"
57
+
58
+
38
59
  class UserNotFoundError(NotFoundError):
39
60
  """User not found."""
40
61
 
@@ -26,6 +26,7 @@ class UserRepository:
26
26
  self,
27
27
  user_id: UUID,
28
28
  for_update: bool = False,
29
+ include_role: bool = True,
29
30
  ) -> User | None:
30
31
  """
31
32
  Get user by ID.
@@ -34,13 +35,16 @@ class UserRepository:
34
35
  user_id: User UUID
35
36
  for_update: If True, lock the row for update (prevents race conditions
36
37
  in operations that depend on current user state like is_active)
38
+ include_role: If True, eagerly load the user's role (default: True).
39
+ Set to False to skip the role query when role info is not needed.
37
40
 
38
41
  Returns:
39
42
  User entity or None if not found
40
43
  """
41
- stmt = (
42
- select(UserModel).options(selectinload(UserModel.role)).where(UserModel.id == user_id)
43
- )
44
+ stmt = select(UserModel).where(UserModel.id == user_id)
45
+
46
+ if include_role:
47
+ stmt = stmt.options(selectinload(UserModel.role))
44
48
 
45
49
  if for_update:
46
50
  stmt = stmt.with_for_update()
@@ -57,6 +61,7 @@ class UserRepository:
57
61
  self,
58
62
  email: str,
59
63
  for_update: bool = False,
64
+ include_role: bool = True,
60
65
  ) -> User | None:
61
66
  """
62
67
  Get user by email address.
@@ -64,13 +69,16 @@ class UserRepository:
64
69
  Args:
65
70
  email: User email
66
71
  for_update: If True, lock the row for update
72
+ include_role: If True, eagerly load the user's role (default: True).
73
+ Set to False to skip the role query when role info is not needed.
67
74
 
68
75
  Returns:
69
76
  User entity or None if not found
70
77
  """
71
- stmt = (
72
- select(UserModel).options(selectinload(UserModel.role)).where(UserModel.email == email)
73
- )
78
+ stmt = select(UserModel).where(UserModel.email == email)
79
+
80
+ if include_role:
81
+ stmt = stmt.options(selectinload(UserModel.role))
74
82
 
75
83
  if for_update:
76
84
  stmt = stmt.with_for_update()
@@ -98,12 +98,19 @@ class AuthService:
98
98
  """Get Google OAuth service."""
99
99
  return self._google_oauth
100
100
 
101
- async def get_user_from_token(self, token: str) -> User:
101
+ async def get_user_from_token(
102
+ self,
103
+ token: str,
104
+ include_role: bool = True,
105
+ ) -> User:
102
106
  """
103
107
  Get user from access token.
104
108
 
105
109
  Args:
106
110
  token: JWT access token
111
+ include_role: If True, eagerly load the user's role (default: True).
112
+ Set to False to skip the role query when role info is not needed,
113
+ reducing database queries for better performance.
107
114
 
108
115
  Returns:
109
116
  User entity
@@ -134,7 +141,7 @@ class AuthService:
134
141
  raise TokenInvalidError("Missing user ID in token")
135
142
 
136
143
  async with self._create_uow() as uow:
137
- user = await uow.users.get_by_id(UUID(user_id))
144
+ user = await uow.users.get_by_id(UUID(user_id), include_role=include_role)
138
145
 
139
146
  if user is None:
140
147
  raise UserNotFoundError()
@@ -9,6 +9,10 @@ from identity_plan_kit.plans.domain.entities import Feature, Plan, PlanLimit, Us
9
9
  from identity_plan_kit.plans.domain.exceptions import (
10
10
  FeatureNotAvailableError,
11
11
  FeatureNotFoundError,
12
+ InvalidCustomLimitsError,
13
+ InvalidPlanDatesError,
14
+ PlanAssignmentError,
15
+ PlanAuthorizationError,
12
16
  PlanExpiredError,
13
17
  PlanNotFoundError,
14
18
  QuotaExceededError,
@@ -26,7 +30,11 @@ __all__ = [
26
30
  # Exceptions
27
31
  "FeatureNotAvailableError",
28
32
  "FeatureNotFoundError",
33
+ "InvalidCustomLimitsError",
34
+ "InvalidPlanDatesError",
29
35
  "Plan",
36
+ "PlanAssignmentError",
37
+ "PlanAuthorizationError",
30
38
  "PlanExpiredError",
31
39
  "PlanLimit",
32
40
  "PlanNotFoundError",
@@ -0,0 +1,205 @@
1
+ """User plan cache for fast user plan lookups."""
2
+
3
+ import asyncio
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from datetime import UTC, datetime, timedelta
7
+ from uuid import UUID
8
+
9
+ from identity_plan_kit.plans.domain.entities import Plan, UserPlan
10
+ from identity_plan_kit.shared.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class UserPlanCacheEntry:
17
+ """Cache entry for user plan with expiration."""
18
+
19
+ user_plan: UserPlan
20
+ plan: Plan
21
+ expires_at: datetime
22
+ fetched_at: float = field(default_factory=time.monotonic)
23
+
24
+ @property
25
+ def is_expired(self) -> bool:
26
+ """Check if entry has expired."""
27
+ return datetime.now(UTC) > self.expires_at
28
+
29
+
30
+ class UserPlanCache:
31
+ """
32
+ In-memory user plan cache.
33
+
34
+ Caches user plan assignments (by user_id) to reduce database queries.
35
+ User plans change less frequently than usage, so caching provides
36
+ significant benefit for quota check operations.
37
+
38
+ Cache key: user_id (UUID)
39
+ Cache value: (UserPlan, Plan) tuple with full plan details
40
+
41
+ **Invalidation:**
42
+
43
+ Call invalidate() when:
44
+ - User's plan is assigned/changed
45
+ - User's plan is cancelled
46
+ - User's plan is extended
47
+ - User's custom limits are updated
48
+
49
+ **TTL:**
50
+
51
+ Default TTL is 5 minutes. This balances:
52
+ - Reducing database load (cache hits avoid 2-4 queries)
53
+ - Ensuring plan changes propagate within reasonable time
54
+ - Memory usage (entries expire and get cleaned up)
55
+ """
56
+
57
+ def __init__(self, ttl_seconds: int = 300) -> None:
58
+ """
59
+ Initialize user plan cache.
60
+
61
+ Args:
62
+ ttl_seconds: Cache TTL in seconds (default: 5 minutes, 0 to disable)
63
+ """
64
+ self._ttl = timedelta(seconds=ttl_seconds)
65
+ self._cache: dict[UUID, UserPlanCacheEntry] = {}
66
+ self._write_lock = asyncio.Lock()
67
+ self._enabled = ttl_seconds > 0
68
+ self._invalidated_at: dict[UUID, float] = {}
69
+ self._global_invalidated_at: float = 0.0
70
+
71
+ async def get(self, user_id: UUID) -> tuple[UserPlan, Plan] | None:
72
+ """
73
+ Get cached user plan by user ID.
74
+
75
+ Args:
76
+ user_id: User UUID
77
+
78
+ Returns:
79
+ Tuple of (UserPlan, Plan) or None if not cached/expired
80
+ """
81
+ if not self._enabled:
82
+ return None
83
+
84
+ entry = self._cache.get(user_id)
85
+
86
+ if entry is None:
87
+ return None
88
+
89
+ if entry.is_expired:
90
+ return None
91
+
92
+ return entry.user_plan, entry.plan
93
+
94
+ def get_fetch_timestamp(self) -> float:
95
+ """
96
+ Get a monotonic timestamp to associate with a DB fetch.
97
+
98
+ Call this BEFORE fetching from the database, then pass the timestamp
99
+ to set() to enable stale write prevention.
100
+
101
+ Returns:
102
+ Monotonic timestamp
103
+ """
104
+ return time.monotonic()
105
+
106
+ async def set(
107
+ self,
108
+ user_id: UUID,
109
+ user_plan: UserPlan,
110
+ plan: Plan,
111
+ fetched_at: float | None = None,
112
+ ) -> bool:
113
+ """
114
+ Cache user plan by user ID.
115
+
116
+ Args:
117
+ user_id: User UUID
118
+ user_plan: UserPlan entity to cache
119
+ plan: Plan entity with full details (permissions, limits)
120
+ fetched_at: Monotonic timestamp when data was fetched from DB
121
+
122
+ Returns:
123
+ True if the entry was cached, False if rejected as stale
124
+ """
125
+ if not self._enabled:
126
+ return False
127
+
128
+ if fetched_at is not None:
129
+ if fetched_at < self._global_invalidated_at:
130
+ logger.debug(
131
+ "user_plan_cache_stale_write_rejected",
132
+ user_id=str(user_id),
133
+ reason="global_invalidation",
134
+ )
135
+ return False
136
+
137
+ key_invalidated_at = self._invalidated_at.get(user_id, 0.0)
138
+ if fetched_at < key_invalidated_at:
139
+ logger.debug(
140
+ "user_plan_cache_stale_write_rejected",
141
+ user_id=str(user_id),
142
+ reason="key_invalidation",
143
+ )
144
+ return False
145
+
146
+ entry = UserPlanCacheEntry(
147
+ user_plan=user_plan,
148
+ plan=plan,
149
+ expires_at=datetime.now(UTC) + self._ttl,
150
+ fetched_at=fetched_at or time.monotonic(),
151
+ )
152
+ self._cache[user_id] = entry
153
+ return True
154
+
155
+ async def invalidate(self, user_id: UUID) -> None:
156
+ """
157
+ Invalidate cached user plan by user ID.
158
+
159
+ Call this when a user's plan changes (assigned, cancelled, extended, etc.).
160
+
161
+ Args:
162
+ user_id: User UUID to invalidate
163
+ """
164
+ self._invalidated_at[user_id] = time.monotonic()
165
+ self._cache.pop(user_id, None)
166
+ logger.debug("user_plan_cache_invalidated", user_id=str(user_id))
167
+
168
+ async def invalidate_all(self) -> None:
169
+ """
170
+ Invalidate all cached user plans.
171
+
172
+ Call this when plans are modified globally.
173
+ """
174
+ async with self._write_lock:
175
+ self._global_invalidated_at = time.monotonic()
176
+ self._cache.clear()
177
+ self._invalidated_at.clear()
178
+ logger.info("user_plan_cache_cleared")
179
+
180
+ async def cleanup_expired(self) -> int:
181
+ """
182
+ Remove expired entries from cache.
183
+
184
+ Returns:
185
+ Number of entries removed
186
+ """
187
+ async with self._write_lock:
188
+ expired_keys = [
189
+ key for key, entry in self._cache.items() if entry.is_expired
190
+ ]
191
+ for key in expired_keys:
192
+ del self._cache[key]
193
+
194
+ if expired_keys:
195
+ logger.debug(
196
+ "user_plan_cache_cleanup",
197
+ removed_count=len(expired_keys),
198
+ )
199
+
200
+ return len(expired_keys)
201
+
202
+ @property
203
+ def size(self) -> int:
204
+ """Get current cache size."""
205
+ return len(self._cache)