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,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error handling decorators and utilities.
|
|
3
|
+
|
|
4
|
+
Provides decorators for common error handling patterns like retry,
|
|
5
|
+
timeout, and logging. Separates error handling concerns from business logic.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from solokit.core.error_handlers import with_retry, log_errors
|
|
9
|
+
|
|
10
|
+
@log_errors()
|
|
11
|
+
@with_retry(max_attempts=3)
|
|
12
|
+
def load_config(path: str) -> dict:
|
|
13
|
+
return json.load(open(path))
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import functools
|
|
19
|
+
import logging
|
|
20
|
+
import subprocess
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any, Callable, Literal, TypeVar
|
|
23
|
+
|
|
24
|
+
from solokit.core.exceptions import ErrorCode, GitError, SolokitError, SubprocessError, SystemError
|
|
25
|
+
from solokit.core.exceptions import TimeoutError as SolokitTimeoutError
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def with_timeout(
|
|
33
|
+
seconds: int, operation_name: str
|
|
34
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
35
|
+
"""
|
|
36
|
+
Decorator to add timeout to function execution.
|
|
37
|
+
|
|
38
|
+
Note: This uses signal.alarm which only works on Unix systems.
|
|
39
|
+
On Windows, the timeout is not enforced but the function still runs.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
seconds: Timeout in seconds
|
|
43
|
+
operation_name: Name of operation for error message
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Decorator function
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
TimeoutError: If function exceeds timeout
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> @with_timeout(seconds=5, operation_name="fetch data")
|
|
53
|
+
... def fetch_data():
|
|
54
|
+
... time.sleep(10) # Will timeout
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
58
|
+
@functools.wraps(func)
|
|
59
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
60
|
+
try:
|
|
61
|
+
import signal
|
|
62
|
+
|
|
63
|
+
def timeout_handler(signum: int, frame: Any) -> None:
|
|
64
|
+
raise SolokitTimeoutError(operation=operation_name, timeout_seconds=seconds)
|
|
65
|
+
|
|
66
|
+
# Set up timeout signal (Unix only)
|
|
67
|
+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
|
68
|
+
signal.alarm(seconds)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
return func(*args, **kwargs)
|
|
72
|
+
finally:
|
|
73
|
+
signal.alarm(0)
|
|
74
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
75
|
+
except AttributeError:
|
|
76
|
+
# Windows doesn't have SIGALRM, just run without timeout
|
|
77
|
+
logger.warning(f"Timeout not supported on this platform for {operation_name}")
|
|
78
|
+
return func(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
return wrapper
|
|
81
|
+
|
|
82
|
+
return decorator
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def with_retry(
|
|
86
|
+
max_attempts: int = 3,
|
|
87
|
+
delay_seconds: float = 1.0,
|
|
88
|
+
backoff_multiplier: float = 2.0,
|
|
89
|
+
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
90
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
91
|
+
"""
|
|
92
|
+
Decorator to retry function on failure.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
max_attempts: Maximum number of attempts
|
|
96
|
+
delay_seconds: Initial delay between attempts
|
|
97
|
+
backoff_multiplier: Multiply delay by this after each attempt
|
|
98
|
+
exceptions: Tuple of exceptions to catch and retry
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Decorator function
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> @with_retry(max_attempts=3, delay_seconds=2.0)
|
|
105
|
+
... def load_file(path: Path) -> dict:
|
|
106
|
+
... return json.load(open(path))
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
110
|
+
@functools.wraps(func)
|
|
111
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
112
|
+
delay = delay_seconds
|
|
113
|
+
last_exception = None
|
|
114
|
+
|
|
115
|
+
for attempt in range(1, max_attempts + 1):
|
|
116
|
+
try:
|
|
117
|
+
return func(*args, **kwargs)
|
|
118
|
+
except exceptions as e:
|
|
119
|
+
last_exception = e
|
|
120
|
+
if attempt < max_attempts:
|
|
121
|
+
logger.warning(
|
|
122
|
+
f"Attempt {attempt}/{max_attempts} failed for {func.__name__}: {e}. "
|
|
123
|
+
f"Retrying in {delay}s..."
|
|
124
|
+
)
|
|
125
|
+
time.sleep(delay)
|
|
126
|
+
delay *= backoff_multiplier
|
|
127
|
+
else:
|
|
128
|
+
logger.error(f"All {max_attempts} attempts failed for {func.__name__}")
|
|
129
|
+
|
|
130
|
+
# Re-raise the last exception
|
|
131
|
+
if last_exception:
|
|
132
|
+
raise last_exception
|
|
133
|
+
# This should never happen, but mypy needs it
|
|
134
|
+
raise RuntimeError(f"{func.__name__} completed without returning or raising")
|
|
135
|
+
|
|
136
|
+
return wrapper
|
|
137
|
+
|
|
138
|
+
return decorator
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def log_errors(
|
|
142
|
+
logger_instance: logging.Logger | None = None,
|
|
143
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
144
|
+
"""
|
|
145
|
+
Decorator to log exceptions with structured data.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
logger_instance: Logger to use (defaults to function's module logger)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Decorator function
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> @log_errors()
|
|
155
|
+
... def process_work_item(item_id: str):
|
|
156
|
+
... # Business logic that may raise SolokitError
|
|
157
|
+
... pass
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
161
|
+
@functools.wraps(func)
|
|
162
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
163
|
+
log = logger_instance or logging.getLogger(func.__module__)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
return func(*args, **kwargs)
|
|
167
|
+
except SolokitError as e:
|
|
168
|
+
# Log structured error data
|
|
169
|
+
log.error(
|
|
170
|
+
f"{func.__name__} failed: {e.message}",
|
|
171
|
+
extra={
|
|
172
|
+
"error_code": e.code.value,
|
|
173
|
+
"error_category": e.category.value,
|
|
174
|
+
"context": e.context,
|
|
175
|
+
"function": func.__name__,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
raise
|
|
179
|
+
except Exception as e: # noqa: BLE001 - Logging decorator catches all for observability
|
|
180
|
+
# Log unexpected errors
|
|
181
|
+
log.exception(
|
|
182
|
+
f"{func.__name__} failed with unexpected error: {e}",
|
|
183
|
+
extra={"function": func.__name__},
|
|
184
|
+
)
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
return wrapper
|
|
188
|
+
|
|
189
|
+
return decorator
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def convert_subprocess_errors(func: Callable[..., T]) -> Callable[..., T]:
|
|
193
|
+
"""
|
|
194
|
+
Decorator to convert subprocess exceptions to SolokitError.
|
|
195
|
+
|
|
196
|
+
Converts FileNotFoundError and subprocess exceptions to structured errors.
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
>>> @convert_subprocess_errors
|
|
200
|
+
... def run_git_command(args: list[str]) -> str:
|
|
201
|
+
... result = subprocess.run(["git"] + args, check=True, capture_output=True, text=True)
|
|
202
|
+
... return result.stdout
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
@functools.wraps(func)
|
|
206
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
207
|
+
try:
|
|
208
|
+
return func(*args, **kwargs)
|
|
209
|
+
except subprocess.TimeoutExpired as e:
|
|
210
|
+
raise SolokitTimeoutError(
|
|
211
|
+
operation=f"subprocess: {' '.join(e.cmd) if isinstance(e.cmd, list) else e.cmd}",
|
|
212
|
+
timeout_seconds=int(e.timeout),
|
|
213
|
+
context={"stdout": e.stdout, "stderr": e.stderr},
|
|
214
|
+
) from e
|
|
215
|
+
except FileNotFoundError as e:
|
|
216
|
+
# Command not found (e.g., git not installed)
|
|
217
|
+
cmd_name = e.filename or "unknown command"
|
|
218
|
+
raise GitError(
|
|
219
|
+
message=f"Command not found: {cmd_name}",
|
|
220
|
+
code=ErrorCode.GIT_NOT_FOUND,
|
|
221
|
+
remediation=f"Install {cmd_name} or ensure it's in your PATH",
|
|
222
|
+
cause=e,
|
|
223
|
+
) from e
|
|
224
|
+
except subprocess.CalledProcessError as e:
|
|
225
|
+
raise SubprocessError(
|
|
226
|
+
command=" ".join(e.cmd) if isinstance(e.cmd, list) else str(e.cmd),
|
|
227
|
+
returncode=e.returncode,
|
|
228
|
+
stderr=e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr,
|
|
229
|
+
stdout=e.stdout.decode() if isinstance(e.stdout, bytes) else e.stdout,
|
|
230
|
+
) from e
|
|
231
|
+
|
|
232
|
+
return wrapper
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def convert_file_errors(func: Callable[..., T]) -> Callable[..., T]:
|
|
236
|
+
"""
|
|
237
|
+
Decorator to convert file operation exceptions to SolokitError.
|
|
238
|
+
|
|
239
|
+
Converts IOError, OSError, FileNotFoundError, etc. to structured errors.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
>>> @convert_file_errors
|
|
243
|
+
... def read_config(path: str) -> dict:
|
|
244
|
+
... with open(path) as f:
|
|
245
|
+
... return json.load(f)
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
@functools.wraps(func)
|
|
249
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
250
|
+
import builtins
|
|
251
|
+
|
|
252
|
+
from solokit.core.exceptions import FileNotFoundError as SolokitFileNotFoundError
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
return func(*args, **kwargs)
|
|
256
|
+
except builtins.FileNotFoundError as e:
|
|
257
|
+
# Catch FileNotFoundError first (it's a subclass of OSError)
|
|
258
|
+
file_path = getattr(e, "filename", "unknown")
|
|
259
|
+
raise SolokitFileNotFoundError(file_path=file_path) from e
|
|
260
|
+
except OSError as e:
|
|
261
|
+
# Extract file path from exception if available
|
|
262
|
+
file_path = getattr(e, "filename", "unknown")
|
|
263
|
+
raise SystemError(
|
|
264
|
+
message=f"File operation failed: {e}",
|
|
265
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
266
|
+
context={"file_path": file_path, "error": str(e)},
|
|
267
|
+
cause=e,
|
|
268
|
+
) from e
|
|
269
|
+
|
|
270
|
+
return wrapper
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ErrorContext:
|
|
274
|
+
"""
|
|
275
|
+
Context manager for handling errors with cleanup.
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> with ErrorContext("processing work item", work_item_id=item_id):
|
|
279
|
+
... # Do work that may raise errors
|
|
280
|
+
... process_item(item_id)
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self, operation: str, cleanup: Callable[[], None] | None = None, **context_data: Any
|
|
285
|
+
) -> None:
|
|
286
|
+
self.operation = operation
|
|
287
|
+
self.cleanup = cleanup
|
|
288
|
+
self.context_data = context_data
|
|
289
|
+
|
|
290
|
+
def __enter__(self) -> ErrorContext:
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]:
|
|
294
|
+
# Run cleanup regardless of success/failure
|
|
295
|
+
if self.cleanup:
|
|
296
|
+
try:
|
|
297
|
+
self.cleanup()
|
|
298
|
+
except Exception as e: # noqa: BLE001 - Cleanup must not fail the main operation
|
|
299
|
+
logger.error(f"Cleanup failed for {self.operation}: {e}")
|
|
300
|
+
|
|
301
|
+
# Add context to SolokitError if present
|
|
302
|
+
if exc_type and issubclass(exc_type, SolokitError):
|
|
303
|
+
exc_val.context.update(self.context_data)
|
|
304
|
+
|
|
305
|
+
# Don't suppress exception
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def safe_execute(
|
|
310
|
+
func: Callable[..., T],
|
|
311
|
+
*args: Any,
|
|
312
|
+
default: T | None = None,
|
|
313
|
+
log_errors: bool = True,
|
|
314
|
+
**kwargs: Any,
|
|
315
|
+
) -> T | None:
|
|
316
|
+
"""
|
|
317
|
+
Execute function and return default value on error instead of raising.
|
|
318
|
+
|
|
319
|
+
Useful for optional operations where failure should not stop execution.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
func: Function to execute
|
|
323
|
+
*args: Positional arguments for func
|
|
324
|
+
default: Value to return on error
|
|
325
|
+
log_errors: Whether to log errors
|
|
326
|
+
**kwargs: Keyword arguments for func
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Function result or default value
|
|
330
|
+
|
|
331
|
+
Example:
|
|
332
|
+
>>> result = safe_execute(
|
|
333
|
+
... load_optional_config,
|
|
334
|
+
... "/path/to/config.json",
|
|
335
|
+
... default={}
|
|
336
|
+
... )
|
|
337
|
+
"""
|
|
338
|
+
try:
|
|
339
|
+
return func(*args, **kwargs)
|
|
340
|
+
except Exception as e: # noqa: BLE001 - safe_execute is designed to catch all errors
|
|
341
|
+
if log_errors:
|
|
342
|
+
logger.warning(
|
|
343
|
+
f"Optional operation failed: {func.__name__}: {e}",
|
|
344
|
+
extra={"function": func.__name__, "error": str(e)},
|
|
345
|
+
)
|
|
346
|
+
return default
|