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.
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/PKG-INFO +1 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/pyproject.toml +2 -2
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/__init__.py +12 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dependencies.py +84 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/exceptions.py +21 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/repositories/user_repo.py +14 -6
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/services/auth_service.py +9 -2
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/__init__.py +8 -0
- identity_plan_kit-0.2.8/src/identity_plan_kit/plans/cache/user_plan_cache.py +205 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/services/plan_service.py +108 -13
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/domain/exceptions.py +11 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/error_formatter.py +10 -8
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/exception_handlers.py +93 -2
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/exceptions.py +13 -5
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/uv.lock +1 -1
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/.gitignore +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/Makefile +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/README.md +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/env.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/script.py.mako +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/versions/20250124_000000_initial_schema.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic/versions/20250127_000000_add_password_hash.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/alembic.ini +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/fastapi_integration.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/sample_alembic_env.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/integration/webhook_integration.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/admin_usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/basic_usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/custom_error_formats.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/extending_models.html +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/playing/extension_example.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/.env.production.example +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/Dockerfile +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/docker-compose.yml +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/nginx.conf +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/production_setup.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/examples/prod/prometheus.yml +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/README.md +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/config.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/locustfile.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_auth.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_database_stress.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_mixed_scenarios.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_plans_quota.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_quota_direct.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/test_rbac_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/loadtests/utils.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/admin/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/admin/auth.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/admin/views.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dto/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dto/requests.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/dto/responses.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/handlers/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/handlers/oauth_routes.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/refresh_token.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/user.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/models/user_provider.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/repositories/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/repositories/token_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/services/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/services/oauth_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/cli.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/config.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/kit.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/migrations.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/cache/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/cache/plan_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dependencies.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/domain/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/domain/entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/domain/exceptions.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dto/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dto/responses.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/dto/usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/handlers/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/handlers/plan_routes.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/feature.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/feature_usage.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/plan.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/plan_limit.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/plan_permission.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/models/user_plan.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/repositories/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/repositories/plan_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/repositories/usage_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/services/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/plans/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/cache/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/cache/permission_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/dependencies.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/domain/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/domain/entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/permission.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/role.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/models/role_permission.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/repositories/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/repositories/rbac_repo.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/services/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/services/rbac_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/rbac/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/audit.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/circuit_breaker.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/cleanup_scheduler.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/database.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/graceful_shutdown.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/health.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/http_utils.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/lockout.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/logging.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/metrics.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/models.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/rate_limiter.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/request_id.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/schemas.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/security.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/state_store.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/uow.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/shared/uuid7.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_auth_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_circuit_breaker.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_csrf_state.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_dependencies.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_domain_entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_http_utils.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_lockout.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_lockout_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_oauth_service_resilience.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_security_comprehensive.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/auth/test_token_security.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/conftest.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/conftest.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_limit_merge.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_plan_operations.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_token_operations.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_user_creation.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_concurrent_user_deactivation.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_limit_boundaries.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_plan_cache_race.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_quota_concurrency.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_token_cleanup.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_token_repository.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_usage_periods.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/integration/test_user_repository.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_domain_entities.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_period_edge_cases.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_plan_cache_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/plans/test_plan_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/__init__.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_permission_cache.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_permission_cache_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_rbac_cache_stampede.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/rbac/test_rbac_service.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_audit_pii_masking.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_circuit_breaker_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_cleanup_scheduler_concurrent.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_config_validation.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_http_utils_security.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_rate_limiter.py +0 -0
- {identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/tests/shared/test_rate_limiter_concurrent.py +0 -0
- {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
|
[project]
|
|
2
2
|
name = "identity-plan-kit"
|
|
3
|
-
version = "0.2.
|
|
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.
|
|
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",
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/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))]
|
{identity_plan_kit-0.2.6 → identity_plan_kit-0.2.8}/src/identity_plan_kit/auth/domain/exceptions.py
RENAMED
|
@@ -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
|
-
|
|
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()
|
|
@@ -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)
|