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
solokit/core/file_ops.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""File operations utilities
|
|
2
|
+
|
|
3
|
+
Centralized file I/O operations with consistent error handling and atomicity guarantees.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from solokit.core.exceptions import (
|
|
13
|
+
ErrorCode,
|
|
14
|
+
FileOperationError,
|
|
15
|
+
SystemError,
|
|
16
|
+
)
|
|
17
|
+
from solokit.core.exceptions import (
|
|
18
|
+
FileNotFoundError as SolokitFileNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JSONFileOperations:
|
|
25
|
+
"""Centralized JSON file I/O with atomic writes and error handling"""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def load_json(
|
|
29
|
+
file_path: Path,
|
|
30
|
+
default: Optional[dict[str, Any]] = None,
|
|
31
|
+
validator: Optional[Callable[[dict], bool]] = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Load JSON file with optional default and validation
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to JSON file
|
|
38
|
+
default: Default value if file doesn't exist (None raises error)
|
|
39
|
+
validator: Optional validation function that returns True if data is valid
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Loaded JSON data as dict
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
FileOperationError: If file not found and no default provided
|
|
46
|
+
FileOperationError: If JSON is invalid
|
|
47
|
+
FileOperationError: If validation fails
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> # Load required file (raises if missing)
|
|
51
|
+
>>> data = JSONFileOperations.load_json(Path("config.json"))
|
|
52
|
+
>>> # Load with default
|
|
53
|
+
>>> data = JSONFileOperations.load_json(Path("optional.json"), default={})
|
|
54
|
+
>>> # Load with validation
|
|
55
|
+
>>> validator = lambda d: "version" in d
|
|
56
|
+
>>> data = JSONFileOperations.load_json(Path("config.json"), validator=validator)
|
|
57
|
+
"""
|
|
58
|
+
if not file_path.exists():
|
|
59
|
+
if default is not None:
|
|
60
|
+
logger.debug(f"File not found, using default: {file_path}")
|
|
61
|
+
return default
|
|
62
|
+
raise FileOperationError(
|
|
63
|
+
operation="read",
|
|
64
|
+
file_path=str(file_path),
|
|
65
|
+
details="File does not exist",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(file_path, encoding="utf-8") as f:
|
|
70
|
+
data: dict[str, Any] = json.load(f)
|
|
71
|
+
except json.JSONDecodeError as e:
|
|
72
|
+
raise FileOperationError(
|
|
73
|
+
operation="parse",
|
|
74
|
+
file_path=str(file_path),
|
|
75
|
+
details=f"Invalid JSON: {e}",
|
|
76
|
+
cause=e,
|
|
77
|
+
) from e
|
|
78
|
+
except OSError as e:
|
|
79
|
+
raise FileOperationError(
|
|
80
|
+
operation="read",
|
|
81
|
+
file_path=str(file_path),
|
|
82
|
+
details=str(e),
|
|
83
|
+
cause=e,
|
|
84
|
+
) from e
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise SystemError(
|
|
87
|
+
message=f"Unexpected error reading {file_path}",
|
|
88
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
89
|
+
context={"file_path": str(file_path), "operation": "read"},
|
|
90
|
+
cause=e,
|
|
91
|
+
) from e
|
|
92
|
+
|
|
93
|
+
if validator and not validator(data):
|
|
94
|
+
raise FileOperationError(
|
|
95
|
+
operation="validate",
|
|
96
|
+
file_path=str(file_path),
|
|
97
|
+
details="Validation function returned False",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
logger.debug(f"Loaded JSON from {file_path}")
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def save_json(
|
|
105
|
+
file_path: Path,
|
|
106
|
+
data: dict[str, Any],
|
|
107
|
+
indent: int = 2,
|
|
108
|
+
atomic: bool = True,
|
|
109
|
+
create_dirs: bool = True,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Save data to JSON file with atomic write option
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
file_path: Path to JSON file
|
|
116
|
+
data: Data to save
|
|
117
|
+
indent: JSON indentation (default 2)
|
|
118
|
+
atomic: Use atomic write via temp file (default True)
|
|
119
|
+
create_dirs: Create parent directories if needed (default True)
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
FileOperationError: If save fails
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
>>> # Save with atomic write (default)
|
|
126
|
+
>>> JSONFileOperations.save_json(Path("data.json"), {"key": "value"})
|
|
127
|
+
>>> # Save without atomic write
|
|
128
|
+
>>> JSONFileOperations.save_json(Path("data.json"), data, atomic=False)
|
|
129
|
+
>>> # Save with custom indent
|
|
130
|
+
>>> JSONFileOperations.save_json(Path("data.json"), data, indent=4)
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
if create_dirs:
|
|
134
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
if atomic:
|
|
137
|
+
# Atomic write via temp file
|
|
138
|
+
temp_path = file_path.with_suffix(file_path.suffix + ".tmp")
|
|
139
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
140
|
+
json.dump(data, f, indent=indent, default=str)
|
|
141
|
+
temp_path.replace(file_path)
|
|
142
|
+
else:
|
|
143
|
+
# Direct write
|
|
144
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
145
|
+
json.dump(data, f, indent=indent, default=str)
|
|
146
|
+
|
|
147
|
+
logger.debug(f"Saved JSON to {file_path}")
|
|
148
|
+
|
|
149
|
+
except OSError as e:
|
|
150
|
+
raise FileOperationError(
|
|
151
|
+
operation="write",
|
|
152
|
+
file_path=str(file_path),
|
|
153
|
+
details=str(e),
|
|
154
|
+
cause=e,
|
|
155
|
+
) from e
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise SystemError(
|
|
158
|
+
message=f"Unexpected error saving to {file_path}",
|
|
159
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
160
|
+
context={
|
|
161
|
+
"file_path": str(file_path),
|
|
162
|
+
"operation": "write",
|
|
163
|
+
"atomic": atomic,
|
|
164
|
+
},
|
|
165
|
+
cause=e,
|
|
166
|
+
) from e
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def load_json_safe(file_path: Path, default: dict[str, Any]) -> dict[str, Any]:
|
|
170
|
+
"""
|
|
171
|
+
Load JSON with guaranteed return (never raises)
|
|
172
|
+
|
|
173
|
+
Logs errors but always returns a value.
|
|
174
|
+
Convenience method for cases where failures should be silent.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
file_path: Path to JSON file
|
|
178
|
+
default: Default value to return if load fails
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Loaded JSON data or default value
|
|
182
|
+
|
|
183
|
+
Examples:
|
|
184
|
+
>>> # Always returns a dict, never raises
|
|
185
|
+
>>> data = JSONFileOperations.load_json_safe(Path("config.json"), {})
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
return JSONFileOperations.load_json(file_path)
|
|
189
|
+
except FileOperationError as e:
|
|
190
|
+
logger.warning(f"Using default for {file_path}: {e}")
|
|
191
|
+
return default
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Convenience functions for backward compatibility
|
|
195
|
+
def load_json(file_path: Path) -> dict[str, Any]:
|
|
196
|
+
"""Load JSON file
|
|
197
|
+
|
|
198
|
+
Backward compatibility wrapper. Raises FileOperationError if file not found.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
file_path: Path to JSON file
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Loaded JSON data
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
FileOperationError: If file not found or JSON is invalid
|
|
208
|
+
"""
|
|
209
|
+
return JSONFileOperations.load_json(file_path)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def save_json(file_path: Path, data: dict[str, Any], indent: int = 2) -> None:
|
|
213
|
+
"""Save data to JSON file with atomic write
|
|
214
|
+
|
|
215
|
+
Backward compatibility wrapper.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
file_path: Path to JSON file
|
|
219
|
+
data: Data to save
|
|
220
|
+
indent: JSON indentation (default 2)
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
FileOperationError: If save fails
|
|
224
|
+
"""
|
|
225
|
+
JSONFileOperations.save_json(file_path, data, indent=indent)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def ensure_directory(path: Path) -> None:
|
|
229
|
+
"""Ensure directory exists"""
|
|
230
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def backup_file(file_path: Path) -> Path:
|
|
234
|
+
"""Create backup of a file
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
file_path: Path to file to backup
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Path to backup file
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
SolokitFileNotFoundError: If source file doesn't exist
|
|
244
|
+
FileOperationError: If backup operation fails
|
|
245
|
+
"""
|
|
246
|
+
if not file_path.exists():
|
|
247
|
+
raise SolokitFileNotFoundError(file_path=str(file_path), file_type="backup source")
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
backup_path = file_path.with_suffix(f"{file_path.suffix}.backup")
|
|
251
|
+
shutil.copy2(file_path, backup_path)
|
|
252
|
+
return backup_path
|
|
253
|
+
except OSError as e:
|
|
254
|
+
raise FileOperationError(
|
|
255
|
+
operation="backup",
|
|
256
|
+
file_path=str(file_path),
|
|
257
|
+
details=str(e),
|
|
258
|
+
cause=e,
|
|
259
|
+
) from e
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def read_file(file_path: Path) -> str:
|
|
263
|
+
"""Read file contents
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
file_path: Path to file to read
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
File contents as string
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
SolokitFileNotFoundError: If file doesn't exist
|
|
273
|
+
FileOperationError: If read operation fails
|
|
274
|
+
"""
|
|
275
|
+
if not file_path.exists():
|
|
276
|
+
raise SolokitFileNotFoundError(file_path=str(file_path), file_type="text file")
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
with open(file_path) as f:
|
|
280
|
+
return f.read()
|
|
281
|
+
except OSError as e:
|
|
282
|
+
raise FileOperationError(
|
|
283
|
+
operation="read",
|
|
284
|
+
file_path=str(file_path),
|
|
285
|
+
details=str(e),
|
|
286
|
+
cause=e,
|
|
287
|
+
) from e
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def write_file(file_path: Path, content: str) -> None:
|
|
291
|
+
"""Write content to file
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
file_path: Path to file to write
|
|
295
|
+
content: Content to write
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
FileOperationError: If write operation fails
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
with open(file_path, "w") as f:
|
|
302
|
+
f.write(content)
|
|
303
|
+
except OSError as e:
|
|
304
|
+
raise FileOperationError(
|
|
305
|
+
operation="write",
|
|
306
|
+
file_path=str(file_path),
|
|
307
|
+
details=str(e),
|
|
308
|
+
cause=e,
|
|
309
|
+
) from e
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Centralized logging configuration for Solokit."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StructuredFormatter(logging.Formatter):
|
|
13
|
+
"""Format logs as structured JSON."""
|
|
14
|
+
|
|
15
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Format log record as JSON.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
record: Log record to format
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
JSON-formatted log string
|
|
24
|
+
"""
|
|
25
|
+
log_data = {
|
|
26
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
27
|
+
"level": record.levelname,
|
|
28
|
+
"logger": record.name,
|
|
29
|
+
"message": record.getMessage(),
|
|
30
|
+
"module": record.module,
|
|
31
|
+
"function": record.funcName,
|
|
32
|
+
"line": record.lineno,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Add extra fields if present
|
|
36
|
+
if hasattr(record, "context"):
|
|
37
|
+
log_data["context"] = record.context
|
|
38
|
+
|
|
39
|
+
# Add exception info if present
|
|
40
|
+
if record.exc_info:
|
|
41
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
42
|
+
|
|
43
|
+
return json.dumps(log_data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HumanReadableFormatter(logging.Formatter):
|
|
47
|
+
"""Format logs for human reading."""
|
|
48
|
+
|
|
49
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Format log record for console.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
record: Log record to format
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Human-readable log string
|
|
58
|
+
"""
|
|
59
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
60
|
+
level = record.levelname
|
|
61
|
+
name = record.name
|
|
62
|
+
message = record.getMessage()
|
|
63
|
+
|
|
64
|
+
# Color code by level
|
|
65
|
+
level_colors = {
|
|
66
|
+
"DEBUG": "\033[36m", # Cyan
|
|
67
|
+
"INFO": "\033[32m", # Green
|
|
68
|
+
"WARNING": "\033[33m", # Yellow
|
|
69
|
+
"ERROR": "\033[31m", # Red
|
|
70
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
71
|
+
}
|
|
72
|
+
color = level_colors.get(level, "")
|
|
73
|
+
reset = "\033[0m"
|
|
74
|
+
|
|
75
|
+
formatted = f"{timestamp} {color}{level:8}{reset} {name} - {message}"
|
|
76
|
+
|
|
77
|
+
# Add exception if present
|
|
78
|
+
if record.exc_info:
|
|
79
|
+
formatted += "\n" + self.formatException(record.exc_info)
|
|
80
|
+
|
|
81
|
+
return formatted
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def setup_logging(
|
|
85
|
+
level: str = "INFO",
|
|
86
|
+
log_file: Optional[Path] = None,
|
|
87
|
+
structured: bool = False,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Configure logging for the application.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
94
|
+
log_file: Optional file path for log output
|
|
95
|
+
structured: Use structured JSON logging (default: False)
|
|
96
|
+
"""
|
|
97
|
+
root_logger = logging.getLogger("solokit")
|
|
98
|
+
root_logger.setLevel(getattr(logging, level.upper()))
|
|
99
|
+
|
|
100
|
+
# Remove existing handlers
|
|
101
|
+
root_logger.handlers.clear()
|
|
102
|
+
|
|
103
|
+
# Console handler
|
|
104
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
105
|
+
if structured:
|
|
106
|
+
console_handler.setFormatter(StructuredFormatter())
|
|
107
|
+
else:
|
|
108
|
+
console_handler.setFormatter(HumanReadableFormatter())
|
|
109
|
+
root_logger.addHandler(console_handler)
|
|
110
|
+
|
|
111
|
+
# File handler if specified
|
|
112
|
+
if log_file:
|
|
113
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
file_handler = logging.FileHandler(log_file)
|
|
115
|
+
if structured:
|
|
116
|
+
file_handler.setFormatter(StructuredFormatter())
|
|
117
|
+
else:
|
|
118
|
+
file_handler.setFormatter(HumanReadableFormatter())
|
|
119
|
+
root_logger.addHandler(file_handler)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_logger(name: str) -> logging.Logger:
|
|
123
|
+
"""
|
|
124
|
+
Get a logger for a module.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name: Logger name (usually __name__)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Configured logger instance
|
|
131
|
+
"""
|
|
132
|
+
return logging.getLogger(f"solokit.{name}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class LogContext:
|
|
136
|
+
"""Context manager for adding context to logs."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, logger: logging.Logger, **context: Any):
|
|
139
|
+
"""
|
|
140
|
+
Initialize with logger and context data.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
logger: Logger instance to add context to
|
|
144
|
+
**context: Key-value pairs to add as context
|
|
145
|
+
"""
|
|
146
|
+
self.logger = logger
|
|
147
|
+
self.context = context
|
|
148
|
+
self.old_factory: Optional[Callable[..., logging.LogRecord]] = None
|
|
149
|
+
|
|
150
|
+
def __enter__(self) -> "LogContext":
|
|
151
|
+
"""Add context to log records."""
|
|
152
|
+
old_factory = logging.getLogRecordFactory()
|
|
153
|
+
|
|
154
|
+
def record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
|
|
155
|
+
record = old_factory(*args, **kwargs)
|
|
156
|
+
record.context = self.context
|
|
157
|
+
return record
|
|
158
|
+
|
|
159
|
+
logging.setLogRecordFactory(record_factory)
|
|
160
|
+
self.old_factory = old_factory
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def __exit__(self, *args: Any) -> None:
|
|
164
|
+
"""Restore original factory."""
|
|
165
|
+
if self.old_factory is not None:
|
|
166
|
+
logging.setLogRecordFactory(self.old_factory)
|
solokit/core/output.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""User output handler separate from diagnostic logging."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OutputHandler:
|
|
7
|
+
"""Handle user-facing output separate from logs."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, quiet: bool = False):
|
|
10
|
+
"""
|
|
11
|
+
Initialize output handler.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
quiet: Suppress all non-error output
|
|
15
|
+
"""
|
|
16
|
+
self.quiet = quiet
|
|
17
|
+
|
|
18
|
+
def info(self, message: str) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Display info message to user.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
message: Message to display
|
|
24
|
+
"""
|
|
25
|
+
if not self.quiet:
|
|
26
|
+
print(message)
|
|
27
|
+
|
|
28
|
+
def success(self, message: str) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Display success message to user.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
message: Success message to display
|
|
34
|
+
"""
|
|
35
|
+
if not self.quiet:
|
|
36
|
+
print(f"✅ {message}")
|
|
37
|
+
|
|
38
|
+
def warning(self, message: str) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Display warning message to user.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
message: Warning message to display
|
|
44
|
+
"""
|
|
45
|
+
if not self.quiet:
|
|
46
|
+
print(f"⚠️ {message}")
|
|
47
|
+
|
|
48
|
+
def error(self, message: str) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Display error message to user.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: Error message to display
|
|
54
|
+
"""
|
|
55
|
+
print(f"❌ {message}", file=sys.stderr)
|
|
56
|
+
|
|
57
|
+
def progress(self, message: str) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Display progress message to user.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
message: Progress message to display
|
|
63
|
+
"""
|
|
64
|
+
if not self.quiet:
|
|
65
|
+
print(f"⏳ {message}")
|
|
66
|
+
|
|
67
|
+
def section(self, title: str) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Display section header.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
title: Section title
|
|
73
|
+
"""
|
|
74
|
+
if not self.quiet:
|
|
75
|
+
print(f"\n=== {title} ===\n")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Global output handler
|
|
79
|
+
_output_handler = OutputHandler()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_output() -> OutputHandler:
|
|
83
|
+
"""
|
|
84
|
+
Get the global output handler.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Global OutputHandler instance
|
|
88
|
+
"""
|
|
89
|
+
return _output_handler
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def set_quiet(quiet: bool) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Set quiet mode for output handler.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
quiet: True to suppress output, False to enable
|
|
98
|
+
"""
|
|
99
|
+
_output_handler.quiet = quiet
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Performance monitoring and profiling"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, Optional, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from solokit.core.logging_config import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def measure_time(operation: Optional[str] = None) -> Callable[[F], F]:
|
|
15
|
+
"""Decorator to measure function execution time"""
|
|
16
|
+
|
|
17
|
+
def decorator(func: F) -> F:
|
|
18
|
+
op_name = operation or f"{func.__module__}.{func.__name__}"
|
|
19
|
+
|
|
20
|
+
@wraps(func)
|
|
21
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
22
|
+
start = time.time()
|
|
23
|
+
try:
|
|
24
|
+
result = func(*args, **kwargs)
|
|
25
|
+
return result
|
|
26
|
+
finally:
|
|
27
|
+
duration = time.time() - start
|
|
28
|
+
if duration > 1.0: # Warn if > 1s
|
|
29
|
+
logger.warning(f"Slow operation: {op_name} took {duration:.3f}s")
|
|
30
|
+
elif duration > 0.1: # Log if > 100ms
|
|
31
|
+
logger.info(f"Performance: {op_name} took {duration:.3f}s")
|
|
32
|
+
|
|
33
|
+
return cast(F, wrapper)
|
|
34
|
+
|
|
35
|
+
return decorator
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Timer:
|
|
39
|
+
"""Context manager for timing code blocks"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, name: str):
|
|
42
|
+
"""Initialize timer with name"""
|
|
43
|
+
self.name = name
|
|
44
|
+
self.start: Optional[float] = None
|
|
45
|
+
self.duration: Optional[float] = None
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> "Timer":
|
|
48
|
+
"""Start timer"""
|
|
49
|
+
self.start = time.time()
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, *args: Any) -> None:
|
|
53
|
+
"""End timer and log"""
|
|
54
|
+
if self.start is not None:
|
|
55
|
+
self.duration = time.time() - self.start
|
|
56
|
+
if self.duration > 0.1:
|
|
57
|
+
logger.info(f"Performance: {self.name} took {self.duration:.3f}s")
|