solokit 0.1.1__py3-none-any.whl
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.
- solokit/__init__.py +10 -0
- solokit/__version__.py +3 -0
- solokit/cli.py +374 -0
- solokit/core/__init__.py +1 -0
- solokit/core/cache.py +102 -0
- solokit/core/command_runner.py +278 -0
- solokit/core/config.py +453 -0
- solokit/core/config_validator.py +204 -0
- solokit/core/constants.py +291 -0
- solokit/core/error_formatter.py +279 -0
- solokit/core/error_handlers.py +346 -0
- solokit/core/exceptions.py +1567 -0
- solokit/core/file_ops.py +309 -0
- solokit/core/logging_config.py +166 -0
- solokit/core/output.py +99 -0
- solokit/core/performance.py +57 -0
- solokit/core/protocols.py +141 -0
- solokit/core/types.py +312 -0
- solokit/deployment/__init__.py +1 -0
- solokit/deployment/executor.py +411 -0
- solokit/git/__init__.py +1 -0
- solokit/git/integration.py +619 -0
- solokit/init/__init__.py +41 -0
- solokit/init/claude_commands_installer.py +87 -0
- solokit/init/dependency_installer.py +313 -0
- solokit/init/docs_structure.py +90 -0
- solokit/init/env_generator.py +160 -0
- solokit/init/environment_validator.py +334 -0
- solokit/init/git_hooks_installer.py +71 -0
- solokit/init/git_setup.py +188 -0
- solokit/init/gitignore_updater.py +195 -0
- solokit/init/initial_commit.py +145 -0
- solokit/init/initial_scans.py +109 -0
- solokit/init/orchestrator.py +246 -0
- solokit/init/readme_generator.py +207 -0
- solokit/init/session_structure.py +239 -0
- solokit/init/template_installer.py +424 -0
- solokit/learning/__init__.py +1 -0
- solokit/learning/archiver.py +115 -0
- solokit/learning/categorizer.py +126 -0
- solokit/learning/curator.py +428 -0
- solokit/learning/extractor.py +352 -0
- solokit/learning/reporter.py +351 -0
- solokit/learning/repository.py +254 -0
- solokit/learning/similarity.py +342 -0
- solokit/learning/validator.py +144 -0
- solokit/project/__init__.py +1 -0
- solokit/project/init.py +1162 -0
- solokit/project/stack.py +436 -0
- solokit/project/sync_plugin.py +438 -0
- solokit/project/tree.py +375 -0
- solokit/quality/__init__.py +1 -0
- solokit/quality/api_validator.py +424 -0
- solokit/quality/checkers/__init__.py +25 -0
- solokit/quality/checkers/base.py +114 -0
- solokit/quality/checkers/context7.py +221 -0
- solokit/quality/checkers/custom.py +162 -0
- solokit/quality/checkers/deployment.py +323 -0
- solokit/quality/checkers/documentation.py +179 -0
- solokit/quality/checkers/formatting.py +161 -0
- solokit/quality/checkers/integration.py +394 -0
- solokit/quality/checkers/linting.py +159 -0
- solokit/quality/checkers/security.py +261 -0
- solokit/quality/checkers/spec_completeness.py +127 -0
- solokit/quality/checkers/tests.py +184 -0
- solokit/quality/env_validator.py +306 -0
- solokit/quality/gates.py +655 -0
- solokit/quality/reporters/__init__.py +10 -0
- solokit/quality/reporters/base.py +25 -0
- solokit/quality/reporters/console.py +98 -0
- solokit/quality/reporters/json_reporter.py +34 -0
- solokit/quality/results.py +98 -0
- solokit/session/__init__.py +1 -0
- solokit/session/briefing/__init__.py +245 -0
- solokit/session/briefing/documentation_loader.py +53 -0
- solokit/session/briefing/formatter.py +476 -0
- solokit/session/briefing/git_context.py +282 -0
- solokit/session/briefing/learning_loader.py +212 -0
- solokit/session/briefing/milestone_builder.py +78 -0
- solokit/session/briefing/orchestrator.py +137 -0
- solokit/session/briefing/stack_detector.py +51 -0
- solokit/session/briefing/tree_generator.py +52 -0
- solokit/session/briefing/work_item_loader.py +209 -0
- solokit/session/briefing.py +353 -0
- solokit/session/complete.py +1188 -0
- solokit/session/status.py +246 -0
- solokit/session/validate.py +452 -0
- solokit/templates/.claude/commands/end.md +109 -0
- solokit/templates/.claude/commands/init.md +159 -0
- solokit/templates/.claude/commands/learn-curate.md +88 -0
- solokit/templates/.claude/commands/learn-search.md +62 -0
- solokit/templates/.claude/commands/learn-show.md +69 -0
- solokit/templates/.claude/commands/learn.md +136 -0
- solokit/templates/.claude/commands/start.md +114 -0
- solokit/templates/.claude/commands/status.md +22 -0
- solokit/templates/.claude/commands/validate.md +27 -0
- solokit/templates/.claude/commands/work-delete.md +119 -0
- solokit/templates/.claude/commands/work-graph.md +139 -0
- solokit/templates/.claude/commands/work-list.md +26 -0
- solokit/templates/.claude/commands/work-new.md +114 -0
- solokit/templates/.claude/commands/work-next.md +25 -0
- solokit/templates/.claude/commands/work-show.md +24 -0
- solokit/templates/.claude/commands/work-update.md +141 -0
- solokit/templates/CHANGELOG.md +17 -0
- solokit/templates/WORK_ITEM_TYPES.md +141 -0
- solokit/templates/__init__.py +1 -0
- solokit/templates/bug_spec.md +217 -0
- solokit/templates/config.schema.json +150 -0
- solokit/templates/dashboard_refine/base/.gitignore +36 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
- solokit/templates/dashboard_refine/base/app/globals.css +60 -0
- solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
- solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
- solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
- solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
- solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
- solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
- solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
- solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
- solokit/templates/dashboard_refine/base/components.json +16 -0
- solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
- solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
- solokit/templates/dashboard_refine/base/next.config.ts +10 -0
- solokit/templates/dashboard_refine/base/package.json.template +40 -0
- solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
- solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
- solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
- solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
- solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
- solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
- solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
- solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
- solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
- solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
- solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
- solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/deployment_spec.md +500 -0
- solokit/templates/feature_spec.md +248 -0
- solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
- solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
- solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
- solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
- solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
- solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
- solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
- solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
- solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
- solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
- solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
- solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
- solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
- solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
- solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
- solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
- solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/git-hooks/prepare-commit-msg +24 -0
- solokit/templates/integration_test_spec.md +363 -0
- solokit/templates/learnings.json +15 -0
- solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
- solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
- solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
- solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
- solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
- solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
- solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
- solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
- solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
- solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
- solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
- solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
- solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
- solokit/templates/refactor_spec.md +287 -0
- solokit/templates/saas_t3/base/.gitignore +36 -0
- solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
- solokit/templates/saas_t3/base/app/globals.css +27 -0
- solokit/templates/saas_t3/base/app/layout.tsx +23 -0
- solokit/templates/saas_t3/base/app/page.tsx +31 -0
- solokit/templates/saas_t3/base/lib/api.tsx +77 -0
- solokit/templates/saas_t3/base/lib/utils.ts +13 -0
- solokit/templates/saas_t3/base/next.config.ts +7 -0
- solokit/templates/saas_t3/base/package.json.template +38 -0
- solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
- solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
- solokit/templates/saas_t3/base/server/api/root.ts +19 -0
- solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
- solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
- solokit/templates/saas_t3/base/server/db.ts +17 -0
- solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
- solokit/templates/saas_t3/base/tsconfig.json +27 -0
- solokit/templates/saas_t3/docker/Dockerfile +60 -0
- solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
- solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
- solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
- solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
- solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
- solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
- solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
- solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
- solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
- solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
- solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
- solokit/templates/security_spec.md +287 -0
- solokit/templates/stack-versions.yaml +617 -0
- solokit/templates/status_update.json +6 -0
- solokit/templates/template-registry.json +257 -0
- solokit/templates/work_items.json +11 -0
- solokit/testing/__init__.py +1 -0
- solokit/testing/integration_runner.py +550 -0
- solokit/testing/performance.py +637 -0
- solokit/visualization/__init__.py +1 -0
- solokit/visualization/dependency_graph.py +788 -0
- solokit/work_items/__init__.py +1 -0
- solokit/work_items/creator.py +217 -0
- solokit/work_items/delete.py +264 -0
- solokit/work_items/get_dependencies.py +185 -0
- solokit/work_items/get_dependents.py +113 -0
- solokit/work_items/get_metadata.py +121 -0
- solokit/work_items/get_next_recommendations.py +133 -0
- solokit/work_items/manager.py +235 -0
- solokit/work_items/milestones.py +137 -0
- solokit/work_items/query.py +376 -0
- solokit/work_items/repository.py +267 -0
- solokit/work_items/scheduler.py +184 -0
- solokit/work_items/spec_parser.py +838 -0
- solokit/work_items/spec_validator.py +493 -0
- solokit/work_items/updater.py +157 -0
- solokit/work_items/validator.py +205 -0
- solokit-0.1.1.dist-info/METADATA +640 -0
- solokit-0.1.1.dist-info/RECORD +323 -0
- solokit-0.1.1.dist-info/WHEEL +5 -0
- solokit-0.1.1.dist-info/entry_points.txt +2 -0
- solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
- solokit-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration test fixtures
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from httpx import ASGITransport, AsyncClient # type: ignore[import-not-found]
|
|
10
|
+
from sqlalchemy.ext.asyncio import create_async_engine # type: ignore[import-not-found]
|
|
11
|
+
from sqlalchemy.orm import sessionmaker # type: ignore[import-not-found]
|
|
12
|
+
from sqlmodel import SQLModel # type: ignore[import-not-found]
|
|
13
|
+
from sqlmodel.ext.asyncio.session import AsyncSession # type: ignore[import-not-found]
|
|
14
|
+
from src.api.dependencies import get_db # type: ignore[import-not-found]
|
|
15
|
+
from src.main import app # type: ignore[import-not-found]
|
|
16
|
+
|
|
17
|
+
# Use a separate test database for integration tests
|
|
18
|
+
INTEGRATION_DATABASE_URL = "sqlite+aiosqlite:///./test_integration.db"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture(scope="function")
|
|
22
|
+
async def integration_db_engine() -> AsyncGenerator[Any, None]:
|
|
23
|
+
"""Create a test database engine for integration tests."""
|
|
24
|
+
engine = create_async_engine(
|
|
25
|
+
INTEGRATION_DATABASE_URL,
|
|
26
|
+
echo=False,
|
|
27
|
+
future=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async with engine.begin() as conn:
|
|
31
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
32
|
+
|
|
33
|
+
yield engine
|
|
34
|
+
|
|
35
|
+
async with engine.begin() as conn:
|
|
36
|
+
await conn.run_sync(SQLModel.metadata.drop_all)
|
|
37
|
+
|
|
38
|
+
await engine.dispose()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture(scope="function")
|
|
42
|
+
async def integration_db_session(
|
|
43
|
+
integration_db_engine: Any,
|
|
44
|
+
) -> AsyncGenerator[AsyncSession, None]:
|
|
45
|
+
"""Create a test database session for integration tests."""
|
|
46
|
+
async_session_maker = sessionmaker(
|
|
47
|
+
integration_db_engine,
|
|
48
|
+
class_=AsyncSession,
|
|
49
|
+
expire_on_commit=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async with async_session_maker() as session:
|
|
53
|
+
yield session
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture(scope="function")
|
|
57
|
+
async def integration_client(
|
|
58
|
+
integration_db_session: AsyncSession,
|
|
59
|
+
) -> AsyncGenerator[AsyncClient, None]:
|
|
60
|
+
"""
|
|
61
|
+
Create a test client for integration tests.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
65
|
+
yield integration_db_session
|
|
66
|
+
|
|
67
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
68
|
+
|
|
69
|
+
async with AsyncClient(
|
|
70
|
+
transport=ASGITransport(app=app), base_url="http://testserver", timeout=30.0
|
|
71
|
+
) as ac: # nosec B113 - Test client timeout is set to 30 seconds
|
|
72
|
+
yield ac
|
|
73
|
+
|
|
74
|
+
app.dependency_overrides.clear()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for API endpoints
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from httpx import AsyncClient # type: ignore[import-not-found]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.integration
|
|
10
|
+
@pytest.mark.api
|
|
11
|
+
class TestItemAPIIntegration:
|
|
12
|
+
"""Integration tests for Item API endpoints."""
|
|
13
|
+
|
|
14
|
+
async def test_create_and_retrieve_item(self, integration_client: AsyncClient) -> None:
|
|
15
|
+
"""Test full cycle: create item and retrieve it."""
|
|
16
|
+
# Create an item
|
|
17
|
+
create_response = await integration_client.post(
|
|
18
|
+
"/api/v1/items",
|
|
19
|
+
json={
|
|
20
|
+
"name": "Integration Test Item",
|
|
21
|
+
"description": "Full stack test",
|
|
22
|
+
"price": 29.99,
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
assert create_response.status_code == 201
|
|
26
|
+
created_item = create_response.json()
|
|
27
|
+
assert created_item["name"] == "Integration Test Item"
|
|
28
|
+
item_id = created_item["id"]
|
|
29
|
+
|
|
30
|
+
# Retrieve the item
|
|
31
|
+
get_response = await integration_client.get(f"/api/v1/items/{item_id}")
|
|
32
|
+
assert get_response.status_code == 200
|
|
33
|
+
retrieved_item = get_response.json()
|
|
34
|
+
assert retrieved_item["id"] == item_id
|
|
35
|
+
assert retrieved_item["name"] == "Integration Test Item"
|
|
36
|
+
|
|
37
|
+
async def test_create_update_delete_workflow(self, integration_client: AsyncClient) -> None:
|
|
38
|
+
"""Test complete CRUD workflow."""
|
|
39
|
+
# Create
|
|
40
|
+
create_response = await integration_client.post(
|
|
41
|
+
"/api/v1/items",
|
|
42
|
+
json={"name": "Workflow Item", "price": 10.0},
|
|
43
|
+
)
|
|
44
|
+
assert create_response.status_code == 201
|
|
45
|
+
item_id = create_response.json()["id"]
|
|
46
|
+
|
|
47
|
+
# Update
|
|
48
|
+
update_response = await integration_client.patch(
|
|
49
|
+
f"/api/v1/items/{item_id}",
|
|
50
|
+
json={"name": "Updated Item", "price": 15.0},
|
|
51
|
+
)
|
|
52
|
+
assert update_response.status_code == 200
|
|
53
|
+
updated_item = update_response.json()
|
|
54
|
+
assert updated_item["name"] == "Updated Item"
|
|
55
|
+
assert updated_item["price"] == 15.0
|
|
56
|
+
|
|
57
|
+
# Verify update
|
|
58
|
+
get_response = await integration_client.get(f"/api/v1/items/{item_id}")
|
|
59
|
+
assert get_response.status_code == 200
|
|
60
|
+
assert get_response.json()["name"] == "Updated Item"
|
|
61
|
+
|
|
62
|
+
# Delete
|
|
63
|
+
delete_response = await integration_client.delete(f"/api/v1/items/{item_id}")
|
|
64
|
+
assert delete_response.status_code == 204
|
|
65
|
+
|
|
66
|
+
# Verify deletion
|
|
67
|
+
get_deleted_response = await integration_client.get(f"/api/v1/items/{item_id}")
|
|
68
|
+
assert get_deleted_response.status_code == 404
|
|
69
|
+
|
|
70
|
+
async def test_list_items_pagination(self, integration_client: AsyncClient) -> None:
|
|
71
|
+
"""Test listing items with pagination."""
|
|
72
|
+
# Create multiple items
|
|
73
|
+
for i in range(15):
|
|
74
|
+
await integration_client.post(
|
|
75
|
+
"/api/v1/items",
|
|
76
|
+
json={"name": f"Item {i}", "price": float(i)},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Test default pagination
|
|
80
|
+
response = await integration_client.get("/api/v1/items")
|
|
81
|
+
assert response.status_code == 200
|
|
82
|
+
items = response.json()
|
|
83
|
+
assert len(items) <= 100
|
|
84
|
+
|
|
85
|
+
# Test custom pagination
|
|
86
|
+
response = await integration_client.get("/api/v1/items?skip=5&limit=5")
|
|
87
|
+
assert response.status_code == 200
|
|
88
|
+
items = response.json()
|
|
89
|
+
assert len(items) == 5
|
|
90
|
+
|
|
91
|
+
async def test_validation_errors(self, integration_client: AsyncClient) -> None:
|
|
92
|
+
"""Test API validation errors."""
|
|
93
|
+
# Missing required field
|
|
94
|
+
response = await integration_client.post(
|
|
95
|
+
"/api/v1/items",
|
|
96
|
+
json={"description": "Missing name and price"},
|
|
97
|
+
)
|
|
98
|
+
assert response.status_code == 422
|
|
99
|
+
|
|
100
|
+
# Invalid price (must be > 0)
|
|
101
|
+
response = await integration_client.post(
|
|
102
|
+
"/api/v1/items",
|
|
103
|
+
json={"name": "Invalid Price", "price": -10.0},
|
|
104
|
+
)
|
|
105
|
+
assert response.status_code == 422
|
|
106
|
+
|
|
107
|
+
async def test_not_found_error(self, integration_client: AsyncClient) -> None:
|
|
108
|
+
"""Test 404 error for non-existent item."""
|
|
109
|
+
response = await integration_client.get("/api/v1/items/99999")
|
|
110
|
+
assert response.status_code == 404
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@pytest.mark.integration
|
|
114
|
+
class TestHealthEndpoints:
|
|
115
|
+
"""Integration tests for health check endpoints."""
|
|
116
|
+
|
|
117
|
+
async def test_health_endpoints_integration(self, integration_client: AsyncClient) -> None:
|
|
118
|
+
"""Test all health check endpoints."""
|
|
119
|
+
# Health check
|
|
120
|
+
response = await integration_client.get("/health")
|
|
121
|
+
assert response.status_code == 200
|
|
122
|
+
assert response.json()["status"] == "healthy"
|
|
123
|
+
|
|
124
|
+
# Readiness check
|
|
125
|
+
response = await integration_client.get("/health/ready")
|
|
126
|
+
assert response.status_code == 200
|
|
127
|
+
|
|
128
|
+
# Liveness check
|
|
129
|
+
response = await integration_client.get("/health/live")
|
|
130
|
+
assert response.status_code == 200
|
|
131
|
+
assert response.json()["status"] == "alive"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{project_name}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{project_description}"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"fastapi==0.115.6",
|
|
8
|
+
"uvicorn[standard]==0.34.0",
|
|
9
|
+
"pydantic==2.12.4",
|
|
10
|
+
"pydantic-core==2.41.5",
|
|
11
|
+
"pydantic-settings==2.11.0",
|
|
12
|
+
"sqlmodel==0.0.25",
|
|
13
|
+
"sqlalchemy==2.0.37",
|
|
14
|
+
"psycopg2-binary==2.9.10",
|
|
15
|
+
"alembic==1.14.0",
|
|
16
|
+
"python-dotenv==1.2.1",
|
|
17
|
+
"asyncpg==0.30.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest==8.3.4",
|
|
23
|
+
"pytest-cov==6.0.0",
|
|
24
|
+
"pytest-asyncio==0.25.2",
|
|
25
|
+
"httpx==0.28.1",
|
|
26
|
+
"aiosqlite==0.20.0",
|
|
27
|
+
"ruff==0.9.2",
|
|
28
|
+
"pyright==1.1.396",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
security = [
|
|
32
|
+
"detect-secrets==1.5.0",
|
|
33
|
+
"pip-audit==2.7.3",
|
|
34
|
+
"bandit[toml]==1.8.0",
|
|
35
|
+
"pre-commit==4.0.1",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
quality = [
|
|
39
|
+
"radon==6.0.1",
|
|
40
|
+
"vulture==2.14",
|
|
41
|
+
"mutmut==3.3.1",
|
|
42
|
+
"locust==2.42.2",
|
|
43
|
+
"semgrep==1.142.1",
|
|
44
|
+
"coverage[toml]==7.11.1",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
production = [
|
|
48
|
+
"prometheus-client==0.23.1",
|
|
49
|
+
"statsd==4.0.1",
|
|
50
|
+
"structlog==25.5.0",
|
|
51
|
+
"python-json-logger==4.0.0",
|
|
52
|
+
"fastapi-health==0.4.0",
|
|
53
|
+
"sentry-sdk==2.27.0",
|
|
54
|
+
"opentelemetry-instrumentation-fastapi==0.59b0",
|
|
55
|
+
"gunicorn==23.0.0",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[build-system]
|
|
59
|
+
requires = ["setuptools>=61.0"]
|
|
60
|
+
build-backend = "setuptools.build_meta"
|
|
61
|
+
|
|
62
|
+
[tool.setuptools]
|
|
63
|
+
packages = ["src"]
|
|
64
|
+
|
|
65
|
+
# Ruff configuration
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 100
|
|
68
|
+
target-version = "py311"
|
|
69
|
+
|
|
70
|
+
select = [
|
|
71
|
+
"E", # pycodestyle errors
|
|
72
|
+
"W", # pycodestyle warnings
|
|
73
|
+
"F", # pyflakes
|
|
74
|
+
"I", # isort
|
|
75
|
+
"N", # pep8-naming
|
|
76
|
+
"UP", # pyupgrade
|
|
77
|
+
"B", # flake8-bugbear
|
|
78
|
+
"S", # bandit security checks
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
ignore = [
|
|
82
|
+
"E501", # line too long (handled by formatter)
|
|
83
|
+
"S101", # assert used (we use asserts in tests)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
exclude = [
|
|
87
|
+
".git",
|
|
88
|
+
".venv",
|
|
89
|
+
"__pycache__",
|
|
90
|
+
"alembic/versions",
|
|
91
|
+
".pytest_cache",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.format]
|
|
95
|
+
quote-style = "double"
|
|
96
|
+
indent-style = "space"
|
|
97
|
+
|
|
98
|
+
[tool.ruff.isort]
|
|
99
|
+
known-first-party = ["src"]
|
|
100
|
+
|
|
101
|
+
[tool.ruff.per-file-ignores]
|
|
102
|
+
"tests/*" = ["S101"]
|
|
103
|
+
|
|
104
|
+
# Pytest configuration
|
|
105
|
+
[tool.pytest.ini_options]
|
|
106
|
+
asyncio_mode = "auto"
|
|
107
|
+
testpaths = ["tests"]
|
|
108
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
109
|
+
python_classes = ["Test*"]
|
|
110
|
+
python_functions = ["test_*"]
|
|
111
|
+
markers = [
|
|
112
|
+
"unit: Unit tests",
|
|
113
|
+
"integration: Integration tests",
|
|
114
|
+
"slow: Slow running tests",
|
|
115
|
+
"api: API tests",
|
|
116
|
+
"db: Database tests",
|
|
117
|
+
]
|
|
118
|
+
addopts = [
|
|
119
|
+
"-v",
|
|
120
|
+
"--strict-markers",
|
|
121
|
+
"--tb=short",
|
|
122
|
+
"--cov=src",
|
|
123
|
+
"--cov-report=term-missing",
|
|
124
|
+
"--cov-report=html",
|
|
125
|
+
"--cov-report=xml",
|
|
126
|
+
"--cov-fail-under=80",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Coverage configuration
|
|
130
|
+
[tool.coverage.run]
|
|
131
|
+
source = ["src"]
|
|
132
|
+
omit = [
|
|
133
|
+
"tests/*",
|
|
134
|
+
"alembic/*",
|
|
135
|
+
"*/__init__.py",
|
|
136
|
+
]
|
|
137
|
+
branch = true
|
|
138
|
+
|
|
139
|
+
[tool.coverage.report]
|
|
140
|
+
precision = 2
|
|
141
|
+
show_missing = true
|
|
142
|
+
skip_covered = false
|
|
143
|
+
fail_under = 80
|
|
144
|
+
exclude_lines = [
|
|
145
|
+
"pragma: no cover",
|
|
146
|
+
"def __repr__",
|
|
147
|
+
"raise AssertionError",
|
|
148
|
+
"raise NotImplementedError",
|
|
149
|
+
"if __name__ == .__main__.:",
|
|
150
|
+
"if TYPE_CHECKING:",
|
|
151
|
+
"@abstractmethod",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# Bandit security configuration
|
|
155
|
+
[tool.bandit]
|
|
156
|
+
exclude_dirs = ["tests", ".venv", "venv", "alembic/versions"]
|
|
157
|
+
skips = ["B101"]
|
|
158
|
+
|
|
159
|
+
# Scripts
|
|
160
|
+
[project.scripts]
|
|
161
|
+
dev = "uvicorn src.main:app --reload --host 0.0.0.0 --port 8000"
|
|
162
|
+
prod = "gunicorn src.main:app -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Production Dependencies for {project_name}
|
|
2
|
+
|
|
3
|
+
# Monitoring & Metrics
|
|
4
|
+
prometheus-client==0.23.1
|
|
5
|
+
statsd==4.0.1
|
|
6
|
+
|
|
7
|
+
# Structured Logging
|
|
8
|
+
structlog==25.5.0
|
|
9
|
+
python-json-logger==4.0.0
|
|
10
|
+
|
|
11
|
+
# Health Checks
|
|
12
|
+
fastapi-health==0.4.0
|
|
13
|
+
|
|
14
|
+
# Error Tracking
|
|
15
|
+
sentry-sdk==2.27.0
|
|
16
|
+
|
|
17
|
+
# OpenTelemetry (optional - may conflict with semgrep)
|
|
18
|
+
opentelemetry-api==1.38.0
|
|
19
|
+
opentelemetry-sdk==1.38.0
|
|
20
|
+
opentelemetry-instrumentation==0.59b0
|
|
21
|
+
opentelemetry-instrumentation-fastapi==0.59b0
|
|
22
|
+
opentelemetry-exporter-otlp==1.38.0
|
|
23
|
+
|
|
24
|
+
# Performance
|
|
25
|
+
gunicorn==23.0.0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus metrics endpoint
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Response # type: ignore[import-not-found]
|
|
6
|
+
from src.core.monitoring import get_metrics # type: ignore[import-not-found]
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get("/metrics")
|
|
12
|
+
async def metrics() -> Response:
|
|
13
|
+
"""
|
|
14
|
+
Prometheus metrics endpoint.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Response: Prometheus metrics in text format
|
|
18
|
+
"""
|
|
19
|
+
return get_metrics()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured logging configuration using structlog
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import structlog # type: ignore[import-not-found]
|
|
10
|
+
from src.core.config import settings # type: ignore[import-not-found]
|
|
11
|
+
from structlog.types import EventDict, Processor # type: ignore[import-not-found]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_app_context(logger: Any, method_name: str, event_dict: EventDict) -> EventDict:
|
|
15
|
+
"""
|
|
16
|
+
Add application context to log events.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
logger: Logger instance
|
|
20
|
+
method_name: Method name
|
|
21
|
+
event_dict: Event dictionary
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
EventDict: Updated event dictionary
|
|
25
|
+
"""
|
|
26
|
+
event_dict["app"] = settings.APP_NAME
|
|
27
|
+
event_dict["env"] = settings.ENVIRONMENT
|
|
28
|
+
return event_dict
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def configure_logging() -> None:
|
|
32
|
+
"""
|
|
33
|
+
Configure structured logging with structlog.
|
|
34
|
+
"""
|
|
35
|
+
# Determine log level
|
|
36
|
+
log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO) # type: ignore[attr-defined]
|
|
37
|
+
|
|
38
|
+
# Configure standard library logging
|
|
39
|
+
logging.basicConfig( # type: ignore[attr-defined]
|
|
40
|
+
format="%(message)s",
|
|
41
|
+
stream=sys.stdout,
|
|
42
|
+
level=log_level,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Configure structlog
|
|
46
|
+
processors: list[Processor] = [
|
|
47
|
+
structlog.contextvars.merge_contextvars,
|
|
48
|
+
add_app_context,
|
|
49
|
+
structlog.stdlib.add_log_level,
|
|
50
|
+
structlog.stdlib.add_logger_name,
|
|
51
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
52
|
+
structlog.processors.StackInfoRenderer(),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Add JSON or console rendering based on settings
|
|
56
|
+
if settings.LOG_FORMAT == "json":
|
|
57
|
+
processors.append(structlog.processors.JSONRenderer())
|
|
58
|
+
else:
|
|
59
|
+
processors.append(structlog.dev.ConsoleRenderer())
|
|
60
|
+
|
|
61
|
+
structlog.configure(
|
|
62
|
+
processors=processors,
|
|
63
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
64
|
+
context_class=dict,
|
|
65
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
66
|
+
cache_logger_on_first_use=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Initialize logging
|
|
71
|
+
configure_logging()
|
|
72
|
+
|
|
73
|
+
# Create logger instance
|
|
74
|
+
logger = structlog.get_logger()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus metrics setup for monitoring
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import Response # type: ignore[import-not-found]
|
|
6
|
+
from prometheus_client import ( # type: ignore[import-not-found]
|
|
7
|
+
CONTENT_TYPE_LATEST,
|
|
8
|
+
CollectorRegistry,
|
|
9
|
+
Counter,
|
|
10
|
+
Gauge,
|
|
11
|
+
Histogram,
|
|
12
|
+
generate_latest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Create a custom registry
|
|
16
|
+
registry = CollectorRegistry()
|
|
17
|
+
|
|
18
|
+
# Request metrics
|
|
19
|
+
http_requests_total = Counter(
|
|
20
|
+
"http_requests_total",
|
|
21
|
+
"Total HTTP requests",
|
|
22
|
+
["method", "endpoint", "status"],
|
|
23
|
+
registry=registry,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
http_request_duration_seconds = Histogram(
|
|
27
|
+
"http_request_duration_seconds",
|
|
28
|
+
"HTTP request duration in seconds",
|
|
29
|
+
["method", "endpoint"],
|
|
30
|
+
registry=registry,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Application metrics
|
|
34
|
+
active_users = Gauge(
|
|
35
|
+
"active_users",
|
|
36
|
+
"Number of active users",
|
|
37
|
+
registry=registry,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
database_connections = Gauge(
|
|
41
|
+
"database_connections",
|
|
42
|
+
"Number of active database connections",
|
|
43
|
+
registry=registry,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Business metrics
|
|
47
|
+
items_created_total = Counter(
|
|
48
|
+
"items_created_total",
|
|
49
|
+
"Total number of items created",
|
|
50
|
+
registry=registry,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
items_deleted_total = Counter(
|
|
54
|
+
"items_deleted_total",
|
|
55
|
+
"Total number of items deleted",
|
|
56
|
+
registry=registry,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_metrics() -> Response:
|
|
61
|
+
"""
|
|
62
|
+
Generate Prometheus metrics response.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Response: Prometheus metrics in text format
|
|
66
|
+
"""
|
|
67
|
+
metrics = generate_latest(registry)
|
|
68
|
+
return Response(content=metrics, media_type=CONTENT_TYPE_LATEST)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentry error tracking integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import sentry_sdk # type: ignore[import-not-found]
|
|
8
|
+
from sentry_sdk.integrations.fastapi import FastApiIntegration # type: ignore[import-not-found]
|
|
9
|
+
from sentry_sdk.integrations.sqlalchemy import (
|
|
10
|
+
SqlalchemyIntegration, # type: ignore[import-not-found]
|
|
11
|
+
)
|
|
12
|
+
from src.core.config import settings # type: ignore[import-not-found]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def initialize_sentry() -> None:
|
|
16
|
+
"""
|
|
17
|
+
Initialize Sentry SDK for error tracking.
|
|
18
|
+
Only initializes in non-development environments.
|
|
19
|
+
"""
|
|
20
|
+
# Only initialize Sentry in production/staging
|
|
21
|
+
if settings.ENVIRONMENT == "development":
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Check if Sentry DSN is configured
|
|
25
|
+
sentry_dsn = getattr(settings, "SENTRY_DSN", None)
|
|
26
|
+
if not sentry_dsn:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
sentry_sdk.init(
|
|
30
|
+
dsn=sentry_dsn,
|
|
31
|
+
environment=settings.ENVIRONMENT,
|
|
32
|
+
release=f"{settings.APP_NAME}@{settings.APP_VERSION}",
|
|
33
|
+
traces_sample_rate=0.1, # 10% of transactions for performance monitoring
|
|
34
|
+
profiles_sample_rate=0.1, # 10% of transactions for profiling
|
|
35
|
+
integrations=[
|
|
36
|
+
FastApiIntegration(),
|
|
37
|
+
SqlalchemyIntegration(),
|
|
38
|
+
],
|
|
39
|
+
# Set traces_sample_rate to 1.0 to capture 100% of transactions
|
|
40
|
+
# Adjust this value in production to reduce overhead
|
|
41
|
+
send_default_pii=False, # Don't send personally identifiable information
|
|
42
|
+
before_send=before_send_filter,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def before_send_filter(event: dict[str, Any], hint: dict[str, Any]) -> dict[str, Any] | None:
|
|
47
|
+
"""
|
|
48
|
+
Filter events before sending to Sentry.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
event: Sentry event
|
|
52
|
+
hint: Additional context
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict | None: Event to send or None to skip
|
|
56
|
+
"""
|
|
57
|
+
# Filter out specific exceptions or add custom logic
|
|
58
|
+
# Example: Don't send 404 errors
|
|
59
|
+
if "exc_info" in hint:
|
|
60
|
+
exc_type, exc_value, tb = hint["exc_info"]
|
|
61
|
+
if exc_type.__name__ == "HTTPException":
|
|
62
|
+
# Don't send HTTP 404 errors
|
|
63
|
+
if hasattr(exc_value, "status_code") and exc_value.status_code == 404:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return event
|