relationalai 0.13.0.dev0__py3-none-any.whl → 0.13.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.
- frontend/debugger/dist/.gitignore +2 -0
- frontend/debugger/dist/assets/favicon-Dy0ZgA6N.png +0 -0
- frontend/debugger/dist/assets/index-Cssla-O7.js +208 -0
- frontend/debugger/dist/assets/index-DlHsYx1V.css +9 -0
- frontend/debugger/dist/index.html +17 -0
- relationalai/__init__.py +256 -1
- relationalai/clients/__init__.py +18 -0
- relationalai/clients/client.py +912 -0
- relationalai/clients/config.py +673 -0
- relationalai/clients/direct_access_client.py +118 -0
- relationalai/clients/exec_txn_poller.py +91 -0
- relationalai/clients/hash_util.py +31 -0
- relationalai/clients/local.py +571 -0
- relationalai/clients/profile_polling.py +73 -0
- relationalai/clients/resources/__init__.py +8 -0
- relationalai/clients/resources/azure/azure.py +477 -0
- relationalai/clients/resources/snowflake/__init__.py +20 -0
- relationalai/clients/resources/snowflake/cli_resources.py +87 -0
- relationalai/clients/resources/snowflake/direct_access_resources.py +694 -0
- relationalai/clients/resources/snowflake/engine_state_handlers.py +309 -0
- relationalai/clients/resources/snowflake/error_handlers.py +199 -0
- relationalai/clients/resources/snowflake/export_procedure.py.jinja +249 -0
- relationalai/clients/resources/snowflake/resources_factory.py +99 -0
- relationalai/clients/resources/snowflake/snowflake.py +3190 -0
- relationalai/clients/resources/snowflake/use_index_poller.py +1019 -0
- relationalai/clients/resources/snowflake/use_index_resources.py +188 -0
- relationalai/clients/resources/snowflake/util.py +387 -0
- relationalai/clients/result_helpers.py +420 -0
- relationalai/clients/types.py +113 -0
- relationalai/clients/util.py +356 -0
- relationalai/debugging.py +389 -0
- relationalai/dsl.py +1749 -0
- relationalai/early_access/builder/__init__.py +30 -0
- relationalai/early_access/builder/builder/__init__.py +35 -0
- relationalai/early_access/builder/snowflake/__init__.py +12 -0
- relationalai/early_access/builder/std/__init__.py +25 -0
- relationalai/early_access/builder/std/decimals/__init__.py +12 -0
- relationalai/early_access/builder/std/integers/__init__.py +12 -0
- relationalai/early_access/builder/std/math/__init__.py +12 -0
- relationalai/early_access/builder/std/strings/__init__.py +14 -0
- relationalai/early_access/devtools/__init__.py +12 -0
- relationalai/early_access/devtools/benchmark_lqp/__init__.py +12 -0
- relationalai/early_access/devtools/extract_lqp/__init__.py +12 -0
- relationalai/early_access/dsl/adapters/orm/adapter_qb.py +427 -0
- relationalai/early_access/dsl/adapters/orm/parser.py +636 -0
- relationalai/early_access/dsl/adapters/owl/adapter.py +176 -0
- relationalai/early_access/dsl/adapters/owl/parser.py +160 -0
- relationalai/early_access/dsl/bindings/common.py +402 -0
- relationalai/early_access/dsl/bindings/csv.py +170 -0
- relationalai/early_access/dsl/bindings/legacy/binding_models.py +143 -0
- relationalai/early_access/dsl/bindings/snowflake.py +64 -0
- relationalai/early_access/dsl/codegen/binder.py +411 -0
- relationalai/early_access/dsl/codegen/common.py +79 -0
- relationalai/early_access/dsl/codegen/helpers.py +23 -0
- relationalai/early_access/dsl/codegen/relations.py +700 -0
- relationalai/early_access/dsl/codegen/weaver.py +417 -0
- relationalai/early_access/dsl/core/builders/__init__.py +47 -0
- relationalai/early_access/dsl/core/builders/logic.py +19 -0
- relationalai/early_access/dsl/core/builders/scalar_constraint.py +11 -0
- relationalai/early_access/dsl/core/constraints/predicate/atomic.py +455 -0
- relationalai/early_access/dsl/core/constraints/predicate/universal.py +73 -0
- relationalai/early_access/dsl/core/constraints/scalar.py +310 -0
- relationalai/early_access/dsl/core/context.py +13 -0
- relationalai/early_access/dsl/core/cset.py +132 -0
- relationalai/early_access/dsl/core/exprs/__init__.py +116 -0
- relationalai/early_access/dsl/core/exprs/relational.py +18 -0
- relationalai/early_access/dsl/core/exprs/scalar.py +412 -0
- relationalai/early_access/dsl/core/instances.py +44 -0
- relationalai/early_access/dsl/core/logic/__init__.py +193 -0
- relationalai/early_access/dsl/core/logic/aggregation.py +98 -0
- relationalai/early_access/dsl/core/logic/exists.py +223 -0
- relationalai/early_access/dsl/core/logic/helper.py +163 -0
- relationalai/early_access/dsl/core/namespaces.py +32 -0
- relationalai/early_access/dsl/core/relations.py +276 -0
- relationalai/early_access/dsl/core/rules.py +112 -0
- relationalai/early_access/dsl/core/std/__init__.py +45 -0
- relationalai/early_access/dsl/core/temporal/recall.py +6 -0
- relationalai/early_access/dsl/core/types/__init__.py +270 -0
- relationalai/early_access/dsl/core/types/concepts.py +128 -0
- relationalai/early_access/dsl/core/types/constrained/__init__.py +267 -0
- relationalai/early_access/dsl/core/types/constrained/nominal.py +143 -0
- relationalai/early_access/dsl/core/types/constrained/subtype.py +124 -0
- relationalai/early_access/dsl/core/types/standard.py +92 -0
- relationalai/early_access/dsl/core/types/unconstrained.py +50 -0
- relationalai/early_access/dsl/core/types/variables.py +203 -0
- relationalai/early_access/dsl/ir/compiler.py +318 -0
- relationalai/early_access/dsl/ir/executor.py +260 -0
- relationalai/early_access/dsl/ontologies/constraints.py +88 -0
- relationalai/early_access/dsl/ontologies/export.py +30 -0
- relationalai/early_access/dsl/ontologies/models.py +453 -0
- relationalai/early_access/dsl/ontologies/python_printer.py +303 -0
- relationalai/early_access/dsl/ontologies/readings.py +60 -0
- relationalai/early_access/dsl/ontologies/relationships.py +322 -0
- relationalai/early_access/dsl/ontologies/roles.py +87 -0
- relationalai/early_access/dsl/ontologies/subtyping.py +55 -0
- relationalai/early_access/dsl/orm/constraints.py +438 -0
- relationalai/early_access/dsl/orm/measures/dimensions.py +200 -0
- relationalai/early_access/dsl/orm/measures/initializer.py +16 -0
- relationalai/early_access/dsl/orm/measures/measure_rules.py +275 -0
- relationalai/early_access/dsl/orm/measures/measures.py +299 -0
- relationalai/early_access/dsl/orm/measures/role_exprs.py +268 -0
- relationalai/early_access/dsl/orm/models.py +256 -0
- relationalai/early_access/dsl/orm/object_oriented_printer.py +344 -0
- relationalai/early_access/dsl/orm/printer.py +469 -0
- relationalai/early_access/dsl/orm/reasoners.py +480 -0
- relationalai/early_access/dsl/orm/relations.py +19 -0
- relationalai/early_access/dsl/orm/relationships.py +251 -0
- relationalai/early_access/dsl/orm/types.py +42 -0
- relationalai/early_access/dsl/orm/utils.py +79 -0
- relationalai/early_access/dsl/orm/verb.py +204 -0
- relationalai/early_access/dsl/physical_metadata/tables.py +133 -0
- relationalai/early_access/dsl/relations.py +170 -0
- relationalai/early_access/dsl/rulesets.py +69 -0
- relationalai/early_access/dsl/schemas/__init__.py +450 -0
- relationalai/early_access/dsl/schemas/builder.py +48 -0
- relationalai/early_access/dsl/schemas/comp_names.py +51 -0
- relationalai/early_access/dsl/schemas/components.py +203 -0
- relationalai/early_access/dsl/schemas/contexts.py +156 -0
- relationalai/early_access/dsl/schemas/exprs.py +89 -0
- relationalai/early_access/dsl/schemas/fragments.py +464 -0
- relationalai/early_access/dsl/serialization.py +79 -0
- relationalai/early_access/dsl/serialize/exporter.py +163 -0
- relationalai/early_access/dsl/snow/api.py +105 -0
- relationalai/early_access/dsl/snow/common.py +76 -0
- relationalai/early_access/dsl/state_mgmt/__init__.py +129 -0
- relationalai/early_access/dsl/state_mgmt/state_charts.py +125 -0
- relationalai/early_access/dsl/state_mgmt/transitions.py +130 -0
- relationalai/early_access/dsl/types/__init__.py +40 -0
- relationalai/early_access/dsl/types/concepts.py +12 -0
- relationalai/early_access/dsl/types/entities.py +135 -0
- relationalai/early_access/dsl/types/values.py +17 -0
- relationalai/early_access/dsl/utils.py +102 -0
- relationalai/early_access/graphs/__init__.py +13 -0
- relationalai/early_access/lqp/__init__.py +12 -0
- relationalai/early_access/lqp/compiler/__init__.py +12 -0
- relationalai/early_access/lqp/constructors/__init__.py +18 -0
- relationalai/early_access/lqp/executor/__init__.py +12 -0
- relationalai/early_access/lqp/ir/__init__.py +12 -0
- relationalai/early_access/lqp/passes/__init__.py +12 -0
- relationalai/early_access/lqp/pragmas/__init__.py +12 -0
- relationalai/early_access/lqp/primitives/__init__.py +12 -0
- relationalai/early_access/lqp/types/__init__.py +12 -0
- relationalai/early_access/lqp/utils/__init__.py +12 -0
- relationalai/early_access/lqp/validators/__init__.py +12 -0
- relationalai/early_access/metamodel/__init__.py +58 -0
- relationalai/early_access/metamodel/builtins/__init__.py +12 -0
- relationalai/early_access/metamodel/compiler/__init__.py +12 -0
- relationalai/early_access/metamodel/dependency/__init__.py +12 -0
- relationalai/early_access/metamodel/factory/__init__.py +17 -0
- relationalai/early_access/metamodel/helpers/__init__.py +12 -0
- relationalai/early_access/metamodel/ir/__init__.py +14 -0
- relationalai/early_access/metamodel/rewrite/__init__.py +7 -0
- relationalai/early_access/metamodel/typer/__init__.py +3 -0
- relationalai/early_access/metamodel/typer/typer/__init__.py +12 -0
- relationalai/early_access/metamodel/types/__init__.py +15 -0
- relationalai/early_access/metamodel/util/__init__.py +15 -0
- relationalai/early_access/metamodel/visitor/__init__.py +12 -0
- relationalai/early_access/rel/__init__.py +12 -0
- relationalai/early_access/rel/executor/__init__.py +12 -0
- relationalai/early_access/rel/rel_utils/__init__.py +12 -0
- relationalai/early_access/rel/rewrite/__init__.py +7 -0
- relationalai/early_access/solvers/__init__.py +19 -0
- relationalai/early_access/sql/__init__.py +11 -0
- relationalai/early_access/sql/executor/__init__.py +3 -0
- relationalai/early_access/sql/rewrite/__init__.py +3 -0
- relationalai/early_access/tests/logging/__init__.py +12 -0
- relationalai/early_access/tests/test_snapshot_base/__init__.py +12 -0
- relationalai/early_access/tests/utils/__init__.py +12 -0
- relationalai/environments/__init__.py +35 -0
- relationalai/environments/base.py +381 -0
- relationalai/environments/colab.py +14 -0
- relationalai/environments/generic.py +71 -0
- relationalai/environments/ipython.py +68 -0
- relationalai/environments/jupyter.py +9 -0
- relationalai/environments/snowbook.py +169 -0
- relationalai/errors.py +2496 -0
- relationalai/experimental/SF.py +38 -0
- relationalai/experimental/inspect.py +47 -0
- relationalai/experimental/pathfinder/__init__.py +158 -0
- relationalai/experimental/pathfinder/api.py +160 -0
- relationalai/experimental/pathfinder/automaton.py +584 -0
- relationalai/experimental/pathfinder/bridge.py +226 -0
- relationalai/experimental/pathfinder/compiler.py +416 -0
- relationalai/experimental/pathfinder/datalog.py +214 -0
- relationalai/experimental/pathfinder/diagnostics.py +56 -0
- relationalai/experimental/pathfinder/filter.py +236 -0
- relationalai/experimental/pathfinder/glushkov.py +439 -0
- relationalai/experimental/pathfinder/options.py +265 -0
- relationalai/experimental/pathfinder/pathfinder-v0.7.0.rel +1951 -0
- relationalai/experimental/pathfinder/rpq.py +344 -0
- relationalai/experimental/pathfinder/transition.py +200 -0
- relationalai/experimental/pathfinder/utils.py +26 -0
- relationalai/experimental/paths/README.md +107 -0
- relationalai/experimental/paths/api.py +143 -0
- relationalai/experimental/paths/benchmarks/grid_graph.py +37 -0
- relationalai/experimental/paths/code_organization.md +2 -0
- relationalai/experimental/paths/examples/Movies.ipynb +16328 -0
- relationalai/experimental/paths/examples/basic_example.py +40 -0
- relationalai/experimental/paths/examples/minimal_engine_warmup.py +3 -0
- relationalai/experimental/paths/examples/movie_example.py +77 -0
- relationalai/experimental/paths/examples/movies_data/actedin.csv +193 -0
- relationalai/experimental/paths/examples/movies_data/directed.csv +45 -0
- relationalai/experimental/paths/examples/movies_data/follows.csv +7 -0
- relationalai/experimental/paths/examples/movies_data/movies.csv +39 -0
- relationalai/experimental/paths/examples/movies_data/person.csv +134 -0
- relationalai/experimental/paths/examples/movies_data/produced.csv +16 -0
- relationalai/experimental/paths/examples/movies_data/ratings.csv +10 -0
- relationalai/experimental/paths/examples/movies_data/wrote.csv +11 -0
- relationalai/experimental/paths/examples/paths_benchmark.py +115 -0
- relationalai/experimental/paths/examples/paths_example.py +116 -0
- relationalai/experimental/paths/examples/pattern_to_automaton.py +28 -0
- relationalai/experimental/paths/find_paths_via_automaton.py +85 -0
- relationalai/experimental/paths/graph.py +185 -0
- relationalai/experimental/paths/path_algorithms/find_paths.py +280 -0
- relationalai/experimental/paths/path_algorithms/one_sided_ball_repetition.py +26 -0
- relationalai/experimental/paths/path_algorithms/one_sided_ball_upto.py +111 -0
- relationalai/experimental/paths/path_algorithms/single.py +59 -0
- relationalai/experimental/paths/path_algorithms/two_sided_balls_repetition.py +39 -0
- relationalai/experimental/paths/path_algorithms/two_sided_balls_upto.py +103 -0
- relationalai/experimental/paths/path_algorithms/usp-old.py +130 -0
- relationalai/experimental/paths/path_algorithms/usp-tuple.py +183 -0
- relationalai/experimental/paths/path_algorithms/usp.py +150 -0
- relationalai/experimental/paths/product_graph.py +93 -0
- relationalai/experimental/paths/rpq/automaton.py +584 -0
- relationalai/experimental/paths/rpq/diagnostics.py +56 -0
- relationalai/experimental/paths/rpq/rpq.py +378 -0
- relationalai/experimental/paths/tests/tests_limit_sp_max_length.py +90 -0
- relationalai/experimental/paths/tests/tests_limit_sp_multiple.py +119 -0
- relationalai/experimental/paths/tests/tests_limit_sp_single.py +104 -0
- relationalai/experimental/paths/tests/tests_limit_walks_multiple.py +113 -0
- relationalai/experimental/paths/tests/tests_limit_walks_single.py +149 -0
- relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_multiple.py +70 -0
- relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_single.py +64 -0
- relationalai/experimental/paths/tests/tests_one_sided_ball_upto_multiple.py +115 -0
- relationalai/experimental/paths/tests/tests_one_sided_ball_upto_single.py +75 -0
- relationalai/experimental/paths/tests/tests_single_paths.py +152 -0
- relationalai/experimental/paths/tests/tests_single_walks.py +208 -0
- relationalai/experimental/paths/tests/tests_single_walks_undirected.py +297 -0
- relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_multiple.py +107 -0
- relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_single.py +76 -0
- relationalai/experimental/paths/tests/tests_two_sided_balls_upto_multiple.py +76 -0
- relationalai/experimental/paths/tests/tests_two_sided_balls_upto_single.py +110 -0
- relationalai/experimental/paths/tests/tests_usp_nsp_multiple.py +229 -0
- relationalai/experimental/paths/tests/tests_usp_nsp_single.py +108 -0
- relationalai/experimental/paths/tree_agg.py +168 -0
- relationalai/experimental/paths/utilities/iterators.py +27 -0
- relationalai/experimental/paths/utilities/prefix_sum.py +91 -0
- relationalai/experimental/solvers.py +1087 -0
- relationalai/loaders/csv.py +195 -0
- relationalai/loaders/loader.py +177 -0
- relationalai/loaders/types.py +23 -0
- relationalai/rel_emitter.py +373 -0
- relationalai/rel_utils.py +185 -0
- relationalai/semantics/__init__.py +22 -146
- relationalai/semantics/designs/query_builder/identify_by.md +106 -0
- relationalai/semantics/devtools/benchmark_lqp.py +535 -0
- relationalai/semantics/devtools/compilation_manager.py +294 -0
- relationalai/semantics/devtools/extract_lqp.py +110 -0
- relationalai/semantics/internal/internal.py +3785 -0
- relationalai/semantics/internal/snowflake.py +325 -0
- relationalai/semantics/lqp/README.md +34 -0
- relationalai/semantics/lqp/builtins.py +16 -0
- relationalai/semantics/lqp/compiler.py +22 -0
- relationalai/semantics/lqp/constructors.py +68 -0
- relationalai/semantics/lqp/executor.py +469 -0
- relationalai/semantics/lqp/intrinsics.py +24 -0
- relationalai/semantics/lqp/model2lqp.py +877 -0
- relationalai/semantics/lqp/passes.py +680 -0
- relationalai/semantics/lqp/primitives.py +252 -0
- relationalai/semantics/lqp/result_helpers.py +202 -0
- relationalai/semantics/lqp/rewrite/annotate_constraints.py +57 -0
- relationalai/semantics/lqp/rewrite/cdc.py +216 -0
- relationalai/semantics/lqp/rewrite/extract_common.py +338 -0
- relationalai/semantics/lqp/rewrite/extract_keys.py +512 -0
- relationalai/semantics/lqp/rewrite/function_annotations.py +114 -0
- relationalai/semantics/lqp/rewrite/functional_dependencies.py +314 -0
- relationalai/semantics/lqp/rewrite/quantify_vars.py +296 -0
- relationalai/semantics/lqp/rewrite/splinter.py +76 -0
- relationalai/semantics/lqp/types.py +101 -0
- relationalai/semantics/lqp/utils.py +160 -0
- relationalai/semantics/lqp/validators.py +57 -0
- relationalai/semantics/metamodel/__init__.py +40 -6
- relationalai/semantics/metamodel/builtins.py +771 -205
- relationalai/semantics/metamodel/compiler.py +133 -0
- relationalai/semantics/metamodel/dependency.py +862 -0
- relationalai/semantics/metamodel/executor.py +61 -0
- relationalai/semantics/metamodel/factory.py +287 -0
- relationalai/semantics/metamodel/helpers.py +361 -0
- relationalai/semantics/metamodel/rewrite/discharge_constraints.py +39 -0
- relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +210 -0
- relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +78 -0
- relationalai/semantics/metamodel/rewrite/flatten.py +554 -0
- relationalai/semantics/metamodel/rewrite/format_outputs.py +165 -0
- relationalai/semantics/metamodel/typer/checker.py +353 -0
- relationalai/semantics/metamodel/typer/typer.py +1395 -0
- relationalai/semantics/metamodel/util.py +506 -0
- relationalai/semantics/reasoners/__init__.py +10 -0
- relationalai/semantics/reasoners/graph/README.md +620 -0
- relationalai/semantics/reasoners/graph/__init__.py +37 -0
- relationalai/semantics/reasoners/graph/core.py +9019 -0
- relationalai/semantics/reasoners/graph/design/beyond_demand_transform.md +797 -0
- relationalai/semantics/reasoners/graph/tests/README.md +21 -0
- relationalai/semantics/reasoners/optimization/__init__.py +68 -0
- relationalai/semantics/reasoners/optimization/common.py +88 -0
- relationalai/semantics/reasoners/optimization/solvers_dev.py +568 -0
- relationalai/semantics/reasoners/optimization/solvers_pb.py +1414 -0
- relationalai/semantics/rel/builtins.py +40 -0
- relationalai/semantics/rel/compiler.py +989 -0
- relationalai/semantics/rel/executor.py +362 -0
- relationalai/semantics/rel/rel.py +482 -0
- relationalai/semantics/rel/rel_utils.py +276 -0
- relationalai/semantics/snowflake/__init__.py +3 -0
- relationalai/semantics/sql/compiler.py +2503 -0
- relationalai/semantics/sql/executor/duck_db.py +52 -0
- relationalai/semantics/sql/executor/result_helpers.py +64 -0
- relationalai/semantics/sql/executor/snowflake.py +149 -0
- relationalai/semantics/sql/rewrite/denormalize.py +222 -0
- relationalai/semantics/sql/rewrite/double_negation.py +49 -0
- relationalai/semantics/sql/rewrite/recursive_union.py +127 -0
- relationalai/semantics/sql/rewrite/sort_output_query.py +246 -0
- relationalai/semantics/sql/sql.py +504 -0
- relationalai/semantics/std/__init__.py +40 -60
- relationalai/semantics/std/constraints.py +43 -37
- relationalai/semantics/std/datetime.py +135 -246
- relationalai/semantics/std/decimals.py +52 -45
- relationalai/semantics/std/floats.py +5 -13
- relationalai/semantics/std/integers.py +11 -26
- relationalai/semantics/std/math.py +112 -183
- relationalai/semantics/std/pragmas.py +11 -0
- relationalai/semantics/std/re.py +62 -80
- relationalai/semantics/std/std.py +14 -0
- relationalai/semantics/std/strings.py +60 -117
- relationalai/semantics/tests/test_snapshot_abstract.py +143 -0
- relationalai/semantics/tests/test_snapshot_base.py +9 -0
- relationalai/semantics/tests/utils.py +46 -0
- relationalai/std/__init__.py +70 -0
- relationalai/tools/cli.py +1936 -0
- relationalai/tools/cli_controls.py +1826 -0
- relationalai/tools/cli_helpers.py +398 -0
- relationalai/tools/debugger.py +183 -289
- relationalai/tools/debugger_client.py +109 -0
- relationalai/tools/debugger_server.py +302 -0
- relationalai/tools/dev.py +685 -0
- relationalai/tools/notes +7 -0
- relationalai/tools/qb_debugger.py +425 -0
- relationalai/util/clean_up_databases.py +95 -0
- relationalai/util/format.py +106 -48
- relationalai/util/list_databases.py +9 -0
- relationalai/util/otel_configuration.py +26 -0
- relationalai/util/otel_handler.py +484 -0
- relationalai/util/snowflake_handler.py +88 -0
- relationalai/util/span_format_test.py +43 -0
- relationalai/util/span_tracker.py +207 -0
- relationalai/util/spans_file_handler.py +72 -0
- relationalai/util/tracing_handler.py +34 -0
- relationalai-0.13.1.dist-info/METADATA +74 -0
- relationalai-0.13.1.dist-info/RECORD +459 -0
- relationalai-0.13.1.dist-info/WHEEL +4 -0
- relationalai-0.13.1.dist-info/entry_points.txt +3 -0
- relationalai-0.13.1.dist-info/licenses/LICENSE +202 -0
- relationalai_test_util/__init__.py +4 -0
- relationalai_test_util/fixtures.py +233 -0
- relationalai_test_util/snapshot.py +252 -0
- relationalai_test_util/traceback.py +118 -0
- relationalai/config/__init__.py +0 -56
- relationalai/config/config.py +0 -289
- relationalai/config/config_fields.py +0 -86
- relationalai/config/connections/__init__.py +0 -46
- relationalai/config/connections/base.py +0 -23
- relationalai/config/connections/duckdb.py +0 -29
- relationalai/config/connections/snowflake.py +0 -243
- relationalai/config/external/__init__.py +0 -17
- relationalai/config/external/dbt_converter.py +0 -101
- relationalai/config/external/dbt_models.py +0 -93
- relationalai/config/external/snowflake_converter.py +0 -41
- relationalai/config/external/snowflake_models.py +0 -85
- relationalai/config/external/utils.py +0 -19
- relationalai/semantics/backends/lqp/annotations.py +0 -11
- relationalai/semantics/backends/sql/sql_compiler.py +0 -327
- relationalai/semantics/frontend/base.py +0 -1707
- relationalai/semantics/frontend/core.py +0 -179
- relationalai/semantics/frontend/front_compiler.py +0 -1313
- relationalai/semantics/frontend/pprint.py +0 -408
- relationalai/semantics/metamodel/metamodel.py +0 -437
- relationalai/semantics/metamodel/metamodel_analyzer.py +0 -519
- relationalai/semantics/metamodel/metamodel_compiler.py +0 -0
- relationalai/semantics/metamodel/pprint.py +0 -412
- relationalai/semantics/metamodel/rewriter.py +0 -266
- relationalai/semantics/metamodel/typer.py +0 -1378
- relationalai/semantics/std/aggregates.py +0 -149
- relationalai/semantics/std/common.py +0 -44
- relationalai/semantics/std/numbers.py +0 -86
- relationalai/shims/executor.py +0 -147
- relationalai/shims/helpers.py +0 -126
- relationalai/shims/hoister.py +0 -221
- relationalai/shims/mm2v0.py +0 -1290
- relationalai/tools/cli/__init__.py +0 -6
- relationalai/tools/cli/cli.py +0 -90
- relationalai/tools/cli/components/__init__.py +0 -5
- relationalai/tools/cli/components/progress_reader.py +0 -1524
- relationalai/tools/cli/components/utils.py +0 -58
- relationalai/tools/cli/config_template.py +0 -45
- relationalai/tools/cli/dev.py +0 -19
- relationalai/tools/typer_debugger.py +0 -93
- relationalai/util/dataclasses.py +0 -43
- relationalai/util/docutils.py +0 -40
- relationalai/util/error.py +0 -199
- relationalai/util/naming.py +0 -145
- relationalai/util/python.py +0 -35
- relationalai/util/runtime.py +0 -156
- relationalai/util/schema.py +0 -197
- relationalai/util/source.py +0 -185
- relationalai/util/structures.py +0 -163
- relationalai/util/tracing.py +0 -261
- relationalai-0.13.0.dev0.dist-info/METADATA +0 -46
- relationalai-0.13.0.dev0.dist-info/RECORD +0 -488
- relationalai-0.13.0.dev0.dist-info/WHEEL +0 -5
- relationalai-0.13.0.dev0.dist-info/entry_points.txt +0 -3
- relationalai-0.13.0.dev0.dist-info/top_level.txt +0 -2
- v0/relationalai/__init__.py +0 -216
- v0/relationalai/clients/__init__.py +0 -5
- v0/relationalai/clients/azure.py +0 -477
- v0/relationalai/clients/client.py +0 -912
- v0/relationalai/clients/config.py +0 -673
- v0/relationalai/clients/direct_access_client.py +0 -118
- v0/relationalai/clients/hash_util.py +0 -31
- v0/relationalai/clients/local.py +0 -571
- v0/relationalai/clients/profile_polling.py +0 -73
- v0/relationalai/clients/result_helpers.py +0 -420
- v0/relationalai/clients/snowflake.py +0 -3869
- v0/relationalai/clients/types.py +0 -113
- v0/relationalai/clients/use_index_poller.py +0 -980
- v0/relationalai/clients/util.py +0 -356
- v0/relationalai/debugging.py +0 -389
- v0/relationalai/dsl.py +0 -1749
- v0/relationalai/early_access/builder/__init__.py +0 -30
- v0/relationalai/early_access/builder/builder/__init__.py +0 -35
- v0/relationalai/early_access/builder/snowflake/__init__.py +0 -12
- v0/relationalai/early_access/builder/std/__init__.py +0 -25
- v0/relationalai/early_access/builder/std/decimals/__init__.py +0 -12
- v0/relationalai/early_access/builder/std/integers/__init__.py +0 -12
- v0/relationalai/early_access/builder/std/math/__init__.py +0 -12
- v0/relationalai/early_access/builder/std/strings/__init__.py +0 -14
- v0/relationalai/early_access/devtools/__init__.py +0 -12
- v0/relationalai/early_access/devtools/benchmark_lqp/__init__.py +0 -12
- v0/relationalai/early_access/devtools/extract_lqp/__init__.py +0 -12
- v0/relationalai/early_access/dsl/adapters/orm/adapter_qb.py +0 -427
- v0/relationalai/early_access/dsl/adapters/orm/parser.py +0 -636
- v0/relationalai/early_access/dsl/adapters/owl/adapter.py +0 -176
- v0/relationalai/early_access/dsl/adapters/owl/parser.py +0 -160
- v0/relationalai/early_access/dsl/bindings/common.py +0 -402
- v0/relationalai/early_access/dsl/bindings/csv.py +0 -170
- v0/relationalai/early_access/dsl/bindings/legacy/binding_models.py +0 -143
- v0/relationalai/early_access/dsl/bindings/snowflake.py +0 -64
- v0/relationalai/early_access/dsl/codegen/binder.py +0 -411
- v0/relationalai/early_access/dsl/codegen/common.py +0 -79
- v0/relationalai/early_access/dsl/codegen/helpers.py +0 -23
- v0/relationalai/early_access/dsl/codegen/relations.py +0 -700
- v0/relationalai/early_access/dsl/codegen/weaver.py +0 -417
- v0/relationalai/early_access/dsl/core/builders/__init__.py +0 -47
- v0/relationalai/early_access/dsl/core/builders/logic.py +0 -19
- v0/relationalai/early_access/dsl/core/builders/scalar_constraint.py +0 -11
- v0/relationalai/early_access/dsl/core/constraints/predicate/atomic.py +0 -455
- v0/relationalai/early_access/dsl/core/constraints/predicate/universal.py +0 -73
- v0/relationalai/early_access/dsl/core/constraints/scalar.py +0 -310
- v0/relationalai/early_access/dsl/core/context.py +0 -13
- v0/relationalai/early_access/dsl/core/cset.py +0 -132
- v0/relationalai/early_access/dsl/core/exprs/__init__.py +0 -116
- v0/relationalai/early_access/dsl/core/exprs/relational.py +0 -18
- v0/relationalai/early_access/dsl/core/exprs/scalar.py +0 -412
- v0/relationalai/early_access/dsl/core/instances.py +0 -44
- v0/relationalai/early_access/dsl/core/logic/__init__.py +0 -193
- v0/relationalai/early_access/dsl/core/logic/aggregation.py +0 -98
- v0/relationalai/early_access/dsl/core/logic/exists.py +0 -223
- v0/relationalai/early_access/dsl/core/logic/helper.py +0 -163
- v0/relationalai/early_access/dsl/core/namespaces.py +0 -32
- v0/relationalai/early_access/dsl/core/relations.py +0 -276
- v0/relationalai/early_access/dsl/core/rules.py +0 -112
- v0/relationalai/early_access/dsl/core/std/__init__.py +0 -45
- v0/relationalai/early_access/dsl/core/temporal/recall.py +0 -6
- v0/relationalai/early_access/dsl/core/types/__init__.py +0 -270
- v0/relationalai/early_access/dsl/core/types/concepts.py +0 -128
- v0/relationalai/early_access/dsl/core/types/constrained/__init__.py +0 -267
- v0/relationalai/early_access/dsl/core/types/constrained/nominal.py +0 -143
- v0/relationalai/early_access/dsl/core/types/constrained/subtype.py +0 -124
- v0/relationalai/early_access/dsl/core/types/standard.py +0 -92
- v0/relationalai/early_access/dsl/core/types/unconstrained.py +0 -50
- v0/relationalai/early_access/dsl/core/types/variables.py +0 -203
- v0/relationalai/early_access/dsl/ir/compiler.py +0 -318
- v0/relationalai/early_access/dsl/ir/executor.py +0 -260
- v0/relationalai/early_access/dsl/ontologies/constraints.py +0 -88
- v0/relationalai/early_access/dsl/ontologies/export.py +0 -30
- v0/relationalai/early_access/dsl/ontologies/models.py +0 -453
- v0/relationalai/early_access/dsl/ontologies/python_printer.py +0 -303
- v0/relationalai/early_access/dsl/ontologies/readings.py +0 -60
- v0/relationalai/early_access/dsl/ontologies/relationships.py +0 -322
- v0/relationalai/early_access/dsl/ontologies/roles.py +0 -87
- v0/relationalai/early_access/dsl/ontologies/subtyping.py +0 -55
- v0/relationalai/early_access/dsl/orm/constraints.py +0 -438
- v0/relationalai/early_access/dsl/orm/measures/dimensions.py +0 -200
- v0/relationalai/early_access/dsl/orm/measures/initializer.py +0 -16
- v0/relationalai/early_access/dsl/orm/measures/measure_rules.py +0 -275
- v0/relationalai/early_access/dsl/orm/measures/measures.py +0 -299
- v0/relationalai/early_access/dsl/orm/measures/role_exprs.py +0 -268
- v0/relationalai/early_access/dsl/orm/models.py +0 -256
- v0/relationalai/early_access/dsl/orm/object_oriented_printer.py +0 -344
- v0/relationalai/early_access/dsl/orm/printer.py +0 -469
- v0/relationalai/early_access/dsl/orm/reasoners.py +0 -480
- v0/relationalai/early_access/dsl/orm/relations.py +0 -19
- v0/relationalai/early_access/dsl/orm/relationships.py +0 -251
- v0/relationalai/early_access/dsl/orm/types.py +0 -42
- v0/relationalai/early_access/dsl/orm/utils.py +0 -79
- v0/relationalai/early_access/dsl/orm/verb.py +0 -204
- v0/relationalai/early_access/dsl/physical_metadata/tables.py +0 -133
- v0/relationalai/early_access/dsl/relations.py +0 -170
- v0/relationalai/early_access/dsl/rulesets.py +0 -69
- v0/relationalai/early_access/dsl/schemas/__init__.py +0 -450
- v0/relationalai/early_access/dsl/schemas/builder.py +0 -48
- v0/relationalai/early_access/dsl/schemas/comp_names.py +0 -51
- v0/relationalai/early_access/dsl/schemas/components.py +0 -203
- v0/relationalai/early_access/dsl/schemas/contexts.py +0 -156
- v0/relationalai/early_access/dsl/schemas/exprs.py +0 -89
- v0/relationalai/early_access/dsl/schemas/fragments.py +0 -464
- v0/relationalai/early_access/dsl/serialization.py +0 -79
- v0/relationalai/early_access/dsl/serialize/exporter.py +0 -163
- v0/relationalai/early_access/dsl/snow/api.py +0 -104
- v0/relationalai/early_access/dsl/snow/common.py +0 -76
- v0/relationalai/early_access/dsl/state_mgmt/__init__.py +0 -129
- v0/relationalai/early_access/dsl/state_mgmt/state_charts.py +0 -125
- v0/relationalai/early_access/dsl/state_mgmt/transitions.py +0 -130
- v0/relationalai/early_access/dsl/types/__init__.py +0 -40
- v0/relationalai/early_access/dsl/types/concepts.py +0 -12
- v0/relationalai/early_access/dsl/types/entities.py +0 -135
- v0/relationalai/early_access/dsl/types/values.py +0 -17
- v0/relationalai/early_access/dsl/utils.py +0 -102
- v0/relationalai/early_access/graphs/__init__.py +0 -13
- v0/relationalai/early_access/lqp/__init__.py +0 -12
- v0/relationalai/early_access/lqp/compiler/__init__.py +0 -12
- v0/relationalai/early_access/lqp/constructors/__init__.py +0 -18
- v0/relationalai/early_access/lqp/executor/__init__.py +0 -12
- v0/relationalai/early_access/lqp/ir/__init__.py +0 -12
- v0/relationalai/early_access/lqp/passes/__init__.py +0 -12
- v0/relationalai/early_access/lqp/pragmas/__init__.py +0 -12
- v0/relationalai/early_access/lqp/primitives/__init__.py +0 -12
- v0/relationalai/early_access/lqp/types/__init__.py +0 -12
- v0/relationalai/early_access/lqp/utils/__init__.py +0 -12
- v0/relationalai/early_access/lqp/validators/__init__.py +0 -12
- v0/relationalai/early_access/metamodel/__init__.py +0 -58
- v0/relationalai/early_access/metamodel/builtins/__init__.py +0 -12
- v0/relationalai/early_access/metamodel/compiler/__init__.py +0 -12
- v0/relationalai/early_access/metamodel/dependency/__init__.py +0 -12
- v0/relationalai/early_access/metamodel/factory/__init__.py +0 -17
- v0/relationalai/early_access/metamodel/helpers/__init__.py +0 -12
- v0/relationalai/early_access/metamodel/ir/__init__.py +0 -14
- v0/relationalai/early_access/metamodel/rewrite/__init__.py +0 -7
- v0/relationalai/early_access/metamodel/typer/__init__.py +0 -3
- v0/relationalai/early_access/metamodel/typer/typer/__init__.py +0 -12
- v0/relationalai/early_access/metamodel/types/__init__.py +0 -15
- v0/relationalai/early_access/metamodel/util/__init__.py +0 -15
- v0/relationalai/early_access/metamodel/visitor/__init__.py +0 -12
- v0/relationalai/early_access/rel/__init__.py +0 -12
- v0/relationalai/early_access/rel/executor/__init__.py +0 -12
- v0/relationalai/early_access/rel/rel_utils/__init__.py +0 -12
- v0/relationalai/early_access/rel/rewrite/__init__.py +0 -7
- v0/relationalai/early_access/solvers/__init__.py +0 -19
- v0/relationalai/early_access/sql/__init__.py +0 -11
- v0/relationalai/early_access/sql/executor/__init__.py +0 -3
- v0/relationalai/early_access/sql/rewrite/__init__.py +0 -3
- v0/relationalai/early_access/tests/logging/__init__.py +0 -12
- v0/relationalai/early_access/tests/test_snapshot_base/__init__.py +0 -12
- v0/relationalai/early_access/tests/utils/__init__.py +0 -12
- v0/relationalai/environments/__init__.py +0 -35
- v0/relationalai/environments/base.py +0 -381
- v0/relationalai/environments/colab.py +0 -14
- v0/relationalai/environments/generic.py +0 -71
- v0/relationalai/environments/ipython.py +0 -68
- v0/relationalai/environments/jupyter.py +0 -9
- v0/relationalai/environments/snowbook.py +0 -169
- v0/relationalai/errors.py +0 -2455
- v0/relationalai/experimental/SF.py +0 -38
- v0/relationalai/experimental/inspect.py +0 -47
- v0/relationalai/experimental/pathfinder/__init__.py +0 -158
- v0/relationalai/experimental/pathfinder/api.py +0 -160
- v0/relationalai/experimental/pathfinder/automaton.py +0 -584
- v0/relationalai/experimental/pathfinder/bridge.py +0 -226
- v0/relationalai/experimental/pathfinder/compiler.py +0 -416
- v0/relationalai/experimental/pathfinder/datalog.py +0 -214
- v0/relationalai/experimental/pathfinder/diagnostics.py +0 -56
- v0/relationalai/experimental/pathfinder/filter.py +0 -236
- v0/relationalai/experimental/pathfinder/glushkov.py +0 -439
- v0/relationalai/experimental/pathfinder/options.py +0 -265
- v0/relationalai/experimental/pathfinder/rpq.py +0 -344
- v0/relationalai/experimental/pathfinder/transition.py +0 -200
- v0/relationalai/experimental/pathfinder/utils.py +0 -26
- v0/relationalai/experimental/paths/api.py +0 -143
- v0/relationalai/experimental/paths/benchmarks/grid_graph.py +0 -37
- v0/relationalai/experimental/paths/examples/basic_example.py +0 -40
- v0/relationalai/experimental/paths/examples/minimal_engine_warmup.py +0 -3
- v0/relationalai/experimental/paths/examples/movie_example.py +0 -77
- v0/relationalai/experimental/paths/examples/paths_benchmark.py +0 -115
- v0/relationalai/experimental/paths/examples/paths_example.py +0 -116
- v0/relationalai/experimental/paths/examples/pattern_to_automaton.py +0 -28
- v0/relationalai/experimental/paths/find_paths_via_automaton.py +0 -85
- v0/relationalai/experimental/paths/graph.py +0 -185
- v0/relationalai/experimental/paths/path_algorithms/find_paths.py +0 -280
- v0/relationalai/experimental/paths/path_algorithms/one_sided_ball_repetition.py +0 -26
- v0/relationalai/experimental/paths/path_algorithms/one_sided_ball_upto.py +0 -111
- v0/relationalai/experimental/paths/path_algorithms/single.py +0 -59
- v0/relationalai/experimental/paths/path_algorithms/two_sided_balls_repetition.py +0 -39
- v0/relationalai/experimental/paths/path_algorithms/two_sided_balls_upto.py +0 -103
- v0/relationalai/experimental/paths/path_algorithms/usp-old.py +0 -130
- v0/relationalai/experimental/paths/path_algorithms/usp-tuple.py +0 -183
- v0/relationalai/experimental/paths/path_algorithms/usp.py +0 -150
- v0/relationalai/experimental/paths/product_graph.py +0 -93
- v0/relationalai/experimental/paths/rpq/automaton.py +0 -584
- v0/relationalai/experimental/paths/rpq/diagnostics.py +0 -56
- v0/relationalai/experimental/paths/rpq/rpq.py +0 -378
- v0/relationalai/experimental/paths/tests/tests_limit_sp_max_length.py +0 -90
- v0/relationalai/experimental/paths/tests/tests_limit_sp_multiple.py +0 -119
- v0/relationalai/experimental/paths/tests/tests_limit_sp_single.py +0 -104
- v0/relationalai/experimental/paths/tests/tests_limit_walks_multiple.py +0 -113
- v0/relationalai/experimental/paths/tests/tests_limit_walks_single.py +0 -149
- v0/relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_multiple.py +0 -70
- v0/relationalai/experimental/paths/tests/tests_one_sided_ball_repetition_single.py +0 -64
- v0/relationalai/experimental/paths/tests/tests_one_sided_ball_upto_multiple.py +0 -115
- v0/relationalai/experimental/paths/tests/tests_one_sided_ball_upto_single.py +0 -75
- v0/relationalai/experimental/paths/tests/tests_single_paths.py +0 -152
- v0/relationalai/experimental/paths/tests/tests_single_walks.py +0 -208
- v0/relationalai/experimental/paths/tests/tests_single_walks_undirected.py +0 -297
- v0/relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_multiple.py +0 -107
- v0/relationalai/experimental/paths/tests/tests_two_sided_balls_repetition_single.py +0 -76
- v0/relationalai/experimental/paths/tests/tests_two_sided_balls_upto_multiple.py +0 -76
- v0/relationalai/experimental/paths/tests/tests_two_sided_balls_upto_single.py +0 -110
- v0/relationalai/experimental/paths/tests/tests_usp_nsp_multiple.py +0 -229
- v0/relationalai/experimental/paths/tests/tests_usp_nsp_single.py +0 -108
- v0/relationalai/experimental/paths/tree_agg.py +0 -168
- v0/relationalai/experimental/paths/utilities/iterators.py +0 -27
- v0/relationalai/experimental/paths/utilities/prefix_sum.py +0 -91
- v0/relationalai/experimental/solvers.py +0 -1087
- v0/relationalai/loaders/csv.py +0 -195
- v0/relationalai/loaders/loader.py +0 -177
- v0/relationalai/loaders/types.py +0 -23
- v0/relationalai/rel_emitter.py +0 -373
- v0/relationalai/rel_utils.py +0 -185
- v0/relationalai/semantics/__init__.py +0 -29
- v0/relationalai/semantics/devtools/benchmark_lqp.py +0 -536
- v0/relationalai/semantics/devtools/compilation_manager.py +0 -294
- v0/relationalai/semantics/devtools/extract_lqp.py +0 -110
- v0/relationalai/semantics/internal/internal.py +0 -3785
- v0/relationalai/semantics/internal/snowflake.py +0 -324
- v0/relationalai/semantics/lqp/builtins.py +0 -16
- v0/relationalai/semantics/lqp/compiler.py +0 -22
- v0/relationalai/semantics/lqp/constructors.py +0 -68
- v0/relationalai/semantics/lqp/executor.py +0 -469
- v0/relationalai/semantics/lqp/intrinsics.py +0 -24
- v0/relationalai/semantics/lqp/model2lqp.py +0 -839
- v0/relationalai/semantics/lqp/passes.py +0 -680
- v0/relationalai/semantics/lqp/primitives.py +0 -252
- v0/relationalai/semantics/lqp/result_helpers.py +0 -202
- v0/relationalai/semantics/lqp/rewrite/annotate_constraints.py +0 -57
- v0/relationalai/semantics/lqp/rewrite/cdc.py +0 -216
- v0/relationalai/semantics/lqp/rewrite/extract_common.py +0 -338
- v0/relationalai/semantics/lqp/rewrite/extract_keys.py +0 -449
- v0/relationalai/semantics/lqp/rewrite/function_annotations.py +0 -114
- v0/relationalai/semantics/lqp/rewrite/functional_dependencies.py +0 -314
- v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +0 -296
- v0/relationalai/semantics/lqp/rewrite/splinter.py +0 -76
- v0/relationalai/semantics/lqp/types.py +0 -101
- v0/relationalai/semantics/lqp/utils.py +0 -160
- v0/relationalai/semantics/lqp/validators.py +0 -57
- v0/relationalai/semantics/metamodel/__init__.py +0 -40
- v0/relationalai/semantics/metamodel/builtins.py +0 -774
- v0/relationalai/semantics/metamodel/compiler.py +0 -133
- v0/relationalai/semantics/metamodel/dependency.py +0 -862
- v0/relationalai/semantics/metamodel/executor.py +0 -61
- v0/relationalai/semantics/metamodel/factory.py +0 -287
- v0/relationalai/semantics/metamodel/helpers.py +0 -361
- v0/relationalai/semantics/metamodel/rewrite/discharge_constraints.py +0 -39
- v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +0 -210
- v0/relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +0 -78
- v0/relationalai/semantics/metamodel/rewrite/flatten.py +0 -549
- v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +0 -165
- v0/relationalai/semantics/metamodel/typer/checker.py +0 -353
- v0/relationalai/semantics/metamodel/typer/typer.py +0 -1395
- v0/relationalai/semantics/metamodel/util.py +0 -505
- v0/relationalai/semantics/reasoners/__init__.py +0 -10
- v0/relationalai/semantics/reasoners/graph/__init__.py +0 -37
- v0/relationalai/semantics/reasoners/graph/core.py +0 -9020
- v0/relationalai/semantics/reasoners/optimization/__init__.py +0 -68
- v0/relationalai/semantics/reasoners/optimization/common.py +0 -88
- v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +0 -568
- v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +0 -1163
- v0/relationalai/semantics/rel/builtins.py +0 -40
- v0/relationalai/semantics/rel/compiler.py +0 -989
- v0/relationalai/semantics/rel/executor.py +0 -359
- v0/relationalai/semantics/rel/rel.py +0 -482
- v0/relationalai/semantics/rel/rel_utils.py +0 -276
- v0/relationalai/semantics/snowflake/__init__.py +0 -3
- v0/relationalai/semantics/sql/compiler.py +0 -2503
- v0/relationalai/semantics/sql/executor/duck_db.py +0 -52
- v0/relationalai/semantics/sql/executor/result_helpers.py +0 -64
- v0/relationalai/semantics/sql/executor/snowflake.py +0 -145
- v0/relationalai/semantics/sql/rewrite/denormalize.py +0 -222
- v0/relationalai/semantics/sql/rewrite/double_negation.py +0 -49
- v0/relationalai/semantics/sql/rewrite/recursive_union.py +0 -127
- v0/relationalai/semantics/sql/rewrite/sort_output_query.py +0 -246
- v0/relationalai/semantics/sql/sql.py +0 -504
- v0/relationalai/semantics/std/__init__.py +0 -54
- v0/relationalai/semantics/std/constraints.py +0 -43
- v0/relationalai/semantics/std/datetime.py +0 -363
- v0/relationalai/semantics/std/decimals.py +0 -62
- v0/relationalai/semantics/std/floats.py +0 -7
- v0/relationalai/semantics/std/integers.py +0 -22
- v0/relationalai/semantics/std/math.py +0 -141
- v0/relationalai/semantics/std/pragmas.py +0 -11
- v0/relationalai/semantics/std/re.py +0 -83
- v0/relationalai/semantics/std/std.py +0 -14
- v0/relationalai/semantics/std/strings.py +0 -63
- v0/relationalai/semantics/tests/__init__.py +0 -0
- v0/relationalai/semantics/tests/test_snapshot_abstract.py +0 -143
- v0/relationalai/semantics/tests/test_snapshot_base.py +0 -9
- v0/relationalai/semantics/tests/utils.py +0 -46
- v0/relationalai/std/__init__.py +0 -70
- v0/relationalai/tools/__init__.py +0 -0
- v0/relationalai/tools/cli.py +0 -1940
- v0/relationalai/tools/cli_controls.py +0 -1826
- v0/relationalai/tools/cli_helpers.py +0 -390
- v0/relationalai/tools/debugger.py +0 -183
- v0/relationalai/tools/debugger_client.py +0 -109
- v0/relationalai/tools/debugger_server.py +0 -302
- v0/relationalai/tools/dev.py +0 -685
- v0/relationalai/tools/qb_debugger.py +0 -425
- v0/relationalai/util/clean_up_databases.py +0 -95
- v0/relationalai/util/format.py +0 -123
- v0/relationalai/util/list_databases.py +0 -9
- v0/relationalai/util/otel_configuration.py +0 -25
- v0/relationalai/util/otel_handler.py +0 -484
- v0/relationalai/util/snowflake_handler.py +0 -88
- v0/relationalai/util/span_format_test.py +0 -43
- v0/relationalai/util/span_tracker.py +0 -207
- v0/relationalai/util/spans_file_handler.py +0 -72
- v0/relationalai/util/tracing_handler.py +0 -34
- /relationalai/{semantics/frontend → analysis}/__init__.py +0 -0
- {v0/relationalai → relationalai}/analysis/mechanistic.py +0 -0
- {v0/relationalai → relationalai}/analysis/whynot.py +0 -0
- /relationalai/{shims → auth}/__init__.py +0 -0
- {v0/relationalai → relationalai}/auth/jwt_generator.py +0 -0
- {v0/relationalai → relationalai}/auth/oauth_callback_server.py +0 -0
- {v0/relationalai → relationalai}/auth/token_handler.py +0 -0
- {v0/relationalai → relationalai}/auth/util.py +0 -0
- {v0/relationalai/clients → relationalai/clients/resources/snowflake}/cache_store.py +0 -0
- {v0/relationalai → relationalai}/compiler.py +0 -0
- {v0/relationalai → relationalai}/dependencies.py +0 -0
- {v0/relationalai → relationalai}/docutils.py +0 -0
- {v0/relationalai/analysis → relationalai/early_access}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/__init__.py +0 -0
- {v0/relationalai/auth → relationalai/early_access/dsl/adapters}/__init__.py +0 -0
- {v0/relationalai/early_access → relationalai/early_access/dsl/adapters/orm}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/adapters/orm/model.py +0 -0
- {v0/relationalai/early_access/dsl/adapters → relationalai/early_access/dsl/adapters/owl}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/adapters/owl/model.py +0 -0
- {v0/relationalai/early_access/dsl/adapters/orm → relationalai/early_access/dsl/bindings}/__init__.py +0 -0
- {v0/relationalai/early_access/dsl/adapters/owl → relationalai/early_access/dsl/bindings/legacy}/__init__.py +0 -0
- {v0/relationalai/early_access/dsl/bindings → relationalai/early_access/dsl/codegen}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/constants.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/core/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/core/constraints/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/core/constraints/predicate/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/core/stack.py +0 -0
- {v0/relationalai/early_access/dsl/bindings/legacy → relationalai/early_access/dsl/core/temporal}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/core/utils.py +0 -0
- {v0/relationalai/early_access/dsl/codegen → relationalai/early_access/dsl/ir}/__init__.py +0 -0
- {v0/relationalai/early_access/dsl/core/temporal → relationalai/early_access/dsl/ontologies}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/ontologies/raw_source.py +0 -0
- {v0/relationalai/early_access/dsl/ir → relationalai/early_access/dsl/orm}/__init__.py +0 -0
- {v0/relationalai/early_access/dsl/ontologies → relationalai/early_access/dsl/orm/measures}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/orm/reasoner_errors.py +0 -0
- {v0/relationalai/early_access/dsl/orm → relationalai/early_access/dsl/physical_metadata}/__init__.py +0 -0
- {v0/relationalai/early_access/dsl/orm/measures → relationalai/early_access/dsl/serialize}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/serialize/binding_model.py +0 -0
- {v0/relationalai → relationalai}/early_access/dsl/serialize/model.py +0 -0
- {v0/relationalai/early_access/dsl/physical_metadata → relationalai/early_access/dsl/snow}/__init__.py +0 -0
- {v0/relationalai → relationalai}/early_access/tests/__init__.py +0 -0
- {v0/relationalai → relationalai}/environments/ci.py +0 -0
- {v0/relationalai → relationalai}/environments/hex.py +0 -0
- {v0/relationalai → relationalai}/environments/terminal.py +0 -0
- {v0/relationalai → relationalai}/experimental/__init__.py +0 -0
- {v0/relationalai → relationalai}/experimental/graphs.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/__init__.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/benchmarks/__init__.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/path_algorithms/__init__.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/rpq/__init__.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/rpq/filter.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/rpq/glushkov.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/rpq/transition.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/utilities/__init__.py +0 -0
- {v0/relationalai → relationalai}/experimental/paths/utilities/utilities.py +0 -0
- {v0/relationalai/early_access/dsl/serialize → relationalai/loaders}/__init__.py +0 -0
- {v0/relationalai → relationalai}/metagen.py +0 -0
- {v0/relationalai → relationalai}/metamodel.py +0 -0
- {v0/relationalai → relationalai}/rel.py +0 -0
- {v0/relationalai → relationalai}/semantics/devtools/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/internal/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/internal/annotations.py +0 -0
- {v0/relationalai → relationalai}/semantics/lqp/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/lqp/ir.py +0 -0
- {v0/relationalai → relationalai}/semantics/lqp/pragmas.py +0 -0
- {v0/relationalai → relationalai}/semantics/lqp/rewrite/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/metamodel/dataflow.py +0 -0
- {v0/relationalai → relationalai}/semantics/metamodel/ir.py +0 -0
- {v0/relationalai → relationalai}/semantics/metamodel/rewrite/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/metamodel/typer/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/metamodel/types.py +0 -0
- {v0/relationalai → relationalai}/semantics/metamodel/visitor.py +0 -0
- {v0/relationalai → relationalai}/semantics/reasoners/experimental/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/rel/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/sql/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/sql/executor/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/sql/rewrite/__init__.py +0 -0
- {v0/relationalai/early_access/dsl/snow → relationalai/semantics/tests}/__init__.py +0 -0
- {v0/relationalai → relationalai}/semantics/tests/logging.py +0 -0
- {v0/relationalai → relationalai}/std/aggregates.py +0 -0
- {v0/relationalai → relationalai}/std/dates.py +0 -0
- {v0/relationalai → relationalai}/std/graphs.py +0 -0
- {v0/relationalai → relationalai}/std/inspect.py +0 -0
- {v0/relationalai → relationalai}/std/math.py +0 -0
- {v0/relationalai → relationalai}/std/re.py +0 -0
- {v0/relationalai → relationalai}/std/strings.py +0 -0
- {v0/relationalai/loaders → relationalai/tools}/__init__.py +0 -0
- {v0/relationalai → relationalai}/tools/cleanup_snapshots.py +0 -0
- {v0/relationalai → relationalai}/tools/constants.py +0 -0
- {v0/relationalai → relationalai}/tools/query_utils.py +0 -0
- {v0/relationalai → relationalai}/tools/snapshot_viewer.py +0 -0
- {v0/relationalai → relationalai}/util/__init__.py +0 -0
- {v0/relationalai → relationalai}/util/constants.py +0 -0
- {v0/relationalai → relationalai}/util/graph.py +0 -0
- {v0/relationalai → relationalai}/util/timeout.py +0 -0
|
@@ -0,0 +1,1826 @@
|
|
|
1
|
+
#pyright: reportPrivateImportUsage=false
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
# Standard library imports
|
|
5
|
+
import io
|
|
6
|
+
import itertools
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import importlib
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable, Dict, List, Sequence, TextIO, cast
|
|
16
|
+
|
|
17
|
+
# Third-party imports
|
|
18
|
+
import rich
|
|
19
|
+
from InquirerPy import inquirer, utils as inquirer_utils
|
|
20
|
+
from InquirerPy.base.complex import FakeDocument
|
|
21
|
+
from InquirerPy.base.control import Choice
|
|
22
|
+
from prompt_toolkit.key_binding import KeyPressEvent
|
|
23
|
+
from prompt_toolkit.validation import ValidationError
|
|
24
|
+
from rich.color import Color
|
|
25
|
+
from rich.console import Console, Group
|
|
26
|
+
from wcwidth import wcwidth
|
|
27
|
+
|
|
28
|
+
# Local imports
|
|
29
|
+
from relationalai import debugging
|
|
30
|
+
from relationalai.util.format import format_duration
|
|
31
|
+
from ..environments import (
|
|
32
|
+
HexEnvironment,
|
|
33
|
+
JupyterEnvironment,
|
|
34
|
+
NotebookRuntimeEnvironment,
|
|
35
|
+
SnowbookEnvironment,
|
|
36
|
+
runtime_env,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
#--------------------------------------------------
|
|
40
|
+
# Constants
|
|
41
|
+
#--------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# Display symbols
|
|
44
|
+
ARROW = "➜"
|
|
45
|
+
CHECK_MARK = "✓"
|
|
46
|
+
SUCCESS_ICON = "✅"
|
|
47
|
+
FAIL_ICON = "❌"
|
|
48
|
+
|
|
49
|
+
# Spinner animation frames
|
|
50
|
+
SPINNER_FRAMES = ["▰▱▱▱", "▰▰▱▱", "▰▰▰▱", "▰▰▰▰", "▱▰▰▰", "▱▱▰▰", "▱▱▱▰", "▱▱▱▱"]
|
|
51
|
+
|
|
52
|
+
# Terminal display constants
|
|
53
|
+
DEFAULT_TERMINAL_WIDTH = 80
|
|
54
|
+
SEPARATOR_WIDTH = 40
|
|
55
|
+
|
|
56
|
+
# Task progress constants
|
|
57
|
+
INITIALIZATION_COMPLETED_TEXT = "Parallel init finished in"
|
|
58
|
+
MIN_CATEGORY_DURATION_SECONDS = 0.25 # Only show categories with duration > 250ms
|
|
59
|
+
|
|
60
|
+
# Task category constants
|
|
61
|
+
TASK_CATEGORY_INDEXING = "indexing"
|
|
62
|
+
TASK_CATEGORY_PROVISIONING = "provisioning"
|
|
63
|
+
TASK_CATEGORY_CHANGE_TRACKING = "change_tracking"
|
|
64
|
+
TASK_CATEGORY_CACHE = "cache"
|
|
65
|
+
TASK_CATEGORY_RELATIONS = "relations"
|
|
66
|
+
TASK_CATEGORY_STATUS = "status"
|
|
67
|
+
TASK_CATEGORY_VALIDATION = "validation"
|
|
68
|
+
TASK_CATEGORY_OTHER = "other"
|
|
69
|
+
|
|
70
|
+
# Default summary categories
|
|
71
|
+
DEFAULT_SUMMARY_CATEGORIES = {
|
|
72
|
+
TASK_CATEGORY_INDEXING: "Indexing",
|
|
73
|
+
TASK_CATEGORY_PROVISIONING: "Provisioning",
|
|
74
|
+
TASK_CATEGORY_CHANGE_TRACKING: "Change tracking",
|
|
75
|
+
TASK_CATEGORY_RELATIONS: "Relations",
|
|
76
|
+
TASK_CATEGORY_STATUS: "Status",
|
|
77
|
+
TASK_CATEGORY_VALIDATION: "Validation",
|
|
78
|
+
TASK_CATEGORY_OTHER: "Other"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Parallel task categories (for duration calculation)
|
|
82
|
+
PARALLEL_TASK_CATEGORIES = {
|
|
83
|
+
TASK_CATEGORY_INDEXING,
|
|
84
|
+
TASK_CATEGORY_PROVISIONING,
|
|
85
|
+
TASK_CATEGORY_VALIDATION,
|
|
86
|
+
TASK_CATEGORY_CHANGE_TRACKING
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Prompt constants
|
|
90
|
+
REFETCH = "[REFETCH LIST]"
|
|
91
|
+
MANUAL_ENTRY = "[MANUAL ENTRY]"
|
|
92
|
+
|
|
93
|
+
# Timing constants
|
|
94
|
+
HIGHLIGHT_DURATION = 2.0
|
|
95
|
+
COMPLETION_DISPLAY_DURATION = 8.0
|
|
96
|
+
TIMER_CHECK_INTERVAL = 0.1
|
|
97
|
+
SPINNER_UPDATE_INTERVAL = 0.15
|
|
98
|
+
INITIAL_DISPLAY_DELAY = 0.25
|
|
99
|
+
BRIEF_PAUSE = 0.1
|
|
100
|
+
LIVE_REFRESH_RATE = 10
|
|
101
|
+
|
|
102
|
+
#--------------------------------------------------
|
|
103
|
+
# Style
|
|
104
|
+
#--------------------------------------------------
|
|
105
|
+
|
|
106
|
+
STYLE = inquirer_utils.get_style({
|
|
107
|
+
"fuzzy_prompt": "#e5c07b"
|
|
108
|
+
}, False)
|
|
109
|
+
|
|
110
|
+
#--------------------------------------------------
|
|
111
|
+
# Helpers
|
|
112
|
+
#--------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def rich_str(string:str, style:str|None = None) -> str:
|
|
115
|
+
output = io.StringIO()
|
|
116
|
+
console = Console(file=output, force_terminal=True)
|
|
117
|
+
console.print(string, style=style)
|
|
118
|
+
return output.getvalue()
|
|
119
|
+
|
|
120
|
+
def _load_ipython_display() -> tuple[Any, Callable[..., Any]]:
|
|
121
|
+
"""Load IPython display helpers, raising if unavailable."""
|
|
122
|
+
try:
|
|
123
|
+
module = importlib.import_module("IPython.display")
|
|
124
|
+
except ImportError as exc: # pragma: no cover - only triggered without IPython
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"NotebookTaskProgress requires IPython when running in a notebook environment."
|
|
127
|
+
) from exc
|
|
128
|
+
|
|
129
|
+
html_factory = getattr(module, "HTML")
|
|
130
|
+
display_fn = getattr(module, "display")
|
|
131
|
+
return html_factory, cast(Callable[..., Any], display_fn)
|
|
132
|
+
|
|
133
|
+
def nat_path(path: Path, base: Path):
|
|
134
|
+
resolved_path = path.resolve()
|
|
135
|
+
resolved_base = base.resolve()
|
|
136
|
+
if resolved_base in resolved_path.parents or resolved_path == resolved_base:
|
|
137
|
+
return resolved_path.relative_to(resolved_base)
|
|
138
|
+
else:
|
|
139
|
+
return resolved_path.absolute()
|
|
140
|
+
|
|
141
|
+
def get_default(value:str|None, list_of_values:Sequence[str]):
|
|
142
|
+
if value is None:
|
|
143
|
+
return None
|
|
144
|
+
list_of_values_lower = [v.lower() for v in list_of_values]
|
|
145
|
+
value_lower = value.lower()
|
|
146
|
+
if value_lower in list_of_values_lower:
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
#--------------------------------------------------
|
|
150
|
+
# Dividers
|
|
151
|
+
#--------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def divider(console=None, flush=False):
|
|
154
|
+
div = "\n[dim]---------------------------------------------------\n "
|
|
155
|
+
if console is None:
|
|
156
|
+
rich.print(div)
|
|
157
|
+
else:
|
|
158
|
+
console.print(div)
|
|
159
|
+
if flush:
|
|
160
|
+
sys.stdout.flush()
|
|
161
|
+
|
|
162
|
+
def abort():
|
|
163
|
+
rich.print()
|
|
164
|
+
rich.print("[yellow]Aborted")
|
|
165
|
+
divider()
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
168
|
+
#--------------------------------------------------
|
|
169
|
+
# Prompts
|
|
170
|
+
#--------------------------------------------------
|
|
171
|
+
|
|
172
|
+
default_bindings = cast(Any, {
|
|
173
|
+
"interrupt": [
|
|
174
|
+
{"key": "escape"},
|
|
175
|
+
{"key": "c-c"},
|
|
176
|
+
{"key": "c-d"}
|
|
177
|
+
],
|
|
178
|
+
"skip": [
|
|
179
|
+
{"key": "c-s"}
|
|
180
|
+
]
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
def prompt(message:str, value:str|None, newline=False, validator:Callable|None = None, invalid_message:str|None = None) -> str:
|
|
184
|
+
if value:
|
|
185
|
+
return value
|
|
186
|
+
if invalid_message is None:
|
|
187
|
+
invalid_message = "Invalid input"
|
|
188
|
+
try:
|
|
189
|
+
result:str = inquirer.text(
|
|
190
|
+
message,
|
|
191
|
+
validate=validator,
|
|
192
|
+
invalid_message=invalid_message,
|
|
193
|
+
keybindings=default_bindings,
|
|
194
|
+
).execute()
|
|
195
|
+
except KeyboardInterrupt:
|
|
196
|
+
abort()
|
|
197
|
+
raise Exception("Unreachable")
|
|
198
|
+
if newline:
|
|
199
|
+
rich.print("")
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def select(message:str, choices:List[str|Choice], value:str|None, newline=False, **kwargs) -> str|Any:
|
|
203
|
+
if value:
|
|
204
|
+
return value
|
|
205
|
+
try:
|
|
206
|
+
result:str = inquirer.select(message, choices, keybindings=default_bindings, **kwargs).execute()
|
|
207
|
+
except KeyboardInterrupt:
|
|
208
|
+
abort()
|
|
209
|
+
raise Exception("Unreachable")
|
|
210
|
+
if newline:
|
|
211
|
+
rich.print("")
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
def _enumerate_static_choices(choices: inquirer_utils.InquirerPyChoice) -> inquirer_utils.InquirerPyChoice:
|
|
215
|
+
return [{"name": f"{i+1} {choice}", "value": choice} for i, choice in enumerate(choices)]
|
|
216
|
+
|
|
217
|
+
def _enumerate_choices(choices: inquirer_utils.InquirerPyListChoices) -> inquirer_utils.InquirerPyListChoices:
|
|
218
|
+
if callable(choices):
|
|
219
|
+
return lambda session: _enumerate_static_choices(choices(session))
|
|
220
|
+
else:
|
|
221
|
+
return _enumerate_static_choices(choices)
|
|
222
|
+
|
|
223
|
+
def _fuzzy(message:str, choices:inquirer_utils.InquirerPyListChoices, default:str|None = None, multiselect=False, show_index=False, **kwargs) -> str|list[str]|None:
|
|
224
|
+
if show_index:
|
|
225
|
+
choices = _enumerate_choices(choices)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
kwargs["keybindings"] = default_bindings
|
|
229
|
+
if multiselect:
|
|
230
|
+
kwargs["keybindings"] = { # pylint: disable=assignment-from-no-return
|
|
231
|
+
"toggle": [
|
|
232
|
+
{"key": "tab"}, # toggle choices
|
|
233
|
+
],
|
|
234
|
+
"toggle-down": [
|
|
235
|
+
{"key": "tab", "filter":False},
|
|
236
|
+
],
|
|
237
|
+
}.update(default_bindings)
|
|
238
|
+
kwargs["multiselect"] = True
|
|
239
|
+
|
|
240
|
+
# NOTE: Using the builtin `default` kwarg to do this also filters
|
|
241
|
+
# results which is undesirable and confusing for pre-filled
|
|
242
|
+
# fields, so we move the cursor ourselves using the internals :(
|
|
243
|
+
prompt = inquirer.fuzzy(message, choices=choices, max_height=8, border=True, style=STYLE, **kwargs)
|
|
244
|
+
prompt._content_control._get_choices(prompt._content_control.choices, default)
|
|
245
|
+
|
|
246
|
+
return prompt.execute()
|
|
247
|
+
except KeyboardInterrupt:
|
|
248
|
+
return abort()
|
|
249
|
+
|
|
250
|
+
def fuzzy(message:str, choices:inquirer_utils.InquirerPyListChoices, default:str|None = None, show_index=False, **kwargs) -> str:
|
|
251
|
+
return cast(str, _fuzzy(message, choices, default=default, show_index=show_index, **kwargs))
|
|
252
|
+
|
|
253
|
+
def fuzzy_multiselect(message:str, choices:inquirer_utils.InquirerPyListChoices, default:str|None = None, show_index=False, **kwargs) -> list[str]:
|
|
254
|
+
return cast(list[str], _fuzzy(message, choices, default=default, show_index=show_index, multiselect=True, **kwargs))
|
|
255
|
+
|
|
256
|
+
def fuzzy_with_refetch(prompt: str, type: str, fn: Callable, *args, **kwargs):
|
|
257
|
+
exception = None
|
|
258
|
+
auto_select = kwargs.get("auto_select", None)
|
|
259
|
+
not_found_message = kwargs.get("not_found_message", None)
|
|
260
|
+
manual_entry = kwargs.get("manual_entry", None)
|
|
261
|
+
items = []
|
|
262
|
+
with Spinner(f"Fetching {type}", f"Fetched {type}"):
|
|
263
|
+
try:
|
|
264
|
+
items = fn(*args)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
exception = e
|
|
267
|
+
if exception is not None:
|
|
268
|
+
rich.print(f"\n[red]Error fetching {type}: {exception}\n")
|
|
269
|
+
return exception
|
|
270
|
+
if len(items) == 0:
|
|
271
|
+
if not_found_message:
|
|
272
|
+
rich.print(f"\n[yellow]{not_found_message}\n")
|
|
273
|
+
else:
|
|
274
|
+
rich.print(f"\n[yellow]No valid {type} found\n")
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
if auto_select and len(items) == 1 and items[0].lower() == auto_select.lower():
|
|
278
|
+
return auto_select
|
|
279
|
+
|
|
280
|
+
if manual_entry:
|
|
281
|
+
items.insert(0, MANUAL_ENTRY)
|
|
282
|
+
items.insert(0, REFETCH)
|
|
283
|
+
|
|
284
|
+
passed_default = kwargs.get("default", None)
|
|
285
|
+
passed_mandatory = kwargs.get("mandatory", False)
|
|
286
|
+
|
|
287
|
+
rich.print("")
|
|
288
|
+
result = fuzzy(
|
|
289
|
+
prompt,
|
|
290
|
+
items,
|
|
291
|
+
default=get_default(passed_default, items),
|
|
292
|
+
mandatory=passed_mandatory
|
|
293
|
+
)
|
|
294
|
+
rich.print("")
|
|
295
|
+
|
|
296
|
+
while result == REFETCH:
|
|
297
|
+
result = fuzzy_with_refetch(prompt, type, fn, *args, **kwargs)
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
def confirm(message:str, default:bool = False) -> bool:
|
|
301
|
+
try:
|
|
302
|
+
return inquirer.confirm(message, default=default, keybindings=default_bindings).execute()
|
|
303
|
+
except KeyboardInterrupt:
|
|
304
|
+
return abort()
|
|
305
|
+
|
|
306
|
+
def text(message:str, default:str|None = None, validator:Callable|None = None, invalid_message:str|None = None, **kwargs) -> str:
|
|
307
|
+
if not invalid_message:
|
|
308
|
+
invalid_message = "Invalid input"
|
|
309
|
+
try:
|
|
310
|
+
return inquirer.text(
|
|
311
|
+
message,
|
|
312
|
+
default=default or "",
|
|
313
|
+
keybindings=default_bindings,
|
|
314
|
+
validate=validator,
|
|
315
|
+
invalid_message=invalid_message,
|
|
316
|
+
**kwargs
|
|
317
|
+
).execute()
|
|
318
|
+
except KeyboardInterrupt:
|
|
319
|
+
return abort()
|
|
320
|
+
|
|
321
|
+
def password(message:str, default:str|None = None, validator:Callable|None = None, invalid_message:str|None = None) -> str:
|
|
322
|
+
if invalid_message is None:
|
|
323
|
+
invalid_message = "Invalid input"
|
|
324
|
+
try:
|
|
325
|
+
return inquirer.secret(
|
|
326
|
+
message,
|
|
327
|
+
default=default or "",
|
|
328
|
+
keybindings=default_bindings,
|
|
329
|
+
validate=validator,
|
|
330
|
+
invalid_message=invalid_message
|
|
331
|
+
).execute()
|
|
332
|
+
except KeyboardInterrupt:
|
|
333
|
+
return abort()
|
|
334
|
+
|
|
335
|
+
def number(message:str, default:float|int|None = None, validator:Callable|None = None, invalid_message:str|None = None, **kwargs) -> float|int:
|
|
336
|
+
if not invalid_message:
|
|
337
|
+
invalid_message = "Invalid input"
|
|
338
|
+
try:
|
|
339
|
+
return inquirer.number(
|
|
340
|
+
message,
|
|
341
|
+
default=default or 0,
|
|
342
|
+
keybindings=default_bindings,
|
|
343
|
+
validate=validator,
|
|
344
|
+
invalid_message=invalid_message,
|
|
345
|
+
**kwargs
|
|
346
|
+
).execute()
|
|
347
|
+
except KeyboardInterrupt:
|
|
348
|
+
return abort()
|
|
349
|
+
|
|
350
|
+
def file(message: str, start_path:Path|None = None, allow_freeform=False, **kwargs) -> str|None:
|
|
351
|
+
try:
|
|
352
|
+
return FuzzyFile(message, start_path, allow_freeform=allow_freeform, max_height=8, border=True, style=STYLE, **kwargs).execute()
|
|
353
|
+
except KeyboardInterrupt:
|
|
354
|
+
return abort()
|
|
355
|
+
|
|
356
|
+
class FuzzyFile(inquirer.fuzzy):
|
|
357
|
+
def __init__(self, message: str, initial_path: Path|None = None, allow_freeform = False, *args, **kwargs):
|
|
358
|
+
self.initial_path = initial_path or Path()
|
|
359
|
+
self.current_path = Path(self.initial_path)
|
|
360
|
+
self.allow_freeform = allow_freeform
|
|
361
|
+
|
|
362
|
+
kwargs["keybindings"] = {
|
|
363
|
+
**default_bindings,
|
|
364
|
+
"answer": [
|
|
365
|
+
{"key": os.sep},
|
|
366
|
+
{"key": "enter"},
|
|
367
|
+
{"key": "tab"},
|
|
368
|
+
{"key": "right"}
|
|
369
|
+
],
|
|
370
|
+
**kwargs.get("keybindings", {})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
super().__init__(message, *args, **kwargs, choices=self._get_choices)
|
|
374
|
+
|
|
375
|
+
def _get_prompt_message(self) -> List[tuple[str, str]]:
|
|
376
|
+
pre_answer = ("class:instruction", f" {self.instruction} " if self.instruction else " ")
|
|
377
|
+
result = str(nat_path(self.current_path, self.initial_path))
|
|
378
|
+
|
|
379
|
+
if result:
|
|
380
|
+
sep = " " if self._amark else ""
|
|
381
|
+
return [
|
|
382
|
+
("class:answermark", self._amark),
|
|
383
|
+
("class:answered_question", f"{sep}{self._message} "),
|
|
384
|
+
("class:answer", f"{result}{os.sep if not self.status['answered'] else ''}"),
|
|
385
|
+
]
|
|
386
|
+
else:
|
|
387
|
+
sep = " " if self._qmark else ""
|
|
388
|
+
return [
|
|
389
|
+
("class:answermark", self._amark),
|
|
390
|
+
("class:questionmark", self._qmark),
|
|
391
|
+
("class:question", f"{sep}{self._message}"),
|
|
392
|
+
pre_answer
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
def _handle_enter(self, event: KeyPressEvent) -> None:
|
|
396
|
+
try:
|
|
397
|
+
fake_document = FakeDocument(self.result_value)
|
|
398
|
+
self._validator.validate(fake_document) # type: ignore
|
|
399
|
+
cc = self.content_control
|
|
400
|
+
if self._multiselect:
|
|
401
|
+
self.status["answered"] = True
|
|
402
|
+
if not self.selected_choices:
|
|
403
|
+
self.status["result"] = [cc.selection["name"]]
|
|
404
|
+
event.app.exit(result=[cc.selection["value"]])
|
|
405
|
+
else:
|
|
406
|
+
self.status["result"] = self.result_name
|
|
407
|
+
event.app.exit(result=self.result_value)
|
|
408
|
+
else:
|
|
409
|
+
res_value = cc.selection["value"]
|
|
410
|
+
self.current_path /= res_value
|
|
411
|
+
if self.current_path.is_dir():
|
|
412
|
+
self._update_choices()
|
|
413
|
+
else:
|
|
414
|
+
self.status["answered"] = True
|
|
415
|
+
self.status["result"] = cc.selection["name"]
|
|
416
|
+
event.app.exit(result=str(nat_path(self.current_path, self.initial_path)))
|
|
417
|
+
except ValidationError as e:
|
|
418
|
+
self._set_error(str(e))
|
|
419
|
+
except IndexError:
|
|
420
|
+
self.status["answered"] = True
|
|
421
|
+
res = self._get_current_text() if self.allow_freeform else None
|
|
422
|
+
if self._multiselect:
|
|
423
|
+
res = [res] if res is not None else []
|
|
424
|
+
self.status["result"] = res
|
|
425
|
+
event.app.exit(result=res)
|
|
426
|
+
|
|
427
|
+
def _get_choices(self, _ = None):
|
|
428
|
+
choices = os.listdir(self.current_path)
|
|
429
|
+
choices.append("..")
|
|
430
|
+
return choices
|
|
431
|
+
|
|
432
|
+
def _update_choices(self):
|
|
433
|
+
raw_choices = self._get_choices()
|
|
434
|
+
cc = self.content_control
|
|
435
|
+
cc.selected_choice_index = 0
|
|
436
|
+
cc._raw_choices = raw_choices
|
|
437
|
+
cc.choices = cc._get_choices(raw_choices, None)
|
|
438
|
+
cc._safety_check()
|
|
439
|
+
cc._format_choices()
|
|
440
|
+
self._buffer.reset()
|
|
441
|
+
|
|
442
|
+
#--------------------------------------------------
|
|
443
|
+
# Line Clearing Mixin
|
|
444
|
+
#--------------------------------------------------
|
|
445
|
+
|
|
446
|
+
class LineClearingMixin:
|
|
447
|
+
"""Mixin class that provides line clearing functionality for different environments."""
|
|
448
|
+
|
|
449
|
+
def __init__(self, *args, **kwargs):
|
|
450
|
+
super().__init__(*args, **kwargs)
|
|
451
|
+
self.last_line_length = 0
|
|
452
|
+
# Detect environment capabilities
|
|
453
|
+
import sys
|
|
454
|
+
self.is_tty = sys.stdout.isatty()
|
|
455
|
+
self.is_snowflake_notebook = isinstance(runtime_env, SnowbookEnvironment)
|
|
456
|
+
self.is_jupyter = isinstance(runtime_env, JupyterEnvironment)
|
|
457
|
+
|
|
458
|
+
def _get_terminal_width(self):
|
|
459
|
+
"""Get terminal width, with fallback to reasonable default."""
|
|
460
|
+
try:
|
|
461
|
+
return shutil.get_terminal_size().columns
|
|
462
|
+
except (OSError, AttributeError):
|
|
463
|
+
return 80 # Fallback width
|
|
464
|
+
|
|
465
|
+
def _clear_line(self, new_text: str):
|
|
466
|
+
"""Clear the current line and write new text using the best available method."""
|
|
467
|
+
import sys
|
|
468
|
+
|
|
469
|
+
if self.is_tty and not self.is_snowflake_notebook and not self.is_jupyter:
|
|
470
|
+
# Use proper ANSI clear line sequence for terminals
|
|
471
|
+
sys.stdout.write(f"\r\033[K{new_text}")
|
|
472
|
+
else:
|
|
473
|
+
# For notebooks and environments without ANSI support, use smart padding
|
|
474
|
+
terminal_width = self._get_terminal_width()
|
|
475
|
+
|
|
476
|
+
# Truncate text if it exceeds terminal width to prevent wrapping
|
|
477
|
+
if len(new_text) > terminal_width:
|
|
478
|
+
new_text = new_text[:terminal_width - 3] + "..."
|
|
479
|
+
|
|
480
|
+
# Calculate how much of the line we need to clear
|
|
481
|
+
# Use the maximum of last line length or terminal width to ensure full clearing
|
|
482
|
+
clear_width = max(self.last_line_length, terminal_width)
|
|
483
|
+
|
|
484
|
+
# Clear with spaces and write new text
|
|
485
|
+
sys.stdout.write(f"\r{' ' * clear_width}\r{new_text}")
|
|
486
|
+
|
|
487
|
+
sys.stdout.flush()
|
|
488
|
+
# Update the tracked line length
|
|
489
|
+
self.last_line_length = len(new_text)
|
|
490
|
+
|
|
491
|
+
def _write_line(self, text: str, newline: bool = False):
|
|
492
|
+
"""Write text to the current line, optionally adding a newline."""
|
|
493
|
+
import sys
|
|
494
|
+
if newline:
|
|
495
|
+
sys.stdout.write(f"{text}\n")
|
|
496
|
+
else:
|
|
497
|
+
sys.stdout.write(text)
|
|
498
|
+
sys.stdout.flush()
|
|
499
|
+
|
|
500
|
+
def _clear_and_write(self, text: str, newline: bool = False):
|
|
501
|
+
"""Clear the current line and write new text, with optional newline."""
|
|
502
|
+
self._clear_line(text)
|
|
503
|
+
if newline:
|
|
504
|
+
import sys
|
|
505
|
+
sys.stdout.write("\n")
|
|
506
|
+
sys.stdout.flush()
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
#--------------------------------------------------
|
|
510
|
+
# Spinner
|
|
511
|
+
#--------------------------------------------------
|
|
512
|
+
|
|
513
|
+
class Spinner(LineClearingMixin):
|
|
514
|
+
"""Shows a spinner control while a task is running.
|
|
515
|
+
The finished_message will not be printed if there was an exception and the failed_message is provided.
|
|
516
|
+
"""
|
|
517
|
+
busy = False
|
|
518
|
+
|
|
519
|
+
def __init__(
|
|
520
|
+
self,
|
|
521
|
+
message="",
|
|
522
|
+
finished_message: str = "",
|
|
523
|
+
failed_message=None,
|
|
524
|
+
delay=None,
|
|
525
|
+
leading_newline=False,
|
|
526
|
+
trailing_newline=False,
|
|
527
|
+
):
|
|
528
|
+
self.message = message
|
|
529
|
+
self.finished_message = finished_message
|
|
530
|
+
self.failed_message = failed_message
|
|
531
|
+
self.spinner_generator = itertools.cycle(SPINNER_FRAMES)
|
|
532
|
+
self.is_snowflake_notebook = isinstance(runtime_env, SnowbookEnvironment)
|
|
533
|
+
self.is_hex = isinstance(runtime_env, HexEnvironment)
|
|
534
|
+
self.is_jupyter = isinstance(runtime_env, JupyterEnvironment)
|
|
535
|
+
self.in_notebook = isinstance(runtime_env, NotebookRuntimeEnvironment)
|
|
536
|
+
self.is_tty = sys.stdout.isatty()
|
|
537
|
+
|
|
538
|
+
self._set_delay(delay)
|
|
539
|
+
self.leading_newline = leading_newline
|
|
540
|
+
self.trailing_newline = trailing_newline
|
|
541
|
+
self.last_message = ""
|
|
542
|
+
self.display = None
|
|
543
|
+
# Add lock to prevent race conditions between spinner thread and main thread
|
|
544
|
+
self._update_lock = threading.Lock()
|
|
545
|
+
|
|
546
|
+
def _set_delay(self, delay: float|int|None) -> None:
|
|
547
|
+
"""Set appropriate delay based on environment and user input."""
|
|
548
|
+
# If delay value is provided, validate and use it
|
|
549
|
+
if delay:
|
|
550
|
+
if isinstance(delay, (int, float)) and delay > 0:
|
|
551
|
+
self.delay = float(delay)
|
|
552
|
+
return
|
|
553
|
+
else:
|
|
554
|
+
raise ValueError(f"Invalid delay value: {delay}")
|
|
555
|
+
# Otherwise, set delay based on environment
|
|
556
|
+
elif self.is_hex:
|
|
557
|
+
self.delay = 0 # Hex tries to append a new block each frame
|
|
558
|
+
elif self.is_snowflake_notebook:
|
|
559
|
+
self.delay = 0.5 # SF notebooks get bogged down
|
|
560
|
+
elif self.in_notebook or self.is_tty:
|
|
561
|
+
# Fast refresh for other notebooks or terminals with good printing support
|
|
562
|
+
self.delay = 0.1
|
|
563
|
+
else:
|
|
564
|
+
# Otherwise disable the spinner animation entirely
|
|
565
|
+
# for non-interactive environments.
|
|
566
|
+
self.delay = 0
|
|
567
|
+
|
|
568
|
+
def get_message(self, starting=False):
|
|
569
|
+
max_width = shutil.get_terminal_size().columns
|
|
570
|
+
spinner = "⏳⏳⏳⏳" if not self.is_tty and starting else next(self.spinner_generator)
|
|
571
|
+
full_message = f"{spinner} {self.message}"
|
|
572
|
+
if len(full_message) > max_width:
|
|
573
|
+
return full_message[:max_width - 3] + "..."
|
|
574
|
+
else:
|
|
575
|
+
return full_message
|
|
576
|
+
|
|
577
|
+
def update(self, message:str|None=None, color:str|None=None, file:TextIO|None=None, starting=False):
|
|
578
|
+
# Use lock to prevent race conditions between spinner thread and main thread
|
|
579
|
+
with self._update_lock:
|
|
580
|
+
if message is None:
|
|
581
|
+
message = self.get_message(starting=starting)
|
|
582
|
+
if self.is_jupyter:
|
|
583
|
+
# @NOTE: IPython isn't available in CI. This won't ever get invoked w/out IPython available though.
|
|
584
|
+
from IPython.display import HTML, display # pyright: ignore[reportMissingImports]
|
|
585
|
+
color_string = ""
|
|
586
|
+
if color:
|
|
587
|
+
color_value = Color.parse(color)
|
|
588
|
+
rgb_tuple = color_value.get_truecolor()
|
|
589
|
+
rgb_hex = f"#{rgb_tuple[0]:02X}{rgb_tuple[1]:02X}{rgb_tuple[2]:02X}"
|
|
590
|
+
color_string = f"color: {rgb_hex};" if color is not None else ""
|
|
591
|
+
content = HTML(f"<span style='font-family: monospace;{color_string}'>{message}</span>")
|
|
592
|
+
if self.display is not None:
|
|
593
|
+
self.display.update(content)
|
|
594
|
+
else:
|
|
595
|
+
self.display = display(content, display_id=True)
|
|
596
|
+
else:
|
|
597
|
+
if self.can_use_terminal_colors() and color is not None:
|
|
598
|
+
rich_message = f"[{color}]{message}"
|
|
599
|
+
else:
|
|
600
|
+
rich_message = message
|
|
601
|
+
rich_string = rich_str(rich_message)
|
|
602
|
+
def width(word):
|
|
603
|
+
return sum(wcwidth(c) for c in word)
|
|
604
|
+
diff = width(self.last_message) - width(rich_string)
|
|
605
|
+
self.reset_cursor()
|
|
606
|
+
# Use rich.print with lock protection
|
|
607
|
+
output_file = file or sys.stdout
|
|
608
|
+
rich.print(rich_message + (" " * diff), file=output_file, end="", flush=False)
|
|
609
|
+
if output_file.isatty() or self.in_notebook:
|
|
610
|
+
output_file.flush()
|
|
611
|
+
self.last_message = rich_string
|
|
612
|
+
|
|
613
|
+
def can_use_terminal_colors(self):
|
|
614
|
+
return not self.is_snowflake_notebook
|
|
615
|
+
|
|
616
|
+
def update_messages(self, updater: dict[str, str]):
|
|
617
|
+
if "message" in updater:
|
|
618
|
+
self.message = updater["message"]
|
|
619
|
+
if "finished_message" in updater:
|
|
620
|
+
self.finished_message = updater["finished_message"]
|
|
621
|
+
if "failed_message" in updater:
|
|
622
|
+
self.failed_message = updater["failed_message"]
|
|
623
|
+
self.update()
|
|
624
|
+
|
|
625
|
+
def spinner_task(self):
|
|
626
|
+
while self.busy and self.delay:
|
|
627
|
+
self.update(color="magenta")
|
|
628
|
+
time.sleep(self.delay) #type: ignore[union-attr] | we only call spinner_task if delay is not None anyway
|
|
629
|
+
self.reset_cursor()
|
|
630
|
+
|
|
631
|
+
def reset_cursor(self):
|
|
632
|
+
if self.is_tty:
|
|
633
|
+
# Clear the entire line and move cursor to beginning
|
|
634
|
+
sys.stdout.write("\r\033[K")
|
|
635
|
+
elif not self.is_jupyter:
|
|
636
|
+
sys.stdout.write("\r")
|
|
637
|
+
|
|
638
|
+
def __enter__(self):
|
|
639
|
+
if self.leading_newline:
|
|
640
|
+
rich.print()
|
|
641
|
+
self.update(color="magenta", starting=True)
|
|
642
|
+
# return control to the event loop briefly so stdout can be sure to flush:
|
|
643
|
+
if self.delay:
|
|
644
|
+
time.sleep(INITIAL_DISPLAY_DELAY)
|
|
645
|
+
self.reset_cursor()
|
|
646
|
+
if not self.delay:
|
|
647
|
+
return self
|
|
648
|
+
self.busy = True
|
|
649
|
+
threading.Thread(target=self.spinner_task, daemon=True).start()
|
|
650
|
+
return self
|
|
651
|
+
|
|
652
|
+
def __exit__(self, exception, value, _):
|
|
653
|
+
self.busy = False
|
|
654
|
+
if exception is not None:
|
|
655
|
+
if self.failed_message is not None:
|
|
656
|
+
self.update(f"{self.failed_message} {value}", color="yellow", file=sys.stderr)
|
|
657
|
+
# Use rich.print with explicit newline to ensure proper formatting
|
|
658
|
+
rich.print(file=sys.stderr)
|
|
659
|
+
return True
|
|
660
|
+
return False
|
|
661
|
+
if self.delay: # will be None for non-interactive environments
|
|
662
|
+
time.sleep(self.delay)
|
|
663
|
+
self.reset_cursor()
|
|
664
|
+
if self.finished_message != "":
|
|
665
|
+
final_message = f"▰▰▰▰ {self.finished_message}"
|
|
666
|
+
self.update(final_message, color="green")
|
|
667
|
+
# Use rich.print with explicit newline to ensure proper formatting
|
|
668
|
+
rich.print()
|
|
669
|
+
elif self.finished_message == "":
|
|
670
|
+
self.update("")
|
|
671
|
+
self.reset_cursor()
|
|
672
|
+
if self.trailing_newline:
|
|
673
|
+
rich.print()
|
|
674
|
+
|
|
675
|
+
class DebuggingSpan:
|
|
676
|
+
span: debugging.Span
|
|
677
|
+
def __init__(self, span_type: str):
|
|
678
|
+
self.span_type = span_type
|
|
679
|
+
self.span_attrs = {}
|
|
680
|
+
|
|
681
|
+
def attrs(self, **kwargs):
|
|
682
|
+
self.span_attrs = kwargs
|
|
683
|
+
return self
|
|
684
|
+
|
|
685
|
+
def __enter__(self):
|
|
686
|
+
self.span = debugging.span_start(self.span_type, **self.span_attrs)
|
|
687
|
+
return self
|
|
688
|
+
|
|
689
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
690
|
+
debugging.span_end(self.span)
|
|
691
|
+
|
|
692
|
+
class SpanSpinner(Spinner):
|
|
693
|
+
span: debugging.Span
|
|
694
|
+
def __init__(self, span_type: str, *spinner_args, **spinner_kwargs):
|
|
695
|
+
super().__init__(*spinner_args, **spinner_kwargs)
|
|
696
|
+
self.span_type = span_type
|
|
697
|
+
self.span_attrs = {}
|
|
698
|
+
|
|
699
|
+
def attrs(self, **kwargs):
|
|
700
|
+
self.span_attrs = kwargs
|
|
701
|
+
return self
|
|
702
|
+
|
|
703
|
+
def __enter__(self):
|
|
704
|
+
self.span = debugging.span_start(self.span_type, **self.span_attrs)
|
|
705
|
+
return super().__enter__()
|
|
706
|
+
|
|
707
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
708
|
+
super().__exit__(exc_type, exc_val, exc_tb)
|
|
709
|
+
debugging.span_end(self.span)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
@dataclass
|
|
713
|
+
class TaskInfo:
|
|
714
|
+
"""Represents a single task with its state and metadata."""
|
|
715
|
+
description: str
|
|
716
|
+
category: str = "other"
|
|
717
|
+
completed: bool = False
|
|
718
|
+
added_time: float = 0.0
|
|
719
|
+
completed_time: float = 0.0
|
|
720
|
+
hidden: bool = False
|
|
721
|
+
|
|
722
|
+
def __post_init__(self):
|
|
723
|
+
if self.added_time == 0.0:
|
|
724
|
+
self.added_time = time.time()
|
|
725
|
+
|
|
726
|
+
def get_duration(self) -> float:
|
|
727
|
+
"""Get the duration of this task in seconds."""
|
|
728
|
+
if not self.completed or self.completed_time == 0.0:
|
|
729
|
+
return 0.0
|
|
730
|
+
|
|
731
|
+
return self.completed_time - self.added_time
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
class _TimerManager:
|
|
735
|
+
"""Manages all delayed operations for TaskProgress."""
|
|
736
|
+
|
|
737
|
+
def __init__(self, progress_instance):
|
|
738
|
+
self._progress = progress_instance
|
|
739
|
+
self._operations = {} # task_id -> (operation_type, scheduled_time)
|
|
740
|
+
self._thread = None
|
|
741
|
+
self._running = False
|
|
742
|
+
|
|
743
|
+
def schedule_highlight_removal(self, task_id: str, delay: float | None = None):
|
|
744
|
+
"""Schedule removal of highlighting for a task."""
|
|
745
|
+
if delay is None:
|
|
746
|
+
delay = HIGHLIGHT_DURATION
|
|
747
|
+
scheduled_time = time.time() + delay
|
|
748
|
+
self._operations[task_id] = ("remove_highlighting", scheduled_time)
|
|
749
|
+
self._start()
|
|
750
|
+
|
|
751
|
+
def schedule_task_removal(self, task_id: str, delay: float | None = None):
|
|
752
|
+
"""Schedule removal of a completed task."""
|
|
753
|
+
if delay is None:
|
|
754
|
+
delay = COMPLETION_DISPLAY_DURATION
|
|
755
|
+
scheduled_time = time.time() + delay
|
|
756
|
+
self._operations[task_id] = ("delayed_removal", scheduled_time)
|
|
757
|
+
self._start()
|
|
758
|
+
|
|
759
|
+
def schedule_task_hiding(self, task_id: str, delay: float | None = None):
|
|
760
|
+
"""Schedule hiding of a completed task from display (but keep in data structure)."""
|
|
761
|
+
if delay is None:
|
|
762
|
+
delay = COMPLETION_DISPLAY_DURATION
|
|
763
|
+
scheduled_time = time.time() + delay
|
|
764
|
+
self._operations[task_id] = ("delayed_hiding", scheduled_time)
|
|
765
|
+
self._start()
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _start(self):
|
|
769
|
+
"""Start the timer thread if not already running."""
|
|
770
|
+
if self._thread is None or not self._thread.is_alive():
|
|
771
|
+
self._running = True
|
|
772
|
+
self._thread = threading.Thread(target=self._worker, daemon=True)
|
|
773
|
+
self._thread.start()
|
|
774
|
+
|
|
775
|
+
def _worker(self):
|
|
776
|
+
"""Worker thread for handling delayed operations."""
|
|
777
|
+
while self._running:
|
|
778
|
+
current_time = time.time()
|
|
779
|
+
completed_ops = []
|
|
780
|
+
|
|
781
|
+
# Find completed operations
|
|
782
|
+
for task_id, (op_type, scheduled_time) in self._operations.items():
|
|
783
|
+
if current_time >= scheduled_time:
|
|
784
|
+
completed_ops.append((task_id, op_type))
|
|
785
|
+
|
|
786
|
+
# Process completed operations
|
|
787
|
+
for task_id, op_type in completed_ops:
|
|
788
|
+
self._process_operation(task_id, op_type)
|
|
789
|
+
del self._operations[task_id]
|
|
790
|
+
|
|
791
|
+
time.sleep(TIMER_CHECK_INTERVAL)
|
|
792
|
+
|
|
793
|
+
def _process_operation(self, task_id: str, op_type: str):
|
|
794
|
+
"""Process a completed delayed operation."""
|
|
795
|
+
progress = self._progress
|
|
796
|
+
if op_type == "remove_highlighting":
|
|
797
|
+
if hasattr(progress, "_highlighted_tasks") and task_id in progress._highlighted_tasks:
|
|
798
|
+
del progress._highlighted_tasks[task_id]
|
|
799
|
+
if hasattr(progress, "_after_task_update"):
|
|
800
|
+
progress._after_task_update()
|
|
801
|
+
elif op_type == "delayed_removal":
|
|
802
|
+
if hasattr(progress, "_tasks") and task_id in progress._tasks:
|
|
803
|
+
del progress._tasks[task_id]
|
|
804
|
+
if hasattr(progress, "_after_task_update"):
|
|
805
|
+
progress._after_task_update()
|
|
806
|
+
elif op_type == "delayed_hiding":
|
|
807
|
+
if hasattr(progress, "_tasks") and task_id in progress._tasks:
|
|
808
|
+
progress._tasks[task_id].hidden = True
|
|
809
|
+
if hasattr(progress, "_after_task_update"):
|
|
810
|
+
progress._after_task_update()
|
|
811
|
+
|
|
812
|
+
def stop(self):
|
|
813
|
+
"""Stop the timer manager."""
|
|
814
|
+
self._running = False
|
|
815
|
+
self._operations.clear()
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class _TaskStateMixin:
|
|
819
|
+
"""Shared task management helpers for notebook and terminal progress displays."""
|
|
820
|
+
|
|
821
|
+
enable_highlighting: bool = True
|
|
822
|
+
|
|
823
|
+
def _init_task_state(self, *, hide_on_completion: bool = False, show_duration_summary: bool = True) -> None:
|
|
824
|
+
self.hide_on_completion = hide_on_completion
|
|
825
|
+
self.show_duration_summary = show_duration_summary
|
|
826
|
+
self._tasks: dict[str, TaskInfo] = {}
|
|
827
|
+
self._next_task_id: int = 1
|
|
828
|
+
self._highlighted_tasks: dict[str, float] = {}
|
|
829
|
+
self._process_start_time: float | None = None
|
|
830
|
+
self._process_end_time: float | None = None
|
|
831
|
+
self.main_completed: bool = False
|
|
832
|
+
self.main_failed: bool = False
|
|
833
|
+
self._timer_manager = _TimerManager(self)
|
|
834
|
+
|
|
835
|
+
def _after_task_update(self) -> None:
|
|
836
|
+
"""Hook for subclasses to react when task state changes."""
|
|
837
|
+
# Implemented by subclasses when they need to update the display immediately.
|
|
838
|
+
return None
|
|
839
|
+
|
|
840
|
+
def _generate_task_id(self) -> str:
|
|
841
|
+
"""Generate a unique task ID."""
|
|
842
|
+
task_id = f"task_{self._next_task_id}"
|
|
843
|
+
self._next_task_id += 1
|
|
844
|
+
return task_id
|
|
845
|
+
|
|
846
|
+
def add_sub_task(self, description: str, task_id: str | None = None, category: str = "general") -> str:
|
|
847
|
+
"""Add a new sub-task and return its unique ID."""
|
|
848
|
+
if task_id is None:
|
|
849
|
+
task_id = self._generate_task_id()
|
|
850
|
+
|
|
851
|
+
if task_id not in self._tasks:
|
|
852
|
+
self._tasks[task_id] = TaskInfo(description=description, category=category)
|
|
853
|
+
self._after_task_update()
|
|
854
|
+
|
|
855
|
+
return task_id
|
|
856
|
+
|
|
857
|
+
def update_sub_task(self, task_id: str, description: str) -> None:
|
|
858
|
+
"""Update an existing sub-task description."""
|
|
859
|
+
if task_id in self._tasks:
|
|
860
|
+
task_info = self._tasks[task_id]
|
|
861
|
+
if self.enable_highlighting and task_info.description != description:
|
|
862
|
+
self._highlighted_tasks[task_id] = time.time() + HIGHLIGHT_DURATION
|
|
863
|
+
self._timer_manager.schedule_highlight_removal(task_id)
|
|
864
|
+
|
|
865
|
+
task_info.description = description
|
|
866
|
+
self._after_task_update()
|
|
867
|
+
|
|
868
|
+
def complete_sub_task(self, task_id: str, record_time: bool = True) -> None:
|
|
869
|
+
"""Complete a sub-task by marking it as done."""
|
|
870
|
+
if task_id in self._tasks:
|
|
871
|
+
if task_id in self._highlighted_tasks:
|
|
872
|
+
del self._highlighted_tasks[task_id]
|
|
873
|
+
|
|
874
|
+
if not self._tasks[task_id].completed and record_time:
|
|
875
|
+
self._tasks[task_id].completed_time = time.time()
|
|
876
|
+
self._tasks[task_id].completed = True
|
|
877
|
+
|
|
878
|
+
self._after_task_update()
|
|
879
|
+
self._timer_manager.schedule_task_hiding(task_id)
|
|
880
|
+
|
|
881
|
+
def remove_sub_task(self, task_id: str, animate: bool = True) -> None:
|
|
882
|
+
"""Remove a sub-task by ID with optional completion animation."""
|
|
883
|
+
if task_id in self._tasks:
|
|
884
|
+
if task_id in self._highlighted_tasks:
|
|
885
|
+
del self._highlighted_tasks[task_id]
|
|
886
|
+
|
|
887
|
+
if animate:
|
|
888
|
+
self.complete_sub_task(task_id)
|
|
889
|
+
else:
|
|
890
|
+
del self._tasks[task_id]
|
|
891
|
+
self._after_task_update()
|
|
892
|
+
|
|
893
|
+
def update_sub_status(self, sub_status: str) -> None:
|
|
894
|
+
"""Legacy method for backward compatibility - creates/updates a default sub-task."""
|
|
895
|
+
self.add_sub_task(sub_status, "default")
|
|
896
|
+
self.update_sub_task("default", sub_status)
|
|
897
|
+
|
|
898
|
+
def update_main_status(self, message: str) -> None:
|
|
899
|
+
"""Update the main status line with custom information."""
|
|
900
|
+
if getattr(self, "description", "") != message:
|
|
901
|
+
self.description = message
|
|
902
|
+
self._after_task_update()
|
|
903
|
+
|
|
904
|
+
def update_messages(self, updater: dict[str, str]) -> None:
|
|
905
|
+
"""Update both main message and sub-status if provided."""
|
|
906
|
+
if "message" in updater:
|
|
907
|
+
self.description = updater["message"]
|
|
908
|
+
self._after_task_update()
|
|
909
|
+
if "sub_status" in updater:
|
|
910
|
+
self.update_sub_status(updater["sub_status"])
|
|
911
|
+
if "success_message" in updater:
|
|
912
|
+
self.success_message = updater["success_message"]
|
|
913
|
+
if "failure_message" in updater:
|
|
914
|
+
self.failure_message = updater["failure_message"]
|
|
915
|
+
|
|
916
|
+
def get_sub_task_count(self) -> int:
|
|
917
|
+
"""Get the current number of active sub-tasks."""
|
|
918
|
+
return len(self._tasks)
|
|
919
|
+
|
|
920
|
+
def list_sub_tasks(self) -> list[str]:
|
|
921
|
+
"""Get a list of all active sub-task IDs."""
|
|
922
|
+
return list(self._tasks.keys())
|
|
923
|
+
|
|
924
|
+
def get_task_status(self) -> str:
|
|
925
|
+
"""Get a human-readable status of current task count vs limit."""
|
|
926
|
+
return f"› Active tasks: {len(self._tasks)}"
|
|
927
|
+
|
|
928
|
+
def get_task_duration(self, task_id: str) -> float:
|
|
929
|
+
"""Get the duration of a specific task in seconds."""
|
|
930
|
+
if task_id in self._tasks:
|
|
931
|
+
return self._tasks[task_id].get_duration()
|
|
932
|
+
return 0.0
|
|
933
|
+
|
|
934
|
+
def get_completed_tasks(self) -> dict[str, TaskInfo]:
|
|
935
|
+
"""Get all completed tasks with their timing information."""
|
|
936
|
+
return {task_id: task_info for task_id, task_info in self._tasks.items() if task_info.completed}
|
|
937
|
+
|
|
938
|
+
def get_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
939
|
+
"""Get all tasks (completed or active) for a specific category."""
|
|
940
|
+
return {
|
|
941
|
+
task_id: task_info
|
|
942
|
+
for task_id, task_info in self._tasks.items()
|
|
943
|
+
if task_info.category == category
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
def get_completed_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
947
|
+
"""Get all completed tasks for a specific category."""
|
|
948
|
+
return {
|
|
949
|
+
task_id: task_info
|
|
950
|
+
for task_id, task_info in self._tasks.items()
|
|
951
|
+
if task_info.category == category and task_info.completed
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
def _clear_all_tasks(self) -> None:
|
|
955
|
+
"""Clear all tasks and related data."""
|
|
956
|
+
self._tasks.clear()
|
|
957
|
+
self._highlighted_tasks.clear()
|
|
958
|
+
|
|
959
|
+
def set_process_start_time(self) -> None:
|
|
960
|
+
"""Set the overall process start time."""
|
|
961
|
+
self._process_start_time = time.time()
|
|
962
|
+
|
|
963
|
+
def set_process_end_time(self) -> None:
|
|
964
|
+
"""Set the overall process end time."""
|
|
965
|
+
self._process_end_time = time.time()
|
|
966
|
+
|
|
967
|
+
def get_total_duration(self) -> float:
|
|
968
|
+
"""Get the total duration from first task added to last task completed."""
|
|
969
|
+
if not self._tasks:
|
|
970
|
+
return 0.0
|
|
971
|
+
|
|
972
|
+
completed_tasks = self.get_completed_tasks()
|
|
973
|
+
if not completed_tasks:
|
|
974
|
+
return 0.0
|
|
975
|
+
|
|
976
|
+
start_times = [task.added_time for task in self._tasks.values()]
|
|
977
|
+
completion_times = [task.completed_time for task in completed_tasks.values() if task.completed_time > 0]
|
|
978
|
+
|
|
979
|
+
if not start_times or not completion_times:
|
|
980
|
+
return 0.0
|
|
981
|
+
|
|
982
|
+
earliest_start = min(start_times)
|
|
983
|
+
latest_completion = max(completion_times)
|
|
984
|
+
return latest_completion - earliest_start
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
class TaskProgress(_TaskStateMixin):
|
|
988
|
+
"""A progress component that uses Rich's Live system to provide proper two-line display.
|
|
989
|
+
|
|
990
|
+
This class provides:
|
|
991
|
+
- Main progress line with spinner and description
|
|
992
|
+
- Sub-status lines with hierarchical arrow indicators (➜)
|
|
993
|
+
- Proper error handling with success/failure messages
|
|
994
|
+
- Task-based progress tracking with context managers
|
|
995
|
+
- Highlighting of subtask text changes in yellow for 2 seconds when text differs
|
|
996
|
+
- Consistent task ordering with active tasks displayed above completed ones
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
def __init__(
|
|
1000
|
+
self,
|
|
1001
|
+
description: str = "",
|
|
1002
|
+
success_message: str = "",
|
|
1003
|
+
failure_message: str = "",
|
|
1004
|
+
leading_newline: bool = False,
|
|
1005
|
+
trailing_newline: bool = False,
|
|
1006
|
+
transient: bool = False,
|
|
1007
|
+
hide_on_completion: bool = False,
|
|
1008
|
+
show_duration_summary: bool = True,
|
|
1009
|
+
):
|
|
1010
|
+
# Public configuration
|
|
1011
|
+
self.description = description
|
|
1012
|
+
self.success_message = success_message
|
|
1013
|
+
self.failure_message = failure_message
|
|
1014
|
+
self.leading_newline = leading_newline
|
|
1015
|
+
self.trailing_newline = trailing_newline
|
|
1016
|
+
self.transient = transient
|
|
1017
|
+
self._init_task_state(
|
|
1018
|
+
hide_on_completion=hide_on_completion,
|
|
1019
|
+
show_duration_summary=show_duration_summary,
|
|
1020
|
+
)
|
|
1021
|
+
self.enable_highlighting = True
|
|
1022
|
+
|
|
1023
|
+
# Detect CI environment to avoid cursor control issues
|
|
1024
|
+
from ..environments import CIEnvironment
|
|
1025
|
+
self.is_ci = isinstance(runtime_env, CIEnvironment)
|
|
1026
|
+
self.is_jupyter = isinstance(runtime_env, JupyterEnvironment)
|
|
1027
|
+
|
|
1028
|
+
# Core components
|
|
1029
|
+
# In CI or Jupyter, avoid forcing terminal rendering to prevent duplicate outputs
|
|
1030
|
+
force_terminal = not self.is_ci and not self.is_jupyter
|
|
1031
|
+
force_jupyter = True if self.is_jupyter else None
|
|
1032
|
+
self.console = Console(
|
|
1033
|
+
force_terminal=force_terminal,
|
|
1034
|
+
force_jupyter=force_jupyter,
|
|
1035
|
+
)
|
|
1036
|
+
self.live = None
|
|
1037
|
+
self.main_completed = False
|
|
1038
|
+
self.main_failed = False
|
|
1039
|
+
|
|
1040
|
+
# Animation state
|
|
1041
|
+
self.spinner_index = 0
|
|
1042
|
+
|
|
1043
|
+
# Performance optimizations
|
|
1044
|
+
self._render_cache = None
|
|
1045
|
+
self._last_state_hash = None
|
|
1046
|
+
|
|
1047
|
+
self._spinner_thread = None
|
|
1048
|
+
|
|
1049
|
+
def _generate_task_id(self) -> str:
|
|
1050
|
+
"""Generate a unique task ID."""
|
|
1051
|
+
task_id = f"task_{self._next_task_id}"
|
|
1052
|
+
self._next_task_id += 1
|
|
1053
|
+
return task_id
|
|
1054
|
+
|
|
1055
|
+
def _compute_state_hash(self) -> int:
|
|
1056
|
+
"""Compute a simple hash of the current state for caching."""
|
|
1057
|
+
# Use a simple hash based on key state variables
|
|
1058
|
+
state_parts = [
|
|
1059
|
+
str(self.main_completed),
|
|
1060
|
+
str(self.main_failed),
|
|
1061
|
+
self.description,
|
|
1062
|
+
str(self.spinner_index),
|
|
1063
|
+
str(len(self._tasks)),
|
|
1064
|
+
str(len(self._highlighted_tasks)),
|
|
1065
|
+
]
|
|
1066
|
+
|
|
1067
|
+
# Add task states (only essential info for performance)
|
|
1068
|
+
for task_id, task_info in self._tasks.items():
|
|
1069
|
+
state_parts.append(f"{task_id}:{task_info.completed}:{task_info.description}")
|
|
1070
|
+
if task_id in self._highlighted_tasks:
|
|
1071
|
+
state_parts.append(f"highlight:{task_id}")
|
|
1072
|
+
|
|
1073
|
+
return hash(tuple(state_parts))
|
|
1074
|
+
|
|
1075
|
+
def _render_display(self):
|
|
1076
|
+
"""Render the current display state with caching optimization."""
|
|
1077
|
+
# Check if we need to re-render
|
|
1078
|
+
current_hash = self._compute_state_hash()
|
|
1079
|
+
if current_hash == self._last_state_hash and self._render_cache is not None:
|
|
1080
|
+
return self._render_cache
|
|
1081
|
+
|
|
1082
|
+
from rich.text import Text
|
|
1083
|
+
|
|
1084
|
+
# Build main task line
|
|
1085
|
+
if self.main_failed:
|
|
1086
|
+
# Split the description to style only the "Failed:" part in red
|
|
1087
|
+
if self.description.startswith("Failed:"):
|
|
1088
|
+
failed_part = "Failed:"
|
|
1089
|
+
rest_part = self.description[len("Failed:"):].lstrip()
|
|
1090
|
+
main_line = (Text(f"{FAIL_ICON} ", style="red") +
|
|
1091
|
+
Text(failed_part, style="red") +
|
|
1092
|
+
Text(f" {rest_part}", style="default"))
|
|
1093
|
+
else:
|
|
1094
|
+
# Fallback if description doesn't start with "Failed:"
|
|
1095
|
+
main_line = Text(f"{FAIL_ICON} ", style="red") + Text(self.description, style="red")
|
|
1096
|
+
elif self.main_completed:
|
|
1097
|
+
main_line = Text(f"{SUCCESS_ICON} ", style="green") + Text(self.description, style="green")
|
|
1098
|
+
else:
|
|
1099
|
+
spinner_text = SPINNER_FRAMES[self.spinner_index]
|
|
1100
|
+
main_line = Text(f"{spinner_text} ", style="magenta") + Text(self.description, style="magenta")
|
|
1101
|
+
|
|
1102
|
+
# Build subtask lines
|
|
1103
|
+
subtask_lines = self._render_subtask_lines()
|
|
1104
|
+
|
|
1105
|
+
# Combine all lines
|
|
1106
|
+
all_lines = [main_line] + subtask_lines
|
|
1107
|
+
|
|
1108
|
+
# Cache the result
|
|
1109
|
+
self._render_cache = Group(*all_lines)
|
|
1110
|
+
self._last_state_hash = current_hash
|
|
1111
|
+
|
|
1112
|
+
return self._render_cache
|
|
1113
|
+
|
|
1114
|
+
def _render_subtask_lines(self):
|
|
1115
|
+
"""Render all subtask lines efficiently."""
|
|
1116
|
+
from rich.text import Text
|
|
1117
|
+
|
|
1118
|
+
subtask_lines = []
|
|
1119
|
+
current_time = time.time()
|
|
1120
|
+
|
|
1121
|
+
# Separate incomplete and completed tasks
|
|
1122
|
+
incomplete_tasks = []
|
|
1123
|
+
completed_tasks = []
|
|
1124
|
+
|
|
1125
|
+
for task_id, task_info in self._tasks.items():
|
|
1126
|
+
# Skip hidden tasks
|
|
1127
|
+
if task_info.hidden:
|
|
1128
|
+
continue
|
|
1129
|
+
if task_info.completed:
|
|
1130
|
+
completed_tasks.append((task_id, task_info))
|
|
1131
|
+
else:
|
|
1132
|
+
incomplete_tasks.append((task_id, task_info))
|
|
1133
|
+
|
|
1134
|
+
# Render incomplete tasks first
|
|
1135
|
+
for task_id, task_info in incomplete_tasks:
|
|
1136
|
+
is_highlighted = (task_id in self._highlighted_tasks and
|
|
1137
|
+
current_time < self._highlighted_tasks[task_id])
|
|
1138
|
+
|
|
1139
|
+
style = "yellow" if is_highlighted else "white"
|
|
1140
|
+
line = Text(f" {ARROW} ", style=style) + Text(task_info.description, style=style)
|
|
1141
|
+
subtask_lines.append(line)
|
|
1142
|
+
|
|
1143
|
+
# Render completed tasks
|
|
1144
|
+
for task_id, task_info in completed_tasks:
|
|
1145
|
+
line = Text(f" {CHECK_MARK} ", style="green") + Text(task_info.description, style="green")
|
|
1146
|
+
subtask_lines.append(line)
|
|
1147
|
+
|
|
1148
|
+
return subtask_lines
|
|
1149
|
+
|
|
1150
|
+
def _advance_spinner(self):
|
|
1151
|
+
"""Advance the spinner animation."""
|
|
1152
|
+
self.spinner_index = (self.spinner_index + 1) % len(SPINNER_FRAMES)
|
|
1153
|
+
|
|
1154
|
+
def _invalidate_cache(self):
|
|
1155
|
+
"""Invalidate the render cache to force re-rendering."""
|
|
1156
|
+
self._last_state_hash = None
|
|
1157
|
+
self._render_cache = None
|
|
1158
|
+
|
|
1159
|
+
def _update_display(self):
|
|
1160
|
+
"""Update the display if live."""
|
|
1161
|
+
if self.live:
|
|
1162
|
+
self.live.update(self._render_display())
|
|
1163
|
+
|
|
1164
|
+
def _after_task_update(self) -> None:
|
|
1165
|
+
"""Refresh the live display when task state changes."""
|
|
1166
|
+
self._invalidate_cache()
|
|
1167
|
+
self._update_display()
|
|
1168
|
+
|
|
1169
|
+
def _clear_all_tasks(self) -> None:
|
|
1170
|
+
"""Clear tasks and refresh display."""
|
|
1171
|
+
super()._clear_all_tasks()
|
|
1172
|
+
self._after_task_update()
|
|
1173
|
+
|
|
1174
|
+
def generate_summary(self, categories: dict[str, str] | None = None) -> str:
|
|
1175
|
+
"""Generate a summary of completed tasks by category."""
|
|
1176
|
+
if categories is None:
|
|
1177
|
+
categories = DEFAULT_SUMMARY_CATEGORIES
|
|
1178
|
+
|
|
1179
|
+
category_durations: dict[str, float] = {}
|
|
1180
|
+
for category_name in categories:
|
|
1181
|
+
tasks = self.get_completed_tasks_by_category(category_name)
|
|
1182
|
+
category_durations[category_name] = _calculate_category_duration(category_name, tasks)
|
|
1183
|
+
|
|
1184
|
+
if not any(category_durations.values()):
|
|
1185
|
+
return ""
|
|
1186
|
+
|
|
1187
|
+
total_duration = self.get_total_duration()
|
|
1188
|
+
|
|
1189
|
+
try:
|
|
1190
|
+
from rich.console import Console
|
|
1191
|
+
from rich.table import Table
|
|
1192
|
+
|
|
1193
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
1194
|
+
table.add_column("Operation", style="white")
|
|
1195
|
+
table.add_column("Duration", style="green", justify="right")
|
|
1196
|
+
|
|
1197
|
+
if total_duration > 0:
|
|
1198
|
+
table.add_row(
|
|
1199
|
+
INITIALIZATION_COMPLETED_TEXT,
|
|
1200
|
+
format_duration(total_duration)
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
for category_name, display_name in categories.items():
|
|
1204
|
+
duration = category_durations[category_name]
|
|
1205
|
+
if duration > MIN_CATEGORY_DURATION_SECONDS:
|
|
1206
|
+
table.add_row(
|
|
1207
|
+
f" {ARROW} {display_name}",
|
|
1208
|
+
format_duration(duration)
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
table.add_row("", "")
|
|
1212
|
+
|
|
1213
|
+
console = Console()
|
|
1214
|
+
with console.capture() as capture:
|
|
1215
|
+
console.print(table)
|
|
1216
|
+
return capture.get()
|
|
1217
|
+
|
|
1218
|
+
except ImportError:
|
|
1219
|
+
lines: list[str] = []
|
|
1220
|
+
if total_duration > 0:
|
|
1221
|
+
lines.append(f"{INITIALIZATION_COMPLETED_TEXT} {format_duration(total_duration)}")
|
|
1222
|
+
|
|
1223
|
+
for category_name, display_name in categories.items():
|
|
1224
|
+
duration = category_durations[category_name]
|
|
1225
|
+
if duration > MIN_CATEGORY_DURATION_SECONDS:
|
|
1226
|
+
lines.append(f" {ARROW} {display_name} {format_duration(duration)}")
|
|
1227
|
+
|
|
1228
|
+
if lines:
|
|
1229
|
+
lines.append("")
|
|
1230
|
+
|
|
1231
|
+
return "\n".join(lines)
|
|
1232
|
+
|
|
1233
|
+
def __enter__(self):
|
|
1234
|
+
if self.leading_newline:
|
|
1235
|
+
print()
|
|
1236
|
+
|
|
1237
|
+
# Start the live display
|
|
1238
|
+
from rich.live import Live
|
|
1239
|
+
self.live = Live(self._render_display(), console=self.console, refresh_per_second=LIVE_REFRESH_RATE)
|
|
1240
|
+
self.live.start()
|
|
1241
|
+
|
|
1242
|
+
# Start spinner animation
|
|
1243
|
+
self._start_spinner()
|
|
1244
|
+
|
|
1245
|
+
return self
|
|
1246
|
+
|
|
1247
|
+
def _start_spinner(self):
|
|
1248
|
+
"""Start the spinner animation thread."""
|
|
1249
|
+
def spinner_animation():
|
|
1250
|
+
while self.live and not self.main_completed and not self.main_failed:
|
|
1251
|
+
time.sleep(SPINNER_UPDATE_INTERVAL)
|
|
1252
|
+
if self.live:
|
|
1253
|
+
self._advance_spinner()
|
|
1254
|
+
self.live.update(self._render_display())
|
|
1255
|
+
|
|
1256
|
+
self._spinner_thread = threading.Thread(target=spinner_animation, daemon=True)
|
|
1257
|
+
self._spinner_thread.start()
|
|
1258
|
+
|
|
1259
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1260
|
+
# Stop timer manager
|
|
1261
|
+
self._timer_manager.stop()
|
|
1262
|
+
|
|
1263
|
+
if exc_type is not None:
|
|
1264
|
+
# Exception occurred - show failure message
|
|
1265
|
+
self._handle_failure(exc_val)
|
|
1266
|
+
return False # Don't suppress the exception
|
|
1267
|
+
else:
|
|
1268
|
+
# Success - show completion
|
|
1269
|
+
self._handle_success()
|
|
1270
|
+
|
|
1271
|
+
return True
|
|
1272
|
+
|
|
1273
|
+
def _handle_failure(self, exc_val):
|
|
1274
|
+
"""Handle failure case in context manager exit."""
|
|
1275
|
+
# Clear all tasks and update main task to show failure state
|
|
1276
|
+
self._clear_all_tasks()
|
|
1277
|
+
self.main_failed = True
|
|
1278
|
+
|
|
1279
|
+
# Update main task description to show failure message
|
|
1280
|
+
if self.failure_message:
|
|
1281
|
+
self.description = self.failure_message
|
|
1282
|
+
else:
|
|
1283
|
+
self.description = f"Failed: {exc_val}"
|
|
1284
|
+
|
|
1285
|
+
# Update the display to show the failure state before stopping
|
|
1286
|
+
if self.live:
|
|
1287
|
+
self.live.update(self._render_display())
|
|
1288
|
+
# Brief pause to show the failure state
|
|
1289
|
+
time.sleep(BRIEF_PAUSE)
|
|
1290
|
+
|
|
1291
|
+
if self.trailing_newline:
|
|
1292
|
+
print()
|
|
1293
|
+
self._cleanup()
|
|
1294
|
+
|
|
1295
|
+
def _handle_success(self):
|
|
1296
|
+
"""Handle success case in context manager exit."""
|
|
1297
|
+
self.main_completed = True
|
|
1298
|
+
|
|
1299
|
+
# Generate summary before clearing tasks (so we have the timing data)
|
|
1300
|
+
# Only generate if show_duration_summary flag is True
|
|
1301
|
+
summary = self.generate_summary() if self.show_duration_summary else ""
|
|
1302
|
+
|
|
1303
|
+
self._clear_all_tasks()
|
|
1304
|
+
|
|
1305
|
+
# Update main task description to show success message
|
|
1306
|
+
if self.success_message:
|
|
1307
|
+
self.description = self.success_message
|
|
1308
|
+
|
|
1309
|
+
# Show success message in Rich Live display
|
|
1310
|
+
if self.live:
|
|
1311
|
+
self.live.update(self._render_display())
|
|
1312
|
+
# Stop the live display
|
|
1313
|
+
self.live.stop()
|
|
1314
|
+
|
|
1315
|
+
# Print summary if available
|
|
1316
|
+
if summary:
|
|
1317
|
+
print() # Blank line for separation
|
|
1318
|
+
print(summary, end="") # summary already has trailing newline
|
|
1319
|
+
print() # Add extra blank line after summary
|
|
1320
|
+
|
|
1321
|
+
if self.trailing_newline:
|
|
1322
|
+
print()
|
|
1323
|
+
self._cleanup()
|
|
1324
|
+
|
|
1325
|
+
def _cleanup(self):
|
|
1326
|
+
"""Clean up resources."""
|
|
1327
|
+
if self.live:
|
|
1328
|
+
# Stop the live display first
|
|
1329
|
+
self.live.stop()
|
|
1330
|
+
# Clear the current line using ANSI escape sequence (only in TTY, not in CI)
|
|
1331
|
+
if not self.is_ci and sys.stdout.isatty():
|
|
1332
|
+
print("\r\033[K", end="", flush=True)
|
|
1333
|
+
|
|
1334
|
+
def _calculate_category_duration(category_name: str, tasks: Dict[str, TaskInfo]) -> float:
|
|
1335
|
+
"""Calculate duration for a category based on task type (parallel vs sequential)."""
|
|
1336
|
+
if not tasks:
|
|
1337
|
+
return 0.0
|
|
1338
|
+
|
|
1339
|
+
if category_name in PARALLEL_TASK_CATEGORIES:
|
|
1340
|
+
# For parallel tasks, use time span (max completion - min start)
|
|
1341
|
+
category_start_times = [task_info.added_time for task_info in tasks.values()]
|
|
1342
|
+
category_completion_times = [
|
|
1343
|
+
task_info.completed_time for task_info in tasks.values()
|
|
1344
|
+
if task_info.completed_time > 0
|
|
1345
|
+
]
|
|
1346
|
+
if category_start_times and category_completion_times:
|
|
1347
|
+
return max(category_completion_times) - min(category_start_times)
|
|
1348
|
+
else:
|
|
1349
|
+
return 0.0
|
|
1350
|
+
else:
|
|
1351
|
+
# For sequential tasks, sum individual durations
|
|
1352
|
+
return sum(task_info.get_duration() for task_info in tasks.values())
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def create_progress(description: str = "", success_message: str = "", failure_message: str = "",
|
|
1356
|
+
leading_newline: bool = False, trailing_newline: bool = False, show_duration_summary: bool = True):
|
|
1357
|
+
"""Factory function to create the appropriate progress component based on environment.
|
|
1358
|
+
|
|
1359
|
+
Automatically detects if we're in a notebook environment (Snowflake, Jupyter, etc.)
|
|
1360
|
+
and returns the appropriate progress class.
|
|
1361
|
+
"""
|
|
1362
|
+
from ..environments import runtime_env, SnowbookEnvironment, NotebookRuntimeEnvironment
|
|
1363
|
+
|
|
1364
|
+
if isinstance(runtime_env, (SnowbookEnvironment, NotebookRuntimeEnvironment)):
|
|
1365
|
+
# Use NotebookTaskProgress for Snowflake and Jupyter notebooks
|
|
1366
|
+
return NotebookTaskProgress(
|
|
1367
|
+
description=description,
|
|
1368
|
+
success_message=success_message,
|
|
1369
|
+
failure_message=failure_message,
|
|
1370
|
+
leading_newline=leading_newline,
|
|
1371
|
+
trailing_newline=trailing_newline,
|
|
1372
|
+
show_duration_summary=show_duration_summary
|
|
1373
|
+
)
|
|
1374
|
+
else:
|
|
1375
|
+
# Use TaskProgress for other environments (terminal, CI, etc.)
|
|
1376
|
+
return TaskProgress(
|
|
1377
|
+
description=description,
|
|
1378
|
+
success_message=success_message,
|
|
1379
|
+
failure_message=failure_message,
|
|
1380
|
+
leading_newline=leading_newline,
|
|
1381
|
+
trailing_newline=trailing_newline,
|
|
1382
|
+
show_duration_summary=show_duration_summary
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
class SubTaskContext:
|
|
1387
|
+
"""Context manager for individual subtasks within a TaskProgress."""
|
|
1388
|
+
|
|
1389
|
+
def __init__(self, task_progress: TaskProgress, description: str, task_id: str | None = None):
|
|
1390
|
+
self.task_progress = task_progress
|
|
1391
|
+
self.description = description
|
|
1392
|
+
self.task_id = task_id
|
|
1393
|
+
self._task_id = None
|
|
1394
|
+
|
|
1395
|
+
def __enter__(self):
|
|
1396
|
+
# Add the subtask and get its ID
|
|
1397
|
+
self._task_id = self.task_progress.add_sub_task(self.description, self.task_id)
|
|
1398
|
+
return self._task_id
|
|
1399
|
+
|
|
1400
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1401
|
+
if self._task_id and exc_type is None:
|
|
1402
|
+
# Success - complete the subtask automatically when context exits
|
|
1403
|
+
self.task_progress.complete_sub_task(self._task_id)
|
|
1404
|
+
# If there was an exception, leave the subtask as-is for debugging
|
|
1405
|
+
return False # Don't suppress exceptions
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
class NotebookTaskProgress(_TaskStateMixin):
|
|
1409
|
+
"""A progress component specifically designed for notebook environments like Snowflake.
|
|
1410
|
+
|
|
1411
|
+
This class copies the EXACT working Spinner code and adapts it for notebook use.
|
|
1412
|
+
"""
|
|
1413
|
+
|
|
1414
|
+
def __init__(
|
|
1415
|
+
self,
|
|
1416
|
+
description: str = "",
|
|
1417
|
+
success_message: str = "",
|
|
1418
|
+
failure_message: str = "",
|
|
1419
|
+
leading_newline: bool = False,
|
|
1420
|
+
trailing_newline: bool = False,
|
|
1421
|
+
show_duration_summary: bool = True,
|
|
1422
|
+
):
|
|
1423
|
+
self.description = description
|
|
1424
|
+
self.success_message = success_message
|
|
1425
|
+
self.failure_message = failure_message
|
|
1426
|
+
self.leading_newline = leading_newline
|
|
1427
|
+
self.trailing_newline = trailing_newline
|
|
1428
|
+
self._init_task_state(show_duration_summary=show_duration_summary)
|
|
1429
|
+
self.enable_highlighting = False
|
|
1430
|
+
|
|
1431
|
+
self.spinner_generator = itertools.cycle(SPINNER_FRAMES)
|
|
1432
|
+
|
|
1433
|
+
# Environment detection for notebook environments only
|
|
1434
|
+
self.is_snowflake_notebook = isinstance(runtime_env, SnowbookEnvironment)
|
|
1435
|
+
self.is_hex = isinstance(runtime_env, HexEnvironment)
|
|
1436
|
+
self.is_jupyter = isinstance(runtime_env, JupyterEnvironment)
|
|
1437
|
+
self.in_notebook = isinstance(runtime_env, NotebookRuntimeEnvironment)
|
|
1438
|
+
|
|
1439
|
+
self._set_delay(None)
|
|
1440
|
+
|
|
1441
|
+
self.last_message = ""
|
|
1442
|
+
self.display = None
|
|
1443
|
+
self._update_lock = threading.Lock()
|
|
1444
|
+
|
|
1445
|
+
# Add sub-task support for TaskProgress compatibility
|
|
1446
|
+
self.spinner_thread = None
|
|
1447
|
+
self._current_subtask = ""
|
|
1448
|
+
self.busy = False # Initialize busy state
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _generate_task_id(self) -> str:
|
|
1452
|
+
"""Generate a unique task ID."""
|
|
1453
|
+
task_id = f"task_{self._next_task_id}"
|
|
1454
|
+
self._next_task_id += 1
|
|
1455
|
+
return task_id
|
|
1456
|
+
|
|
1457
|
+
def _set_delay(self, delay: float|int|None) -> None:
|
|
1458
|
+
"""Set appropriate delay for notebook environments."""
|
|
1459
|
+
# If delay value is provided, validate and use it
|
|
1460
|
+
if delay:
|
|
1461
|
+
if isinstance(delay, (int, float)) and delay > 0:
|
|
1462
|
+
self.delay = float(delay)
|
|
1463
|
+
return
|
|
1464
|
+
else:
|
|
1465
|
+
raise ValueError(f"Invalid delay value: {delay}")
|
|
1466
|
+
# Simple delay for notebooks - no complex environment detection needed
|
|
1467
|
+
elif self.in_notebook or self.is_snowflake_notebook or self.is_jupyter or self.is_hex:
|
|
1468
|
+
self.delay = 0.2 # Simple, consistent delay for all notebook environments
|
|
1469
|
+
else:
|
|
1470
|
+
# Disable animation for non-interactive environments
|
|
1471
|
+
self.delay = 0
|
|
1472
|
+
|
|
1473
|
+
def get_message(self, starting=False):
|
|
1474
|
+
"""Get the current message with spinner - notebook environments only."""
|
|
1475
|
+
# For notebook environments, use a reasonable default width
|
|
1476
|
+
max_width = DEFAULT_TERMINAL_WIDTH
|
|
1477
|
+
try:
|
|
1478
|
+
max_width = shutil.get_terminal_size().columns
|
|
1479
|
+
except (OSError, AttributeError):
|
|
1480
|
+
pass # Use default width if terminal size can't be determined
|
|
1481
|
+
|
|
1482
|
+
spinner = "⏳⏳⏳⏳" if starting else next(self.spinner_generator)
|
|
1483
|
+
|
|
1484
|
+
# If there's an active subtask, show ONLY the subtask
|
|
1485
|
+
if hasattr(self, '_current_subtask') and self._current_subtask:
|
|
1486
|
+
full_message = f"{spinner} {self._current_subtask}"
|
|
1487
|
+
else:
|
|
1488
|
+
# Otherwise show the main task with subtask count if any
|
|
1489
|
+
if len(self._tasks) > 0:
|
|
1490
|
+
full_message = f"{spinner} {self.description} ({len(self._tasks)} active)"
|
|
1491
|
+
else:
|
|
1492
|
+
full_message = f"{spinner} {self.description}"
|
|
1493
|
+
|
|
1494
|
+
if len(full_message) > max_width:
|
|
1495
|
+
return full_message[:max_width - 3] + "..."
|
|
1496
|
+
else:
|
|
1497
|
+
return full_message
|
|
1498
|
+
|
|
1499
|
+
def update(self, message:str|None=None, file:TextIO|None=None, starting=False):
|
|
1500
|
+
"""Update the display - notebook environments only."""
|
|
1501
|
+
# Use lock to prevent race conditions between spinner thread and main thread
|
|
1502
|
+
with self._update_lock:
|
|
1503
|
+
if self.is_jupyter:
|
|
1504
|
+
_, display_fn = _load_ipython_display()
|
|
1505
|
+
|
|
1506
|
+
if message is None:
|
|
1507
|
+
lines = self._build_jupyter_lines(starting=starting)
|
|
1508
|
+
elif message == "":
|
|
1509
|
+
lines = []
|
|
1510
|
+
else:
|
|
1511
|
+
lines = [message]
|
|
1512
|
+
|
|
1513
|
+
rendered = "\n".join(lines)
|
|
1514
|
+
content = {"text/plain": rendered}
|
|
1515
|
+
if self.display is not None:
|
|
1516
|
+
self.display.update(content, raw=True)
|
|
1517
|
+
else:
|
|
1518
|
+
self.display = display_fn(content, display_id=True, raw=True)
|
|
1519
|
+
return
|
|
1520
|
+
|
|
1521
|
+
if message is None:
|
|
1522
|
+
message = self.get_message(starting=starting)
|
|
1523
|
+
|
|
1524
|
+
rich_string = rich_str(message)
|
|
1525
|
+
|
|
1526
|
+
def width(word):
|
|
1527
|
+
return sum(wcwidth(c) for c in word)
|
|
1528
|
+
|
|
1529
|
+
diff = width(self.last_message) - width(rich_string)
|
|
1530
|
+
|
|
1531
|
+
sys.stdout.write("\r") # Move to beginning
|
|
1532
|
+
sys.stdout.write(" " * DEFAULT_TERMINAL_WIDTH) # Clear with spaces
|
|
1533
|
+
sys.stdout.write("\r") # Move back to beginning
|
|
1534
|
+
|
|
1535
|
+
sys.stdout.write(message + (" " * diff)) # Write text directly
|
|
1536
|
+
if self.in_notebook:
|
|
1537
|
+
sys.stdout.flush() # Force output
|
|
1538
|
+
self.last_message = rich_string
|
|
1539
|
+
|
|
1540
|
+
def _build_jupyter_lines(self, starting: bool) -> list[str]:
|
|
1541
|
+
"""Compose the main status and subtasks for Jupyter display."""
|
|
1542
|
+
if self.busy or starting:
|
|
1543
|
+
spinner = SPINNER_FRAMES[0] if starting else next(self.spinner_generator)
|
|
1544
|
+
main_line = f"{spinner} {self.description}"
|
|
1545
|
+
else:
|
|
1546
|
+
main_text = self.success_message or self.description
|
|
1547
|
+
main_line = f"{SUCCESS_ICON} {main_text}"
|
|
1548
|
+
|
|
1549
|
+
visible_tasks = self._collect_visible_tasks()
|
|
1550
|
+
lines = [main_line]
|
|
1551
|
+
|
|
1552
|
+
for marker, tasks in (
|
|
1553
|
+
(ARROW, visible_tasks["incomplete"]),
|
|
1554
|
+
(CHECK_MARK, visible_tasks["completed"]),
|
|
1555
|
+
):
|
|
1556
|
+
for task_info in tasks:
|
|
1557
|
+
lines.append(f" {marker} {task_info.description}")
|
|
1558
|
+
|
|
1559
|
+
return lines
|
|
1560
|
+
|
|
1561
|
+
def _collect_visible_tasks(self) -> dict[str, list["TaskInfo"]]:
|
|
1562
|
+
"""Separate visible tasks into incomplete and completed lists."""
|
|
1563
|
+
incomplete: list["TaskInfo"] = []
|
|
1564
|
+
completed: list["TaskInfo"] = []
|
|
1565
|
+
|
|
1566
|
+
for task_info in self._tasks.values():
|
|
1567
|
+
if task_info.hidden:
|
|
1568
|
+
continue
|
|
1569
|
+
if task_info.completed:
|
|
1570
|
+
completed.append(task_info)
|
|
1571
|
+
else:
|
|
1572
|
+
incomplete.append(task_info)
|
|
1573
|
+
|
|
1574
|
+
return {"incomplete": incomplete, "completed": completed}
|
|
1575
|
+
|
|
1576
|
+
def reset_cursor(self):
|
|
1577
|
+
"""Reset cursor to beginning of line - notebook environments only."""
|
|
1578
|
+
# For notebook environments, use simple carriage return
|
|
1579
|
+
if not self.is_jupyter:
|
|
1580
|
+
sys.stdout.write("\r")
|
|
1581
|
+
|
|
1582
|
+
def spinner_task(self):
|
|
1583
|
+
"""Spinner animation task."""
|
|
1584
|
+
while self.busy and self.delay:
|
|
1585
|
+
self.update()
|
|
1586
|
+
time.sleep(self.delay) #type: ignore[union-attr] | we only call spinner_task if delay is not None anyway
|
|
1587
|
+
self.reset_cursor()
|
|
1588
|
+
|
|
1589
|
+
def _update_subtask_display(self, subtask_text: str):
|
|
1590
|
+
"""Update sub-task display - shows ONLY the subtask text."""
|
|
1591
|
+
# Store the current display state
|
|
1592
|
+
if not hasattr(self, '_current_display'):
|
|
1593
|
+
self._current_display = ""
|
|
1594
|
+
|
|
1595
|
+
# Only update if the display has changed
|
|
1596
|
+
if self._current_display != subtask_text:
|
|
1597
|
+
# Store the subtask text for the spinner to use
|
|
1598
|
+
self._current_subtask = subtask_text
|
|
1599
|
+
self._current_display = subtask_text
|
|
1600
|
+
# The spinner will now show the subtask instead of main task
|
|
1601
|
+
|
|
1602
|
+
def add_sub_task(self, description: str, task_id: str | None = None, category: str = "general") -> str:
|
|
1603
|
+
task_id = super().add_sub_task(description, task_id, category)
|
|
1604
|
+
# Update spinner display with the active subtask
|
|
1605
|
+
if task_id in self._tasks:
|
|
1606
|
+
self._update_subtask_display(self._tasks[task_id].description)
|
|
1607
|
+
return task_id
|
|
1608
|
+
|
|
1609
|
+
def update_sub_task(self, task_id: str, description: str) -> None:
|
|
1610
|
+
super().update_sub_task(task_id, description)
|
|
1611
|
+
if task_id in self._tasks:
|
|
1612
|
+
self._update_subtask_display(description)
|
|
1613
|
+
|
|
1614
|
+
def complete_sub_task(self, task_id: str, record_time: bool = True) -> None:
|
|
1615
|
+
super().complete_sub_task(task_id, record_time=record_time)
|
|
1616
|
+
# Clear the subtask display when completed
|
|
1617
|
+
self._current_subtask = ""
|
|
1618
|
+
self._current_display = ""
|
|
1619
|
+
|
|
1620
|
+
def remove_sub_task(self, task_id: str, animate: bool = True) -> None:
|
|
1621
|
+
"""Remove a sub-task by ID."""
|
|
1622
|
+
task_description: str | None = None
|
|
1623
|
+
if task_id in self._tasks:
|
|
1624
|
+
task_description = self._tasks[task_id].description
|
|
1625
|
+
|
|
1626
|
+
# Notebook display should drop the subtask immediately to clear the UI.
|
|
1627
|
+
super().remove_sub_task(task_id, animate=False)
|
|
1628
|
+
|
|
1629
|
+
if task_description and getattr(self, "_current_subtask", "") == task_description:
|
|
1630
|
+
self._current_subtask = ""
|
|
1631
|
+
self._current_display = ""
|
|
1632
|
+
# The spinner will now show the main task again
|
|
1633
|
+
|
|
1634
|
+
def update_sub_status(self, sub_status: str):
|
|
1635
|
+
"""Legacy method for backward compatibility - creates/updates a default sub-task."""
|
|
1636
|
+
super().update_sub_status(sub_status)
|
|
1637
|
+
|
|
1638
|
+
def update_main_status(self, message: str):
|
|
1639
|
+
"""Update the main status line with real-time updates."""
|
|
1640
|
+
super().update_main_status(message)
|
|
1641
|
+
self._current_subtask = ""
|
|
1642
|
+
self._current_display = ""
|
|
1643
|
+
# The spinner will now show the updated main task
|
|
1644
|
+
|
|
1645
|
+
def update_messages(self, updater: dict[str, str]):
|
|
1646
|
+
"""Update both main message and sub-status if provided."""
|
|
1647
|
+
super().update_messages(updater)
|
|
1648
|
+
|
|
1649
|
+
def _clear_all_tasks(self) -> None:
|
|
1650
|
+
super()._clear_all_tasks()
|
|
1651
|
+
self._current_subtask = ""
|
|
1652
|
+
self._current_display = ""
|
|
1653
|
+
|
|
1654
|
+
def __enter__(self):
|
|
1655
|
+
# Skip leading newline for Jupyter - it interferes with IPython display
|
|
1656
|
+
if self.leading_newline and not self.is_jupyter:
|
|
1657
|
+
rich.print()
|
|
1658
|
+
self.update(starting=True)
|
|
1659
|
+
# return control to the event loop briefly so stdout can be sure to flush:
|
|
1660
|
+
if self.delay:
|
|
1661
|
+
time.sleep(INITIAL_DISPLAY_DELAY)
|
|
1662
|
+
self.reset_cursor()
|
|
1663
|
+
if not self.delay:
|
|
1664
|
+
return self
|
|
1665
|
+
self.busy = True
|
|
1666
|
+
threading.Thread(target=self.spinner_task, daemon=True).start()
|
|
1667
|
+
return self
|
|
1668
|
+
|
|
1669
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1670
|
+
self.busy = False
|
|
1671
|
+
if exc_type is not None:
|
|
1672
|
+
if self.failure_message is not None:
|
|
1673
|
+
self.update(f"{self.failure_message} {exc_val}", file=sys.stderr)
|
|
1674
|
+
# For non-Jupyter, add newline to ensure proper formatting
|
|
1675
|
+
# For Jupyter, IPython display handles formatting
|
|
1676
|
+
if not self.is_jupyter:
|
|
1677
|
+
rich.print(file=sys.stderr)
|
|
1678
|
+
return True
|
|
1679
|
+
return False
|
|
1680
|
+
if self.delay: # will be None for non-interactive environments
|
|
1681
|
+
time.sleep(self.delay)
|
|
1682
|
+
|
|
1683
|
+
# Generate summary BEFORE clearing the spinner line (so we have timing data)
|
|
1684
|
+
# Only generate if show_duration_summary flag is True
|
|
1685
|
+
summary = self.generate_summary() if self.show_duration_summary else ""
|
|
1686
|
+
|
|
1687
|
+
# Clear the spinner line completely
|
|
1688
|
+
self._clear_spinner_line()
|
|
1689
|
+
|
|
1690
|
+
final_message: str | None = None
|
|
1691
|
+
if self.success_message:
|
|
1692
|
+
final_message = f"{SUCCESS_ICON} {self.success_message}"
|
|
1693
|
+
elif summary:
|
|
1694
|
+
final_message = f"{SUCCESS_ICON} Done"
|
|
1695
|
+
|
|
1696
|
+
if final_message:
|
|
1697
|
+
if self.is_jupyter:
|
|
1698
|
+
if self.display is not None:
|
|
1699
|
+
self.display.update({"text/plain": final_message}, raw=True)
|
|
1700
|
+
else:
|
|
1701
|
+
_, display_fn = _load_ipython_display()
|
|
1702
|
+
self.display = display_fn({"text/plain": final_message}, display_id=True, raw=True)
|
|
1703
|
+
else:
|
|
1704
|
+
print(final_message)
|
|
1705
|
+
elif self.success_message == "":
|
|
1706
|
+
# When there's no success message, clear the display for notebooks
|
|
1707
|
+
# The summary will be printed below if available
|
|
1708
|
+
if self.is_jupyter:
|
|
1709
|
+
self.update("")
|
|
1710
|
+
# For non-Jupyter notebooks, _clear_spinner_line() already handled it
|
|
1711
|
+
|
|
1712
|
+
# Print summary if there are completed tasks
|
|
1713
|
+
if summary:
|
|
1714
|
+
if self.is_jupyter:
|
|
1715
|
+
# Use IPython display to avoid blank stdout lines in notebooks
|
|
1716
|
+
_, display_fn = _load_ipython_display()
|
|
1717
|
+
display_fn({"text/plain": summary.strip()}, raw=True)
|
|
1718
|
+
else:
|
|
1719
|
+
print()
|
|
1720
|
+
print(summary.strip()) # Summary includes visual separator line
|
|
1721
|
+
|
|
1722
|
+
# Skip trailing newline for Jupyter - it interferes with IPython display
|
|
1723
|
+
if self.trailing_newline and not self.is_jupyter:
|
|
1724
|
+
rich.print()
|
|
1725
|
+
return True
|
|
1726
|
+
|
|
1727
|
+
def _clear_spinner_line(self):
|
|
1728
|
+
"""Clear the current spinner line completely."""
|
|
1729
|
+
# Skip clearing for Jupyter notebooks - IPython display handles it
|
|
1730
|
+
if self.is_jupyter:
|
|
1731
|
+
return
|
|
1732
|
+
|
|
1733
|
+
# Write enough spaces to clear any content, then move to start of line
|
|
1734
|
+
terminal_width = DEFAULT_TERMINAL_WIDTH
|
|
1735
|
+
try:
|
|
1736
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
1737
|
+
except (OSError, AttributeError):
|
|
1738
|
+
pass
|
|
1739
|
+
|
|
1740
|
+
# Clear with spaces, carriage return, and newline to ensure we're on a fresh line
|
|
1741
|
+
sys.stdout.write("\r" + " " * terminal_width + "\r\n")
|
|
1742
|
+
sys.stdout.flush()
|
|
1743
|
+
|
|
1744
|
+
def set_process_start_time(self) -> None:
|
|
1745
|
+
"""Set the overall process start time."""
|
|
1746
|
+
self._process_start_time = time.time()
|
|
1747
|
+
|
|
1748
|
+
def set_process_end_time(self) -> None:
|
|
1749
|
+
"""Set the overall process end time."""
|
|
1750
|
+
self._process_end_time = time.time()
|
|
1751
|
+
|
|
1752
|
+
def get_total_duration(self) -> float:
|
|
1753
|
+
"""Get the total duration from first task added to last task completed."""
|
|
1754
|
+
if not self._tasks:
|
|
1755
|
+
return 0.0
|
|
1756
|
+
|
|
1757
|
+
completed_tasks = self.get_completed_tasks()
|
|
1758
|
+
if not completed_tasks:
|
|
1759
|
+
return 0.0
|
|
1760
|
+
|
|
1761
|
+
# Find earliest start time and latest completion time
|
|
1762
|
+
start_times = [task.added_time for task in self._tasks.values()]
|
|
1763
|
+
completion_times = [task.completed_time for task in completed_tasks.values() if task.completed_time > 0]
|
|
1764
|
+
|
|
1765
|
+
if not start_times or not completion_times:
|
|
1766
|
+
return 0.0
|
|
1767
|
+
|
|
1768
|
+
earliest_start = min(start_times)
|
|
1769
|
+
latest_completion = max(completion_times)
|
|
1770
|
+
|
|
1771
|
+
return latest_completion - earliest_start
|
|
1772
|
+
|
|
1773
|
+
def generate_summary(self, categories: dict[str, str] | None = None) -> str:
|
|
1774
|
+
"""Generate a summary of completed tasks by category."""
|
|
1775
|
+
if categories is None:
|
|
1776
|
+
categories = DEFAULT_SUMMARY_CATEGORIES
|
|
1777
|
+
|
|
1778
|
+
# Get completed tasks by category and calculate durations
|
|
1779
|
+
category_durations = {}
|
|
1780
|
+
for category_name in categories:
|
|
1781
|
+
tasks = self.get_completed_tasks_by_category(category_name)
|
|
1782
|
+
category_durations[category_name] = _calculate_category_duration(category_name, tasks)
|
|
1783
|
+
|
|
1784
|
+
# If there's nothing meaningful to show, return empty string
|
|
1785
|
+
if not any(category_durations.values()):
|
|
1786
|
+
return ""
|
|
1787
|
+
|
|
1788
|
+
# Generate summary lines with proper alignment
|
|
1789
|
+
summary_lines = []
|
|
1790
|
+
label_width = 30 # Width for category labels
|
|
1791
|
+
time_width = 10 # Width for time column (right-aligned)
|
|
1792
|
+
|
|
1793
|
+
# Add total time FIRST (at the top) - align with arrow lines
|
|
1794
|
+
total_duration = self.get_total_duration()
|
|
1795
|
+
if total_duration > 0:
|
|
1796
|
+
formatted_total = format_duration(total_duration)
|
|
1797
|
+
# Use the same format as arrow lines but with a different prefix
|
|
1798
|
+
# This ensures perfect alignment with the time column
|
|
1799
|
+
summary_lines.append(f" {INITIALIZATION_COMPLETED_TEXT:<{label_width-1}} {formatted_total:>{time_width}}")
|
|
1800
|
+
|
|
1801
|
+
# Add category breakdown
|
|
1802
|
+
category_lines = []
|
|
1803
|
+
for category_name, display_name in categories.items():
|
|
1804
|
+
duration = category_durations[category_name]
|
|
1805
|
+
if duration > MIN_CATEGORY_DURATION_SECONDS: # Only show significant durations
|
|
1806
|
+
formatted_duration = format_duration(duration)
|
|
1807
|
+
# Use arrow for visual consistency with right-aligned time
|
|
1808
|
+
category_lines.append(f" {ARROW} {display_name:<{label_width-4}} {formatted_duration:>{time_width}}")
|
|
1809
|
+
|
|
1810
|
+
# Only add category lines if there are any
|
|
1811
|
+
if category_lines:
|
|
1812
|
+
summary_lines.extend(category_lines)
|
|
1813
|
+
|
|
1814
|
+
# Add a visual separator line for Snowflake notebook environment
|
|
1815
|
+
summary_lines.append("─" * SEPARATOR_WIDTH)
|
|
1816
|
+
|
|
1817
|
+
return "\n".join(summary_lines) + "\n"
|
|
1818
|
+
|
|
1819
|
+
def get_completed_tasks(self) -> dict[str, TaskInfo]:
|
|
1820
|
+
"""Get all completed tasks with their timing information."""
|
|
1821
|
+
return {task_id: task_info for task_id, task_info in self._tasks.items() if task_info.completed}
|
|
1822
|
+
|
|
1823
|
+
def get_completed_tasks_by_category(self, category: str) -> dict[str, TaskInfo]:
|
|
1824
|
+
"""Get all completed tasks for a specific category."""
|
|
1825
|
+
return {task_id: task_info for task_id, task_info in self._tasks.items()
|
|
1826
|
+
if task_info.category == category and task_info.completed}
|