identity-plan-kit 0.2.6__tar.gz → 0.2.7__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.
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/PKG-INFO +1 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/pyproject.toml +2 -2
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/dependencies.py +84 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/repositories/user_repo.py +14 -6
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/services/auth_service.py +9 -2
- identity_plan_kit-0.2.7/src/identity_plan_kit/plans/cache/user_plan_cache.py +205 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/services/plan_service.py +108 -13
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/uv.lock +1 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/.gitignore +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/Makefile +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/README.md +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/alembic/env.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/alembic/script.py.mako +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/alembic/versions/20250124_000000_initial_schema.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/alembic/versions/20250127_000000_add_password_hash.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/alembic.ini +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/fastapi_integration.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/sample_alembic_env.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/webhook_integration.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/playing/admin_usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/playing/basic_usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/playing/custom_error_formats.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/playing/extending_models.html +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/playing/extension_example.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/prod/.env.production.example +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/prod/Dockerfile +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/prod/docker-compose.yml +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/prod/nginx.conf +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/prod/production_setup.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/prod/prometheus.yml +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/README.md +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/config.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/locustfile.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/test_auth.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/test_database_stress.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/test_mixed_scenarios.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/test_plans_quota.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/test_quota_direct.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/test_rbac_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/loadtests/utils.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/admin/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/admin/auth.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/admin/views.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/domain/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/domain/entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/domain/exceptions.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/dto/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/dto/requests.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/dto/responses.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/handlers/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/handlers/oauth_routes.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/models/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/models/refresh_token.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/models/user.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/models/user_provider.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/repositories/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/repositories/token_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/services/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/services/oauth_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/cli.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/config.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/kit.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/migrations.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/cache/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/cache/plan_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/dependencies.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/domain/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/domain/entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/domain/exceptions.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/dto/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/dto/responses.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/dto/usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/handlers/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/handlers/plan_routes.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/feature.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/feature_usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/plan.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/plan_limit.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/plan_permission.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/models/user_plan.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/repositories/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/repositories/plan_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/repositories/usage_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/services/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/plans/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/cache/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/cache/permission_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/dependencies.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/domain/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/domain/entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/domain/exceptions.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/models/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/models/permission.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/models/role.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/models/role_permission.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/repositories/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/repositories/rbac_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/services/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/services/rbac_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/rbac/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/audit.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/circuit_breaker.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/cleanup_scheduler.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/database.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/error_formatter.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/exception_handlers.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/exceptions.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/graceful_shutdown.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/health.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/http_utils.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/lockout.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/logging.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/metrics.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/models.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/rate_limiter.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/request_id.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/schemas.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/security.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/state_store.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/shared/uuid7.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_auth_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_circuit_breaker.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_csrf_state.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_dependencies.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_domain_entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_http_utils.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_lockout.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_lockout_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_oauth_service_resilience.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_security_comprehensive.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/auth/test_token_security.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/conftest.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/conftest.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_concurrent_limit_merge.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_concurrent_plan_operations.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_concurrent_token_operations.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_concurrent_user_creation.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_concurrent_user_deactivation.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_limit_boundaries.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_plan_cache_race.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_quota_concurrency.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_token_cleanup.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_token_repository.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_usage_periods.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/integration/test_user_repository.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/plans/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/plans/test_domain_entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/plans/test_period_edge_cases.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/plans/test_plan_cache_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/plans/test_plan_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/rbac/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/rbac/test_permission_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/rbac/test_permission_cache_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/rbac/test_rbac_cache_stampede.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/rbac/test_rbac_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_audit_pii_masking.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_circuit_breaker_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_cleanup_scheduler_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_config_validation.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_http_utils_security.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_rate_limiter.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_rate_limiter_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/tests/shared/test_state_store_concurrent.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "identity-plan-kit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.7"
|
|
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.
|
|
143
|
+
current_version = "0.2.7"
|
|
144
144
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
145
145
|
serialize = ["{major}.{minor}.{patch}"]
|
|
146
146
|
tag = true
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/src/identity_plan_kit/auth/dependencies.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Auth FastAPI dependencies."""
|
|
2
2
|
|
|
3
|
-
from
|
|
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))]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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()
|
|
@@ -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)
|
|
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
9
9
|
|
|
10
10
|
from identity_plan_kit.config import IdentityPlanKitConfig
|
|
11
11
|
from identity_plan_kit.plans.cache.plan_cache import PlanCache
|
|
12
|
+
from identity_plan_kit.plans.cache.user_plan_cache import UserPlanCache
|
|
12
13
|
from identity_plan_kit.plans.domain.entities import Feature, Plan, UserPlan
|
|
13
14
|
from identity_plan_kit.plans.domain.exceptions import (
|
|
14
15
|
FeatureNotAvailableError,
|
|
@@ -88,12 +89,14 @@ class PlanService:
|
|
|
88
89
|
config: IdentityPlanKitConfig,
|
|
89
90
|
session_factory: async_sessionmaker[AsyncSession],
|
|
90
91
|
plan_cache_ttl_seconds: int = DEFAULT_PLAN_CACHE_TTL_SECONDS,
|
|
92
|
+
user_plan_cache_ttl_seconds: int = DEFAULT_PLAN_CACHE_TTL_SECONDS,
|
|
91
93
|
state_store_manager: StateStoreManager | None = None,
|
|
92
94
|
authorization_callback: AuthorizationCallback | None = None,
|
|
93
95
|
) -> None:
|
|
94
96
|
self._config = config
|
|
95
97
|
self._session_factory = session_factory
|
|
96
98
|
self._plan_cache = PlanCache(ttl_seconds=plan_cache_ttl_seconds)
|
|
99
|
+
self._user_plan_cache = UserPlanCache(ttl_seconds=user_plan_cache_ttl_seconds)
|
|
97
100
|
self._state_store_manager = state_store_manager
|
|
98
101
|
self._authorization_callback = authorization_callback
|
|
99
102
|
|
|
@@ -234,12 +237,29 @@ class PlanService:
|
|
|
234
237
|
Database errors and other infrastructure failures will propagate.
|
|
235
238
|
"""
|
|
236
239
|
try:
|
|
240
|
+
# Check user plan cache first
|
|
241
|
+
cached_user_plan = await self._user_plan_cache.get(user_id)
|
|
242
|
+
if cached_user_plan is not None:
|
|
243
|
+
_user_plan, plan = cached_user_plan
|
|
244
|
+
limit = plan.get_feature_limit(feature_code)
|
|
245
|
+
return limit is not None
|
|
246
|
+
|
|
237
247
|
async with self._create_uow(session=session) as uow:
|
|
238
|
-
|
|
239
|
-
|
|
248
|
+
# Fetch from database and cache
|
|
249
|
+
fetch_ts = self._user_plan_cache.get_fetch_timestamp()
|
|
250
|
+
result = await uow.plans.get_user_active_plan_with_details(user_id)
|
|
251
|
+
if result is None:
|
|
240
252
|
return False
|
|
241
253
|
|
|
242
|
-
|
|
254
|
+
user_plan, plan = result
|
|
255
|
+
|
|
256
|
+
# Cache the result
|
|
257
|
+
await self._user_plan_cache.set(
|
|
258
|
+
user_id, user_plan, plan, fetched_at=fetch_ts
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Extract limit from already-loaded plan (no extra query needed)
|
|
262
|
+
limit = plan.get_feature_limit(feature_code)
|
|
243
263
|
return limit is not None
|
|
244
264
|
|
|
245
265
|
except (UserPlanNotFoundError, FeatureNotAvailableError):
|
|
@@ -347,17 +367,41 @@ class PlanService:
|
|
|
347
367
|
exc_info=True,
|
|
348
368
|
)
|
|
349
369
|
|
|
370
|
+
# Check user plan cache first
|
|
371
|
+
cached_user_plan = await self._user_plan_cache.get(user_id)
|
|
372
|
+
user_plan: UserPlan | None = None
|
|
373
|
+
plan: Plan | None = None
|
|
374
|
+
|
|
375
|
+
if cached_user_plan is not None:
|
|
376
|
+
user_plan, plan = cached_user_plan
|
|
377
|
+
logger.debug(
|
|
378
|
+
"user_plan_cache_hit",
|
|
379
|
+
user_id=str(user_id),
|
|
380
|
+
plan_code=user_plan.plan_code,
|
|
381
|
+
)
|
|
382
|
+
|
|
350
383
|
async with self._create_uow(session=session) as uow:
|
|
351
|
-
#
|
|
352
|
-
user_plan
|
|
353
|
-
|
|
354
|
-
|
|
384
|
+
# If not in cache, fetch from database
|
|
385
|
+
if user_plan is None or plan is None:
|
|
386
|
+
fetch_ts = self._user_plan_cache.get_fetch_timestamp()
|
|
387
|
+
result = await uow.plans.get_user_active_plan_with_details(user_id)
|
|
388
|
+
if result is None:
|
|
389
|
+
raise UserPlanNotFoundError()
|
|
390
|
+
|
|
391
|
+
user_plan, plan = result
|
|
392
|
+
|
|
393
|
+
# Cache the result
|
|
394
|
+
await self._user_plan_cache.set(
|
|
395
|
+
user_id, user_plan, plan, fetched_at=fetch_ts
|
|
396
|
+
)
|
|
355
397
|
|
|
356
398
|
if user_plan.is_expired:
|
|
399
|
+
# Invalidate cache for expired plans
|
|
400
|
+
await self._user_plan_cache.invalidate(user_id)
|
|
357
401
|
raise PlanExpiredError()
|
|
358
402
|
|
|
359
|
-
#
|
|
360
|
-
limit =
|
|
403
|
+
# Extract limit from already-loaded plan (no extra query needed)
|
|
404
|
+
limit = plan.get_feature_limit(feature_code)
|
|
361
405
|
if limit is None:
|
|
362
406
|
raise FeatureNotAvailableError(feature_code, user_plan.plan_code)
|
|
363
407
|
|
|
@@ -464,12 +508,31 @@ class PlanService:
|
|
|
464
508
|
Returns:
|
|
465
509
|
UsageInfo with current usage
|
|
466
510
|
"""
|
|
511
|
+
# Check user plan cache first
|
|
512
|
+
cached_user_plan = await self._user_plan_cache.get(user_id)
|
|
513
|
+
user_plan: UserPlan | None = None
|
|
514
|
+
plan: Plan | None = None
|
|
515
|
+
|
|
516
|
+
if cached_user_plan is not None:
|
|
517
|
+
user_plan, plan = cached_user_plan
|
|
518
|
+
|
|
467
519
|
async with self._create_uow() as uow:
|
|
468
|
-
|
|
469
|
-
if user_plan is None:
|
|
470
|
-
|
|
520
|
+
# If not in cache, fetch from database
|
|
521
|
+
if user_plan is None or plan is None:
|
|
522
|
+
fetch_ts = self._user_plan_cache.get_fetch_timestamp()
|
|
523
|
+
result = await uow.plans.get_user_active_plan_with_details(user_id)
|
|
524
|
+
if result is None:
|
|
525
|
+
raise UserPlanNotFoundError()
|
|
526
|
+
|
|
527
|
+
user_plan, plan = result
|
|
528
|
+
|
|
529
|
+
# Cache the result
|
|
530
|
+
await self._user_plan_cache.set(
|
|
531
|
+
user_id, user_plan, plan, fetched_at=fetch_ts
|
|
532
|
+
)
|
|
471
533
|
|
|
472
|
-
limit
|
|
534
|
+
# Extract limit from already-loaded plan (no extra query needed)
|
|
535
|
+
limit = plan.get_feature_limit(feature_code)
|
|
473
536
|
if limit is None:
|
|
474
537
|
raise FeatureNotAvailableError(feature_code, user_plan.plan_code)
|
|
475
538
|
|
|
@@ -666,6 +729,9 @@ class PlanService:
|
|
|
666
729
|
custom_limits=custom_limits,
|
|
667
730
|
)
|
|
668
731
|
|
|
732
|
+
# Invalidate user plan cache
|
|
733
|
+
await self._user_plan_cache.invalidate(user_id)
|
|
734
|
+
|
|
669
735
|
logger.info(
|
|
670
736
|
"plan_assigned",
|
|
671
737
|
user_id=str(user_id),
|
|
@@ -726,6 +792,9 @@ class PlanService:
|
|
|
726
792
|
result = await uow.plans.cancel_user_plan(user_id, immediate=immediate)
|
|
727
793
|
|
|
728
794
|
if result:
|
|
795
|
+
# Invalidate user plan cache
|
|
796
|
+
await self._user_plan_cache.invalidate(user_id)
|
|
797
|
+
|
|
729
798
|
logger.info(
|
|
730
799
|
"plan_cancelled",
|
|
731
800
|
user_id=str(user_id),
|
|
@@ -795,6 +864,9 @@ class PlanService:
|
|
|
795
864
|
if updated_plan is None:
|
|
796
865
|
raise UserPlanNotFoundError()
|
|
797
866
|
|
|
867
|
+
# Invalidate user plan cache
|
|
868
|
+
await self._user_plan_cache.invalidate(user_id)
|
|
869
|
+
|
|
798
870
|
logger.info(
|
|
799
871
|
"plan_extended",
|
|
800
872
|
user_id=str(user_id),
|
|
@@ -877,6 +949,9 @@ class PlanService:
|
|
|
877
949
|
if updated_plan is None:
|
|
878
950
|
raise UserPlanNotFoundError()
|
|
879
951
|
|
|
952
|
+
# Invalidate user plan cache
|
|
953
|
+
await self._user_plan_cache.invalidate(user_id)
|
|
954
|
+
|
|
880
955
|
logger.info(
|
|
881
956
|
"plan_limits_updated",
|
|
882
957
|
user_id=str(user_id),
|
|
@@ -1041,6 +1116,26 @@ class PlanService:
|
|
|
1041
1116
|
"""
|
|
1042
1117
|
await self._plan_cache.invalidate_all()
|
|
1043
1118
|
|
|
1119
|
+
async def invalidate_user_plan_cache(self, user_id: UUID) -> None:
|
|
1120
|
+
"""
|
|
1121
|
+
Invalidate cached user plan by user ID.
|
|
1122
|
+
|
|
1123
|
+
Call this when a user's plan assignment changes.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
user_id: User UUID to invalidate
|
|
1127
|
+
"""
|
|
1128
|
+
await self._user_plan_cache.invalidate(user_id)
|
|
1129
|
+
logger.debug("user_plan_cache_invalidated", user_id=str(user_id))
|
|
1130
|
+
|
|
1131
|
+
async def invalidate_all_user_plan_cache(self) -> None:
|
|
1132
|
+
"""
|
|
1133
|
+
Invalidate all cached user plans.
|
|
1134
|
+
|
|
1135
|
+
Call this when plans are modified globally (e.g., plan limits change).
|
|
1136
|
+
"""
|
|
1137
|
+
await self._user_plan_cache.invalidate_all()
|
|
1138
|
+
|
|
1044
1139
|
# =========================================================================
|
|
1045
1140
|
# Plan Listing Methods (for public display)
|
|
1046
1141
|
# =========================================================================
|
|
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
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/fastapi_integration.py
RENAMED
|
File without changes
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/sample_alembic_env.py
RENAMED
|
File without changes
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/integration/webhook_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.7}/examples/playing/custom_error_formats.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|