truthound 1.0.8__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.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
"""Optimized cron expression parser and scheduler.
|
|
2
|
+
|
|
3
|
+
This module provides a high-performance cron expression parser with support
|
|
4
|
+
for standard and extended syntax, efficient next-run calculation, and
|
|
5
|
+
comprehensive validation.
|
|
6
|
+
|
|
7
|
+
Design Principles:
|
|
8
|
+
1. Immutable expressions: Thread-safe by design
|
|
9
|
+
2. Lazy evaluation: Parse once, evaluate many times
|
|
10
|
+
3. Efficient iteration: O(1) memory for next-run calculation
|
|
11
|
+
4. Extensible: Easy to add new field types or syntax
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import calendar
|
|
17
|
+
import re
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
from enum import Enum, auto
|
|
22
|
+
from functools import cached_property
|
|
23
|
+
from typing import Any, Iterator, Set, FrozenSet
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Exceptions
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CronParseError(ValueError):
|
|
32
|
+
"""Raised when cron expression parsing fails."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, expression: str = "", position: int = -1) -> None:
|
|
35
|
+
self.expression = expression
|
|
36
|
+
self.position = position
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# Field Types
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CronFieldType(Enum):
|
|
46
|
+
"""Types of cron fields."""
|
|
47
|
+
|
|
48
|
+
SECOND = auto()
|
|
49
|
+
MINUTE = auto()
|
|
50
|
+
HOUR = auto()
|
|
51
|
+
DAY_OF_MONTH = auto()
|
|
52
|
+
MONTH = auto()
|
|
53
|
+
DAY_OF_WEEK = auto()
|
|
54
|
+
YEAR = auto()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class FieldConstraints:
|
|
59
|
+
"""Constraints for a cron field."""
|
|
60
|
+
|
|
61
|
+
min_value: int
|
|
62
|
+
max_value: int
|
|
63
|
+
names: dict[str, int] = field(default_factory=dict)
|
|
64
|
+
supports_l: bool = False
|
|
65
|
+
supports_w: bool = False
|
|
66
|
+
supports_hash: bool = False
|
|
67
|
+
supports_question: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Field constraint definitions
|
|
71
|
+
FIELD_CONSTRAINTS: dict[CronFieldType, FieldConstraints] = {
|
|
72
|
+
CronFieldType.SECOND: FieldConstraints(0, 59),
|
|
73
|
+
CronFieldType.MINUTE: FieldConstraints(0, 59),
|
|
74
|
+
CronFieldType.HOUR: FieldConstraints(0, 23),
|
|
75
|
+
CronFieldType.DAY_OF_MONTH: FieldConstraints(
|
|
76
|
+
1, 31,
|
|
77
|
+
supports_l=True,
|
|
78
|
+
supports_w=True,
|
|
79
|
+
supports_question=True,
|
|
80
|
+
),
|
|
81
|
+
CronFieldType.MONTH: FieldConstraints(
|
|
82
|
+
1, 12,
|
|
83
|
+
names={
|
|
84
|
+
"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4,
|
|
85
|
+
"MAY": 5, "JUN": 6, "JUL": 7, "AUG": 8,
|
|
86
|
+
"SEP": 9, "OCT": 10, "NOV": 11, "DEC": 12,
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
CronFieldType.DAY_OF_WEEK: FieldConstraints(
|
|
90
|
+
0, 6,
|
|
91
|
+
names={
|
|
92
|
+
"SUN": 0, "MON": 1, "TUE": 2, "WED": 3,
|
|
93
|
+
"THU": 4, "FRI": 5, "SAT": 6,
|
|
94
|
+
},
|
|
95
|
+
supports_l=True,
|
|
96
|
+
supports_hash=True,
|
|
97
|
+
supports_question=True,
|
|
98
|
+
),
|
|
99
|
+
CronFieldType.YEAR: FieldConstraints(1970, 2099),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# Cron Field
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CronField:
|
|
109
|
+
"""Represents a parsed cron field.
|
|
110
|
+
|
|
111
|
+
A CronField contains the set of valid values for a particular field
|
|
112
|
+
(minute, hour, etc.) along with any special modifiers (L, W, #).
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
field_type: The type of field (MINUTE, HOUR, etc.)
|
|
116
|
+
values: Frozen set of valid integer values
|
|
117
|
+
is_any: True if field matches any value (*)
|
|
118
|
+
last: True if L modifier is present
|
|
119
|
+
weekday_nearest: Day number if W modifier is present
|
|
120
|
+
nth_weekday: Tuple of (weekday, nth) if # modifier is present
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
__slots__ = (
|
|
124
|
+
"_field_type",
|
|
125
|
+
"_values",
|
|
126
|
+
"_is_any",
|
|
127
|
+
"_last",
|
|
128
|
+
"_weekday_nearest",
|
|
129
|
+
"_nth_weekday",
|
|
130
|
+
"_original",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
field_type: CronFieldType,
|
|
136
|
+
values: FrozenSet[int],
|
|
137
|
+
*,
|
|
138
|
+
is_any: bool = False,
|
|
139
|
+
last: bool = False,
|
|
140
|
+
weekday_nearest: int | None = None,
|
|
141
|
+
nth_weekday: tuple[int, int] | None = None,
|
|
142
|
+
original: str = "",
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Initialize cron field.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
field_type: Type of this field.
|
|
148
|
+
values: Set of valid values.
|
|
149
|
+
is_any: Matches any value.
|
|
150
|
+
last: L modifier present.
|
|
151
|
+
weekday_nearest: W modifier value.
|
|
152
|
+
nth_weekday: (weekday, nth) for # modifier.
|
|
153
|
+
original: Original expression string.
|
|
154
|
+
"""
|
|
155
|
+
self._field_type = field_type
|
|
156
|
+
self._values = values
|
|
157
|
+
self._is_any = is_any
|
|
158
|
+
self._last = last
|
|
159
|
+
self._weekday_nearest = weekday_nearest
|
|
160
|
+
self._nth_weekday = nth_weekday
|
|
161
|
+
self._original = original
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def field_type(self) -> CronFieldType:
|
|
165
|
+
return self._field_type
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def values(self) -> FrozenSet[int]:
|
|
169
|
+
return self._values
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def is_any(self) -> bool:
|
|
173
|
+
return self._is_any
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def has_special(self) -> bool:
|
|
177
|
+
"""Check if field has special modifiers."""
|
|
178
|
+
return self._last or self._weekday_nearest is not None or self._nth_weekday is not None
|
|
179
|
+
|
|
180
|
+
def matches(self, value: int, context: "MatchContext | None" = None) -> bool:
|
|
181
|
+
"""Check if a value matches this field.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
value: Value to check.
|
|
185
|
+
context: Optional context for special modifiers.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if value matches.
|
|
189
|
+
"""
|
|
190
|
+
if self._is_any:
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
# Handle special modifiers
|
|
194
|
+
if context:
|
|
195
|
+
if self._last:
|
|
196
|
+
if self._field_type == CronFieldType.DAY_OF_MONTH:
|
|
197
|
+
last_day = calendar.monthrange(context.year, context.month)[1]
|
|
198
|
+
return value == last_day
|
|
199
|
+
elif self._field_type == CronFieldType.DAY_OF_WEEK:
|
|
200
|
+
# Last occurrence of weekday in month
|
|
201
|
+
return self._is_last_weekday(value, context)
|
|
202
|
+
|
|
203
|
+
if self._weekday_nearest is not None:
|
|
204
|
+
return self._is_nearest_weekday(value, context)
|
|
205
|
+
|
|
206
|
+
if self._nth_weekday is not None:
|
|
207
|
+
return self._is_nth_weekday(value, context)
|
|
208
|
+
|
|
209
|
+
return value in self._values
|
|
210
|
+
|
|
211
|
+
def _is_last_weekday(self, value: int, context: "MatchContext") -> bool:
|
|
212
|
+
"""Check if value is last occurrence of weekday in month."""
|
|
213
|
+
if not self._values:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
target_weekday = next(iter(self._values))
|
|
217
|
+
last_day = calendar.monthrange(context.year, context.month)[1]
|
|
218
|
+
|
|
219
|
+
# Find last occurrence of target weekday
|
|
220
|
+
for day in range(last_day, 0, -1):
|
|
221
|
+
dt = datetime(context.year, context.month, day)
|
|
222
|
+
if dt.weekday() == (target_weekday - 1) % 7: # Convert to Python weekday
|
|
223
|
+
return context.day == day
|
|
224
|
+
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def _is_nearest_weekday(self, value: int, context: "MatchContext") -> bool:
|
|
228
|
+
"""Check if value is nearest weekday to specified day."""
|
|
229
|
+
target_day = self._weekday_nearest
|
|
230
|
+
if target_day is None:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
dt = datetime(context.year, context.month, target_day)
|
|
234
|
+
weekday = dt.weekday()
|
|
235
|
+
|
|
236
|
+
if weekday == 5: # Saturday -> Friday
|
|
237
|
+
nearest = target_day - 1
|
|
238
|
+
elif weekday == 6: # Sunday -> Monday
|
|
239
|
+
nearest = target_day + 1
|
|
240
|
+
else:
|
|
241
|
+
nearest = target_day
|
|
242
|
+
|
|
243
|
+
# Handle month boundaries
|
|
244
|
+
last_day = calendar.monthrange(context.year, context.month)[1]
|
|
245
|
+
nearest = max(1, min(nearest, last_day))
|
|
246
|
+
|
|
247
|
+
return context.day == nearest
|
|
248
|
+
|
|
249
|
+
def _is_nth_weekday(self, value: int, context: "MatchContext") -> bool:
|
|
250
|
+
"""Check if value is nth occurrence of weekday in month."""
|
|
251
|
+
if self._nth_weekday is None:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
target_weekday, nth = self._nth_weekday
|
|
255
|
+
|
|
256
|
+
# Find nth occurrence
|
|
257
|
+
count = 0
|
|
258
|
+
for day in range(1, calendar.monthrange(context.year, context.month)[1] + 1):
|
|
259
|
+
dt = datetime(context.year, context.month, day)
|
|
260
|
+
if dt.weekday() == (target_weekday - 1) % 7:
|
|
261
|
+
count += 1
|
|
262
|
+
if count == nth:
|
|
263
|
+
return context.day == day
|
|
264
|
+
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
def __repr__(self) -> str:
|
|
268
|
+
return f"CronField({self._field_type.name}, {self._original!r})"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class MatchContext:
|
|
273
|
+
"""Context for matching special cron modifiers."""
|
|
274
|
+
|
|
275
|
+
year: int
|
|
276
|
+
month: int
|
|
277
|
+
day: int
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# =============================================================================
|
|
281
|
+
# Cron Parser
|
|
282
|
+
# =============================================================================
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class CronParser:
|
|
286
|
+
"""Parser for cron expressions.
|
|
287
|
+
|
|
288
|
+
Supports:
|
|
289
|
+
- Standard 5-field cron (minute hour day month weekday)
|
|
290
|
+
- Extended 6-field cron (second minute hour day month weekday)
|
|
291
|
+
- Extended 7-field cron (second minute hour day month weekday year)
|
|
292
|
+
- Special expressions (@yearly, @monthly, etc.)
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
# Predefined expression aliases
|
|
296
|
+
ALIASES: dict[str, str] = {
|
|
297
|
+
"@yearly": "0 0 1 1 *",
|
|
298
|
+
"@annually": "0 0 1 1 *",
|
|
299
|
+
"@monthly": "0 0 1 * *",
|
|
300
|
+
"@weekly": "0 0 * * 0",
|
|
301
|
+
"@daily": "0 0 * * *",
|
|
302
|
+
"@midnight": "0 0 * * *",
|
|
303
|
+
"@hourly": "0 * * * *",
|
|
304
|
+
"@every_minute": "* * * * *",
|
|
305
|
+
"@every_second": "* * * * * *", # 6-field
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def __init__(self, expression: str) -> None:
|
|
309
|
+
"""Initialize parser with expression.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
expression: Cron expression string.
|
|
313
|
+
"""
|
|
314
|
+
self._original = expression.strip()
|
|
315
|
+
self._expression = self._resolve_alias(self._original)
|
|
316
|
+
self._fields: list[CronField] = []
|
|
317
|
+
|
|
318
|
+
def _resolve_alias(self, expression: str) -> str:
|
|
319
|
+
"""Resolve predefined aliases."""
|
|
320
|
+
lower = expression.lower()
|
|
321
|
+
if lower in self.ALIASES:
|
|
322
|
+
return self.ALIASES[lower]
|
|
323
|
+
return expression
|
|
324
|
+
|
|
325
|
+
def parse(self) -> list[CronField]:
|
|
326
|
+
"""Parse the cron expression.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of CronField objects.
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
CronParseError: If expression is invalid.
|
|
333
|
+
"""
|
|
334
|
+
parts = self._expression.split()
|
|
335
|
+
|
|
336
|
+
if len(parts) == 5:
|
|
337
|
+
# Standard: minute hour day month weekday
|
|
338
|
+
field_types = [
|
|
339
|
+
CronFieldType.MINUTE,
|
|
340
|
+
CronFieldType.HOUR,
|
|
341
|
+
CronFieldType.DAY_OF_MONTH,
|
|
342
|
+
CronFieldType.MONTH,
|
|
343
|
+
CronFieldType.DAY_OF_WEEK,
|
|
344
|
+
]
|
|
345
|
+
elif len(parts) == 6:
|
|
346
|
+
# Extended: second minute hour day month weekday
|
|
347
|
+
field_types = [
|
|
348
|
+
CronFieldType.SECOND,
|
|
349
|
+
CronFieldType.MINUTE,
|
|
350
|
+
CronFieldType.HOUR,
|
|
351
|
+
CronFieldType.DAY_OF_MONTH,
|
|
352
|
+
CronFieldType.MONTH,
|
|
353
|
+
CronFieldType.DAY_OF_WEEK,
|
|
354
|
+
]
|
|
355
|
+
elif len(parts) == 7:
|
|
356
|
+
# Extended with year: second minute hour day month weekday year
|
|
357
|
+
field_types = [
|
|
358
|
+
CronFieldType.SECOND,
|
|
359
|
+
CronFieldType.MINUTE,
|
|
360
|
+
CronFieldType.HOUR,
|
|
361
|
+
CronFieldType.DAY_OF_MONTH,
|
|
362
|
+
CronFieldType.MONTH,
|
|
363
|
+
CronFieldType.DAY_OF_WEEK,
|
|
364
|
+
CronFieldType.YEAR,
|
|
365
|
+
]
|
|
366
|
+
else:
|
|
367
|
+
raise CronParseError(
|
|
368
|
+
f"Invalid number of fields: {len(parts)}. "
|
|
369
|
+
"Expected 5, 6, or 7 fields.",
|
|
370
|
+
self._original,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
self._fields = [
|
|
374
|
+
self._parse_field(part, field_type)
|
|
375
|
+
for part, field_type in zip(parts, field_types)
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
return self._fields
|
|
379
|
+
|
|
380
|
+
def _parse_field(self, part: str, field_type: CronFieldType) -> CronField:
|
|
381
|
+
"""Parse a single cron field.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
part: Field expression string.
|
|
385
|
+
field_type: Type of this field.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Parsed CronField.
|
|
389
|
+
"""
|
|
390
|
+
constraints = FIELD_CONSTRAINTS[field_type]
|
|
391
|
+
original = part
|
|
392
|
+
|
|
393
|
+
# Handle ? (no specific value)
|
|
394
|
+
if part == "?":
|
|
395
|
+
if not constraints.supports_question:
|
|
396
|
+
raise CronParseError(
|
|
397
|
+
f"? not supported for {field_type.name}",
|
|
398
|
+
self._original,
|
|
399
|
+
)
|
|
400
|
+
return CronField(field_type, frozenset(), is_any=True, original=original)
|
|
401
|
+
|
|
402
|
+
# Handle * (any value)
|
|
403
|
+
if part == "*":
|
|
404
|
+
return CronField(field_type, frozenset(), is_any=True, original=original)
|
|
405
|
+
|
|
406
|
+
# Handle L (last)
|
|
407
|
+
if "L" in part.upper():
|
|
408
|
+
return self._parse_last(part, field_type, constraints, original)
|
|
409
|
+
|
|
410
|
+
# Handle W (weekday nearest) - only if it ends with W and is numeric+W
|
|
411
|
+
if part.upper().endswith("W") and constraints.supports_w:
|
|
412
|
+
# Check if it's actually a W modifier (number + W or LW)
|
|
413
|
+
prefix = part.upper()[:-1]
|
|
414
|
+
if prefix.isdigit() or prefix == "L":
|
|
415
|
+
return self._parse_weekday(part, field_type, constraints, original)
|
|
416
|
+
|
|
417
|
+
# Handle # (nth weekday)
|
|
418
|
+
if "#" in part:
|
|
419
|
+
return self._parse_nth(part, field_type, constraints, original)
|
|
420
|
+
|
|
421
|
+
# Parse normal expression (ranges, lists, steps)
|
|
422
|
+
values = self._parse_values(part, field_type, constraints)
|
|
423
|
+
|
|
424
|
+
return CronField(field_type, frozenset(values), original=original)
|
|
425
|
+
|
|
426
|
+
def _parse_last(
|
|
427
|
+
self,
|
|
428
|
+
part: str,
|
|
429
|
+
field_type: CronFieldType,
|
|
430
|
+
constraints: FieldConstraints,
|
|
431
|
+
original: str,
|
|
432
|
+
) -> CronField:
|
|
433
|
+
"""Parse L (last) modifier."""
|
|
434
|
+
if not constraints.supports_l:
|
|
435
|
+
raise CronParseError(
|
|
436
|
+
f"L not supported for {field_type.name}",
|
|
437
|
+
self._original,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
part_upper = part.upper()
|
|
441
|
+
|
|
442
|
+
if part_upper == "L":
|
|
443
|
+
# Just L - last day of month or last day of week
|
|
444
|
+
return CronField(field_type, frozenset(), last=True, original=original)
|
|
445
|
+
|
|
446
|
+
# Handle nL (e.g., 5L = last Friday)
|
|
447
|
+
if part_upper.endswith("L"):
|
|
448
|
+
weekday_str = part_upper[:-1]
|
|
449
|
+
weekday = self._resolve_value(weekday_str, constraints)
|
|
450
|
+
return CronField(
|
|
451
|
+
field_type,
|
|
452
|
+
frozenset([weekday]),
|
|
453
|
+
last=True,
|
|
454
|
+
original=original,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
raise CronParseError(f"Invalid L expression: {part}", self._original)
|
|
458
|
+
|
|
459
|
+
def _parse_weekday(
|
|
460
|
+
self,
|
|
461
|
+
part: str,
|
|
462
|
+
field_type: CronFieldType,
|
|
463
|
+
constraints: FieldConstraints,
|
|
464
|
+
original: str,
|
|
465
|
+
) -> CronField:
|
|
466
|
+
"""Parse W (nearest weekday) modifier."""
|
|
467
|
+
if not constraints.supports_w:
|
|
468
|
+
raise CronParseError(
|
|
469
|
+
f"W not supported for {field_type.name}",
|
|
470
|
+
self._original,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
part_upper = part.upper()
|
|
474
|
+
|
|
475
|
+
if part_upper == "LW":
|
|
476
|
+
# Last weekday of month
|
|
477
|
+
return CronField(field_type, frozenset(), last=True, original=original)
|
|
478
|
+
|
|
479
|
+
# Handle nW (e.g., 15W = nearest weekday to 15th)
|
|
480
|
+
if part_upper.endswith("W"):
|
|
481
|
+
day_str = part_upper[:-1]
|
|
482
|
+
day = int(day_str)
|
|
483
|
+
if day < constraints.min_value or day > constraints.max_value:
|
|
484
|
+
raise CronParseError(
|
|
485
|
+
f"Day {day} out of range for W",
|
|
486
|
+
self._original,
|
|
487
|
+
)
|
|
488
|
+
return CronField(
|
|
489
|
+
field_type,
|
|
490
|
+
frozenset(),
|
|
491
|
+
weekday_nearest=day,
|
|
492
|
+
original=original,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
raise CronParseError(f"Invalid W expression: {part}", self._original)
|
|
496
|
+
|
|
497
|
+
def _parse_nth(
|
|
498
|
+
self,
|
|
499
|
+
part: str,
|
|
500
|
+
field_type: CronFieldType,
|
|
501
|
+
constraints: FieldConstraints,
|
|
502
|
+
original: str,
|
|
503
|
+
) -> CronField:
|
|
504
|
+
"""Parse # (nth weekday) modifier."""
|
|
505
|
+
if not constraints.supports_hash:
|
|
506
|
+
raise CronParseError(
|
|
507
|
+
f"# not supported for {field_type.name}",
|
|
508
|
+
self._original,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
parts = part.split("#")
|
|
512
|
+
if len(parts) != 2:
|
|
513
|
+
raise CronParseError(f"Invalid # expression: {part}", self._original)
|
|
514
|
+
|
|
515
|
+
weekday = self._resolve_value(parts[0], constraints)
|
|
516
|
+
nth = int(parts[1])
|
|
517
|
+
|
|
518
|
+
if nth < 1 or nth > 5:
|
|
519
|
+
raise CronParseError(f"Invalid nth value: {nth}", self._original)
|
|
520
|
+
|
|
521
|
+
return CronField(
|
|
522
|
+
field_type,
|
|
523
|
+
frozenset([weekday]),
|
|
524
|
+
nth_weekday=(weekday, nth),
|
|
525
|
+
original=original,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def _parse_values(
|
|
529
|
+
self,
|
|
530
|
+
part: str,
|
|
531
|
+
field_type: CronFieldType,
|
|
532
|
+
constraints: FieldConstraints,
|
|
533
|
+
) -> set[int]:
|
|
534
|
+
"""Parse normal cron values (ranges, lists, steps)."""
|
|
535
|
+
values: set[int] = set()
|
|
536
|
+
|
|
537
|
+
# Handle comma-separated list
|
|
538
|
+
for segment in part.split(","):
|
|
539
|
+
segment = segment.strip()
|
|
540
|
+
|
|
541
|
+
# Handle step (*/n or n-m/s)
|
|
542
|
+
if "/" in segment:
|
|
543
|
+
values.update(self._parse_step(segment, constraints))
|
|
544
|
+
# Handle range (n-m)
|
|
545
|
+
elif "-" in segment:
|
|
546
|
+
values.update(self._parse_range(segment, constraints))
|
|
547
|
+
# Handle single value
|
|
548
|
+
else:
|
|
549
|
+
value = self._resolve_value(segment, constraints)
|
|
550
|
+
values.add(value)
|
|
551
|
+
|
|
552
|
+
return values
|
|
553
|
+
|
|
554
|
+
def _parse_step(
|
|
555
|
+
self,
|
|
556
|
+
segment: str,
|
|
557
|
+
constraints: FieldConstraints,
|
|
558
|
+
) -> set[int]:
|
|
559
|
+
"""Parse step expression (*/n or n-m/s)."""
|
|
560
|
+
parts = segment.split("/")
|
|
561
|
+
if len(parts) != 2:
|
|
562
|
+
raise CronParseError(f"Invalid step: {segment}", self._original)
|
|
563
|
+
|
|
564
|
+
step = int(parts[1])
|
|
565
|
+
if step <= 0:
|
|
566
|
+
raise CronParseError(f"Step must be positive: {step}", self._original)
|
|
567
|
+
|
|
568
|
+
base = parts[0]
|
|
569
|
+
|
|
570
|
+
if base == "*":
|
|
571
|
+
start = constraints.min_value
|
|
572
|
+
end = constraints.max_value
|
|
573
|
+
elif "-" in base:
|
|
574
|
+
range_parts = base.split("-")
|
|
575
|
+
start = self._resolve_value(range_parts[0], constraints)
|
|
576
|
+
end = self._resolve_value(range_parts[1], constraints)
|
|
577
|
+
else:
|
|
578
|
+
start = self._resolve_value(base, constraints)
|
|
579
|
+
end = constraints.max_value
|
|
580
|
+
|
|
581
|
+
return set(range(start, end + 1, step))
|
|
582
|
+
|
|
583
|
+
def _parse_range(
|
|
584
|
+
self,
|
|
585
|
+
segment: str,
|
|
586
|
+
constraints: FieldConstraints,
|
|
587
|
+
) -> set[int]:
|
|
588
|
+
"""Parse range expression (n-m)."""
|
|
589
|
+
parts = segment.split("-")
|
|
590
|
+
if len(parts) != 2:
|
|
591
|
+
raise CronParseError(f"Invalid range: {segment}", self._original)
|
|
592
|
+
|
|
593
|
+
start = self._resolve_value(parts[0], constraints)
|
|
594
|
+
end = self._resolve_value(parts[1], constraints)
|
|
595
|
+
|
|
596
|
+
if start > end:
|
|
597
|
+
# Handle wraparound (e.g., FRI-MON)
|
|
598
|
+
values = set(range(start, constraints.max_value + 1))
|
|
599
|
+
values.update(range(constraints.min_value, end + 1))
|
|
600
|
+
return values
|
|
601
|
+
|
|
602
|
+
return set(range(start, end + 1))
|
|
603
|
+
|
|
604
|
+
def _resolve_value(self, value: str, constraints: FieldConstraints) -> int:
|
|
605
|
+
"""Resolve a value (number or name) to integer."""
|
|
606
|
+
value = value.strip().upper()
|
|
607
|
+
|
|
608
|
+
# Check named values
|
|
609
|
+
if value in constraints.names:
|
|
610
|
+
return constraints.names[value]
|
|
611
|
+
|
|
612
|
+
# Parse as integer
|
|
613
|
+
try:
|
|
614
|
+
num = int(value)
|
|
615
|
+
if num < constraints.min_value or num > constraints.max_value:
|
|
616
|
+
raise CronParseError(
|
|
617
|
+
f"Value {num} out of range "
|
|
618
|
+
f"[{constraints.min_value}-{constraints.max_value}]",
|
|
619
|
+
self._original,
|
|
620
|
+
)
|
|
621
|
+
return num
|
|
622
|
+
except ValueError:
|
|
623
|
+
raise CronParseError(
|
|
624
|
+
f"Invalid value: {value}",
|
|
625
|
+
self._original,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# =============================================================================
|
|
630
|
+
# Cron Expression
|
|
631
|
+
# =============================================================================
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class CronExpression:
|
|
635
|
+
"""Parsed cron expression with efficient next-run calculation.
|
|
636
|
+
|
|
637
|
+
CronExpression is immutable and thread-safe. It can be used to:
|
|
638
|
+
- Check if a datetime matches the expression
|
|
639
|
+
- Calculate the next matching datetime
|
|
640
|
+
- Iterate over matching datetimes
|
|
641
|
+
|
|
642
|
+
Example:
|
|
643
|
+
>>> expr = CronExpression.parse("0 9 * * MON-FRI")
|
|
644
|
+
>>> expr.matches(datetime(2024, 1, 15, 9, 0)) # True (Monday)
|
|
645
|
+
>>> expr.next() # Next matching datetime
|
|
646
|
+
>>> list(expr.iter(limit=5)) # Next 5 matching datetimes
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
__slots__ = (
|
|
650
|
+
"_expression",
|
|
651
|
+
"_fields",
|
|
652
|
+
"_has_seconds",
|
|
653
|
+
"_has_years",
|
|
654
|
+
"_field_map",
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
def __init__(self, expression: str, fields: list[CronField]) -> None:
|
|
658
|
+
"""Initialize cron expression.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
expression: Original expression string.
|
|
662
|
+
fields: Parsed cron fields.
|
|
663
|
+
"""
|
|
664
|
+
self._expression = expression
|
|
665
|
+
self._fields = tuple(fields)
|
|
666
|
+
self._has_seconds = len(fields) >= 6
|
|
667
|
+
self._has_years = len(fields) >= 7
|
|
668
|
+
|
|
669
|
+
# Build field map for quick access
|
|
670
|
+
self._field_map: dict[CronFieldType, CronField] = {
|
|
671
|
+
f.field_type: f for f in fields
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
@classmethod
|
|
675
|
+
def parse(cls, expression: str) -> "CronExpression":
|
|
676
|
+
"""Parse a cron expression.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
expression: Cron expression string.
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
Parsed CronExpression.
|
|
683
|
+
|
|
684
|
+
Raises:
|
|
685
|
+
CronParseError: If expression is invalid.
|
|
686
|
+
"""
|
|
687
|
+
parser = CronParser(expression)
|
|
688
|
+
fields = parser.parse()
|
|
689
|
+
return cls(expression, fields)
|
|
690
|
+
|
|
691
|
+
@classmethod
|
|
692
|
+
def builder(cls) -> "CronBuilder":
|
|
693
|
+
"""Create a cron expression builder.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
CronBuilder for fluent construction.
|
|
697
|
+
"""
|
|
698
|
+
return CronBuilder()
|
|
699
|
+
|
|
700
|
+
# Predefined expression factories
|
|
701
|
+
@classmethod
|
|
702
|
+
def yearly(cls) -> "CronExpression":
|
|
703
|
+
return cls.parse("0 0 1 1 *")
|
|
704
|
+
|
|
705
|
+
@classmethod
|
|
706
|
+
def monthly(cls) -> "CronExpression":
|
|
707
|
+
return cls.parse("0 0 1 * *")
|
|
708
|
+
|
|
709
|
+
@classmethod
|
|
710
|
+
def weekly(cls) -> "CronExpression":
|
|
711
|
+
return cls.parse("0 0 * * 0")
|
|
712
|
+
|
|
713
|
+
@classmethod
|
|
714
|
+
def daily(cls) -> "CronExpression":
|
|
715
|
+
return cls.parse("0 0 * * *")
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def hourly(cls) -> "CronExpression":
|
|
719
|
+
return cls.parse("0 * * * *")
|
|
720
|
+
|
|
721
|
+
@classmethod
|
|
722
|
+
def every_n_minutes(cls, n: int) -> "CronExpression":
|
|
723
|
+
return cls.parse(f"*/{n} * * * *")
|
|
724
|
+
|
|
725
|
+
@classmethod
|
|
726
|
+
def every_n_hours(cls, n: int) -> "CronExpression":
|
|
727
|
+
return cls.parse(f"0 */{n} * * *")
|
|
728
|
+
|
|
729
|
+
@property
|
|
730
|
+
def expression(self) -> str:
|
|
731
|
+
"""Get original expression string."""
|
|
732
|
+
return self._expression
|
|
733
|
+
|
|
734
|
+
@property
|
|
735
|
+
def fields(self) -> tuple[CronField, ...]:
|
|
736
|
+
"""Get parsed fields."""
|
|
737
|
+
return self._fields
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def has_seconds(self) -> bool:
|
|
741
|
+
"""Check if expression includes seconds."""
|
|
742
|
+
return self._has_seconds
|
|
743
|
+
|
|
744
|
+
def get_field(self, field_type: CronFieldType) -> CronField | None:
|
|
745
|
+
"""Get a specific field by type."""
|
|
746
|
+
return self._field_map.get(field_type)
|
|
747
|
+
|
|
748
|
+
def matches(self, dt: datetime) -> bool:
|
|
749
|
+
"""Check if a datetime matches this expression.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
dt: Datetime to check.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
True if datetime matches.
|
|
756
|
+
"""
|
|
757
|
+
context = MatchContext(year=dt.year, month=dt.month, day=dt.day)
|
|
758
|
+
|
|
759
|
+
# Check each field
|
|
760
|
+
for field in self._fields:
|
|
761
|
+
if field.field_type == CronFieldType.SECOND:
|
|
762
|
+
if not field.matches(dt.second, context):
|
|
763
|
+
return False
|
|
764
|
+
elif field.field_type == CronFieldType.MINUTE:
|
|
765
|
+
if not field.matches(dt.minute, context):
|
|
766
|
+
return False
|
|
767
|
+
elif field.field_type == CronFieldType.HOUR:
|
|
768
|
+
if not field.matches(dt.hour, context):
|
|
769
|
+
return False
|
|
770
|
+
elif field.field_type == CronFieldType.DAY_OF_MONTH:
|
|
771
|
+
if not field.matches(dt.day, context):
|
|
772
|
+
return False
|
|
773
|
+
elif field.field_type == CronFieldType.MONTH:
|
|
774
|
+
if not field.matches(dt.month, context):
|
|
775
|
+
return False
|
|
776
|
+
elif field.field_type == CronFieldType.DAY_OF_WEEK:
|
|
777
|
+
# Python weekday: Monday=0, Sunday=6
|
|
778
|
+
# Cron weekday: Sunday=0, Saturday=6
|
|
779
|
+
cron_weekday = (dt.weekday() + 1) % 7
|
|
780
|
+
if not field.matches(cron_weekday, context):
|
|
781
|
+
return False
|
|
782
|
+
elif field.field_type == CronFieldType.YEAR:
|
|
783
|
+
if not field.matches(dt.year, context):
|
|
784
|
+
return False
|
|
785
|
+
|
|
786
|
+
return True
|
|
787
|
+
|
|
788
|
+
def next(self, after: datetime | None = None) -> datetime | None:
|
|
789
|
+
"""Get next matching datetime.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
after: Start searching after this datetime (default: now).
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Next matching datetime, or None if none found within 4 years.
|
|
796
|
+
"""
|
|
797
|
+
if after is None:
|
|
798
|
+
after = datetime.now()
|
|
799
|
+
|
|
800
|
+
# Start from next second/minute
|
|
801
|
+
if self._has_seconds:
|
|
802
|
+
current = after.replace(microsecond=0) + timedelta(seconds=1)
|
|
803
|
+
else:
|
|
804
|
+
current = after.replace(second=0, microsecond=0) + timedelta(minutes=1)
|
|
805
|
+
|
|
806
|
+
# Search limit: 4 years
|
|
807
|
+
end = current + timedelta(days=365 * 4)
|
|
808
|
+
|
|
809
|
+
while current < end:
|
|
810
|
+
if self.matches(current):
|
|
811
|
+
return current
|
|
812
|
+
|
|
813
|
+
# Advance to next candidate
|
|
814
|
+
current = self._advance(current)
|
|
815
|
+
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
def next_n(self, n: int, after: datetime | None = None) -> list[datetime]:
|
|
819
|
+
"""Get next n matching datetimes.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
n: Number of matches to find.
|
|
823
|
+
after: Start searching after this datetime.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
List of matching datetimes.
|
|
827
|
+
"""
|
|
828
|
+
results = []
|
|
829
|
+
current = after
|
|
830
|
+
|
|
831
|
+
for _ in range(n):
|
|
832
|
+
next_dt = self.next(current)
|
|
833
|
+
if next_dt is None:
|
|
834
|
+
break
|
|
835
|
+
results.append(next_dt)
|
|
836
|
+
current = next_dt
|
|
837
|
+
|
|
838
|
+
return results
|
|
839
|
+
|
|
840
|
+
def iter(
|
|
841
|
+
self,
|
|
842
|
+
after: datetime | None = None,
|
|
843
|
+
limit: int | None = None,
|
|
844
|
+
) -> "CronIterator":
|
|
845
|
+
"""Create iterator over matching datetimes.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
after: Start after this datetime.
|
|
849
|
+
limit: Maximum number of matches.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
CronIterator.
|
|
853
|
+
"""
|
|
854
|
+
return CronIterator(self, after, limit)
|
|
855
|
+
|
|
856
|
+
def _advance(self, current: datetime) -> datetime:
|
|
857
|
+
"""Advance to next candidate datetime.
|
|
858
|
+
|
|
859
|
+
Uses field constraints to skip non-matching times.
|
|
860
|
+
"""
|
|
861
|
+
# Check month
|
|
862
|
+
month_field = self._field_map.get(CronFieldType.MONTH)
|
|
863
|
+
if month_field and not month_field.is_any:
|
|
864
|
+
if current.month not in month_field.values:
|
|
865
|
+
# Skip to next valid month
|
|
866
|
+
for m in sorted(month_field.values):
|
|
867
|
+
if m > current.month:
|
|
868
|
+
return current.replace(month=m, day=1, hour=0, minute=0, second=0)
|
|
869
|
+
# Wrap to next year
|
|
870
|
+
return current.replace(
|
|
871
|
+
year=current.year + 1,
|
|
872
|
+
month=min(month_field.values),
|
|
873
|
+
day=1, hour=0, minute=0, second=0,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
# Check day of month
|
|
877
|
+
dom_field = self._field_map.get(CronFieldType.DAY_OF_MONTH)
|
|
878
|
+
if dom_field and not dom_field.is_any and not dom_field.has_special:
|
|
879
|
+
if current.day not in dom_field.values:
|
|
880
|
+
# Skip to next valid day
|
|
881
|
+
for d in sorted(dom_field.values):
|
|
882
|
+
if d > current.day:
|
|
883
|
+
try:
|
|
884
|
+
return current.replace(day=d, hour=0, minute=0, second=0)
|
|
885
|
+
except ValueError:
|
|
886
|
+
pass # Day doesn't exist in this month
|
|
887
|
+
# Wrap to next month
|
|
888
|
+
if current.month == 12:
|
|
889
|
+
return current.replace(
|
|
890
|
+
year=current.year + 1,
|
|
891
|
+
month=1, day=1, hour=0, minute=0, second=0,
|
|
892
|
+
)
|
|
893
|
+
return current.replace(
|
|
894
|
+
month=current.month + 1,
|
|
895
|
+
day=1, hour=0, minute=0, second=0,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
# Default: advance by smallest unit
|
|
899
|
+
if self._has_seconds:
|
|
900
|
+
return current + timedelta(seconds=1)
|
|
901
|
+
return current + timedelta(minutes=1)
|
|
902
|
+
|
|
903
|
+
def __repr__(self) -> str:
|
|
904
|
+
return f"CronExpression({self._expression!r})"
|
|
905
|
+
|
|
906
|
+
def __str__(self) -> str:
|
|
907
|
+
return self._expression
|
|
908
|
+
|
|
909
|
+
def __eq__(self, other: object) -> bool:
|
|
910
|
+
if isinstance(other, CronExpression):
|
|
911
|
+
return self._expression == other._expression
|
|
912
|
+
return NotImplemented
|
|
913
|
+
|
|
914
|
+
def __hash__(self) -> int:
|
|
915
|
+
return hash(self._expression)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
# =============================================================================
|
|
919
|
+
# Cron Iterator
|
|
920
|
+
# =============================================================================
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
class CronIterator(Iterator[datetime]):
|
|
924
|
+
"""Iterator over matching datetimes.
|
|
925
|
+
|
|
926
|
+
Efficiently iterates without storing all matches in memory.
|
|
927
|
+
"""
|
|
928
|
+
|
|
929
|
+
def __init__(
|
|
930
|
+
self,
|
|
931
|
+
expression: CronExpression,
|
|
932
|
+
after: datetime | None = None,
|
|
933
|
+
limit: int | None = None,
|
|
934
|
+
) -> None:
|
|
935
|
+
"""Initialize iterator.
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
expression: Cron expression.
|
|
939
|
+
after: Start after this datetime.
|
|
940
|
+
limit: Maximum matches.
|
|
941
|
+
"""
|
|
942
|
+
self._expression = expression
|
|
943
|
+
self._current = after
|
|
944
|
+
self._limit = limit
|
|
945
|
+
self._count = 0
|
|
946
|
+
|
|
947
|
+
def __iter__(self) -> "CronIterator":
|
|
948
|
+
return self
|
|
949
|
+
|
|
950
|
+
def __next__(self) -> datetime:
|
|
951
|
+
if self._limit is not None and self._count >= self._limit:
|
|
952
|
+
raise StopIteration
|
|
953
|
+
|
|
954
|
+
next_dt = self._expression.next(self._current)
|
|
955
|
+
if next_dt is None:
|
|
956
|
+
raise StopIteration
|
|
957
|
+
|
|
958
|
+
self._current = next_dt
|
|
959
|
+
self._count += 1
|
|
960
|
+
|
|
961
|
+
return next_dt
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
# =============================================================================
|
|
965
|
+
# Cron Builder
|
|
966
|
+
# =============================================================================
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class CronBuilder:
|
|
970
|
+
"""Fluent builder for cron expressions.
|
|
971
|
+
|
|
972
|
+
Example:
|
|
973
|
+
>>> expr = (CronBuilder()
|
|
974
|
+
... .at_minute(0, 30)
|
|
975
|
+
... .at_hour(9, 17)
|
|
976
|
+
... .on_weekdays()
|
|
977
|
+
... .build())
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
def __init__(self) -> None:
|
|
981
|
+
"""Initialize builder with defaults (every minute)."""
|
|
982
|
+
self._second: str = "0"
|
|
983
|
+
self._minute: str = "*"
|
|
984
|
+
self._hour: str = "*"
|
|
985
|
+
self._day_of_month: str = "*"
|
|
986
|
+
self._month: str = "*"
|
|
987
|
+
self._day_of_week: str = "*"
|
|
988
|
+
self._include_seconds: bool = False
|
|
989
|
+
|
|
990
|
+
def with_seconds(self) -> "CronBuilder":
|
|
991
|
+
"""Include seconds field in expression."""
|
|
992
|
+
self._include_seconds = True
|
|
993
|
+
return self
|
|
994
|
+
|
|
995
|
+
def at_second(self, *seconds: int) -> "CronBuilder":
|
|
996
|
+
"""Set specific seconds."""
|
|
997
|
+
self._include_seconds = True
|
|
998
|
+
self._second = ",".join(str(s) for s in seconds)
|
|
999
|
+
return self
|
|
1000
|
+
|
|
1001
|
+
def every_n_seconds(self, n: int) -> "CronBuilder":
|
|
1002
|
+
"""Run every n seconds."""
|
|
1003
|
+
self._include_seconds = True
|
|
1004
|
+
self._second = f"*/{n}"
|
|
1005
|
+
return self
|
|
1006
|
+
|
|
1007
|
+
def at_minute(self, *minutes: int) -> "CronBuilder":
|
|
1008
|
+
"""Set specific minutes."""
|
|
1009
|
+
self._minute = ",".join(str(m) for m in minutes)
|
|
1010
|
+
return self
|
|
1011
|
+
|
|
1012
|
+
def every_n_minutes(self, n: int) -> "CronBuilder":
|
|
1013
|
+
"""Run every n minutes."""
|
|
1014
|
+
self._minute = f"*/{n}"
|
|
1015
|
+
return self
|
|
1016
|
+
|
|
1017
|
+
def at_hour(self, *hours: int) -> "CronBuilder":
|
|
1018
|
+
"""Set specific hours."""
|
|
1019
|
+
self._hour = ",".join(str(h) for h in hours)
|
|
1020
|
+
return self
|
|
1021
|
+
|
|
1022
|
+
def every_n_hours(self, n: int) -> "CronBuilder":
|
|
1023
|
+
"""Run every n hours."""
|
|
1024
|
+
self._hour = f"*/{n}"
|
|
1025
|
+
return self
|
|
1026
|
+
|
|
1027
|
+
def on_day(self, *days: int) -> "CronBuilder":
|
|
1028
|
+
"""Set specific days of month."""
|
|
1029
|
+
self._day_of_month = ",".join(str(d) for d in days)
|
|
1030
|
+
return self
|
|
1031
|
+
|
|
1032
|
+
def on_last_day(self) -> "CronBuilder":
|
|
1033
|
+
"""Run on last day of month."""
|
|
1034
|
+
self._day_of_month = "L"
|
|
1035
|
+
return self
|
|
1036
|
+
|
|
1037
|
+
def on_weekday_nearest(self, day: int) -> "CronBuilder":
|
|
1038
|
+
"""Run on weekday nearest to specified day."""
|
|
1039
|
+
self._day_of_month = f"{day}W"
|
|
1040
|
+
return self
|
|
1041
|
+
|
|
1042
|
+
def in_month(self, *months: int | str) -> "CronBuilder":
|
|
1043
|
+
"""Set specific months."""
|
|
1044
|
+
self._month = ",".join(str(m).upper() for m in months)
|
|
1045
|
+
return self
|
|
1046
|
+
|
|
1047
|
+
def on_weekday(self, *weekdays: int | str) -> "CronBuilder":
|
|
1048
|
+
"""Set specific weekdays (0=SUN, 6=SAT)."""
|
|
1049
|
+
self._day_of_week = ",".join(str(w).upper() for w in weekdays)
|
|
1050
|
+
return self
|
|
1051
|
+
|
|
1052
|
+
def on_weekdays(self) -> "CronBuilder":
|
|
1053
|
+
"""Run Monday through Friday."""
|
|
1054
|
+
self._day_of_week = "MON-FRI"
|
|
1055
|
+
return self
|
|
1056
|
+
|
|
1057
|
+
def on_weekends(self) -> "CronBuilder":
|
|
1058
|
+
"""Run Saturday and Sunday."""
|
|
1059
|
+
self._day_of_week = "SAT,SUN"
|
|
1060
|
+
return self
|
|
1061
|
+
|
|
1062
|
+
def every_day(self) -> "CronBuilder":
|
|
1063
|
+
"""Run every day."""
|
|
1064
|
+
self._day_of_month = "*"
|
|
1065
|
+
self._day_of_week = "*"
|
|
1066
|
+
return self
|
|
1067
|
+
|
|
1068
|
+
def daily_at(self, hour: int, minute: int = 0) -> "CronBuilder":
|
|
1069
|
+
"""Run daily at specific time."""
|
|
1070
|
+
self._minute = str(minute)
|
|
1071
|
+
self._hour = str(hour)
|
|
1072
|
+
return self
|
|
1073
|
+
|
|
1074
|
+
def hourly_at(self, minute: int) -> "CronBuilder":
|
|
1075
|
+
"""Run hourly at specific minute."""
|
|
1076
|
+
self._minute = str(minute)
|
|
1077
|
+
return self
|
|
1078
|
+
|
|
1079
|
+
def build(self) -> CronExpression:
|
|
1080
|
+
"""Build the cron expression.
|
|
1081
|
+
|
|
1082
|
+
Returns:
|
|
1083
|
+
Parsed CronExpression.
|
|
1084
|
+
"""
|
|
1085
|
+
if self._include_seconds:
|
|
1086
|
+
expr = (
|
|
1087
|
+
f"{self._second} {self._minute} {self._hour} "
|
|
1088
|
+
f"{self._day_of_month} {self._month} {self._day_of_week}"
|
|
1089
|
+
)
|
|
1090
|
+
else:
|
|
1091
|
+
expr = (
|
|
1092
|
+
f"{self._minute} {self._hour} "
|
|
1093
|
+
f"{self._day_of_month} {self._month} {self._day_of_week}"
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
return CronExpression.parse(expr)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
# =============================================================================
|
|
1100
|
+
# Validation Functions
|
|
1101
|
+
# =============================================================================
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def validate_expression(expression: str) -> list[str]:
|
|
1105
|
+
"""Validate a cron expression.
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
expression: Cron expression to validate.
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
List of validation errors (empty if valid).
|
|
1112
|
+
"""
|
|
1113
|
+
errors = []
|
|
1114
|
+
|
|
1115
|
+
try:
|
|
1116
|
+
CronExpression.parse(expression)
|
|
1117
|
+
except CronParseError as e:
|
|
1118
|
+
errors.append(str(e))
|
|
1119
|
+
|
|
1120
|
+
return errors
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def is_valid_expression(expression: str) -> bool:
|
|
1124
|
+
"""Check if a cron expression is valid.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
expression: Cron expression to check.
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
True if valid.
|
|
1131
|
+
"""
|
|
1132
|
+
try:
|
|
1133
|
+
CronExpression.parse(expression)
|
|
1134
|
+
return True
|
|
1135
|
+
except CronParseError:
|
|
1136
|
+
return False
|