mycli 2.16.0__tar.gz → 2.17.0__tar.gz
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.
- {mycli-2.16.0/mycli.egg-info → mycli-2.17.0}/PKG-INFO +5 -6
- {mycli-2.16.0 → mycli-2.17.0}/changelog.md +19 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/client_commands.py +18 -1
- {mycli-2.16.0 → mycli-2.17.0}/mycli/client_query.py +1 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/completion_refresher.py +1 -1
- {mycli-2.16.0 → mycli-2.17.0}/mycli/lexer.py +1 -3
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main_modes/repl.py +11 -3
- {mycli-2.16.0 → mycli-2.17.0}/mycli/myclirc +8 -0
- mycli-2.17.0/mycli/packages/polars_completion.py +293 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/polars_transform.py +36 -24
- mycli-2.17.0/mycli/packages/ptoolkit/history.py +198 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/dbcommands.py +3 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/iocommands.py +17 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/main.py +21 -1
- {mycli-2.16.0 → mycli-2.17.0}/mycli/sqlcompleter.py +43 -14
- {mycli-2.16.0 → mycli-2.17.0/mycli.egg-info}/PKG-INFO +5 -6
- {mycli-2.16.0 → mycli-2.17.0}/mycli.egg-info/SOURCES.txt +2 -1
- {mycli-2.16.0 → mycli-2.17.0}/mycli.egg-info/requires.txt +4 -5
- {mycli-2.16.0 → mycli-2.17.0}/mycli.egg-info/scm_file_list.json +2 -1
- mycli-2.17.0/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.16.0 → mycli-2.17.0}/pyproject.toml +10 -8
- {mycli-2.16.0 → mycli-2.17.0}/test/myclirc +8 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_client_commands.py +26 -1
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_client_query.py +4 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_completion_refresher.py +6 -2
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_main_modes_repl.py +42 -4
- mycli-2.17.0/test/pytests/test_polars_completion.py +259 -0
- mycli-2.17.0/test/pytests/test_ptoolkit_history.py +410 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_smart_completion_public_schema_only.py +14 -8
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_special_main.py +14 -1
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_sqlcompleter.py +86 -0
- mycli-2.16.0/mycli/packages/ptoolkit/history.py +0 -59
- mycli-2.16.0/mycli.egg-info/scm_version.json +0 -8
- mycli-2.16.0/test/pytests/test_lexer.py +0 -10
- mycli-2.16.0/test/pytests/test_ptoolkit_history.py +0 -72
- {mycli-2.16.0 → mycli-2.17.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/dependabot.yml +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/workflows/ci.yml +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/workflows/publish.yml +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/.gitignore +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/AGENTS.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/AUTHORS.rst +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/CONTRIBUTING.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/LICENSE.txt +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/MANIFEST.in +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/README.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/SPONSORS.rst +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/doc/key_bindings.rst +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/doc/llm.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/doc/transforms.md +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/AUTHORS +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/SPONSORS +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/TIPS +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/app_state.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/cli_runner.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/clibuffer.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/client.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/client_connection.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/clistyle.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/clitoolbar.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/compat.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/config.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/constants.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/key_bindings.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main_modes/completions.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main_modes/execute.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/output.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/filepaths.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/llm.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/special/utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/password_sources.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/sqlexecute.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/types.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli/vault.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/setup.cfg +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/connection.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/crud_database.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/crud_table.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/db_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/environment.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/iocommands.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/named_queries.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/specials.feature +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/connection.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/specials.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/features/wrappager.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/mylogin.cnf +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/conftest.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_client.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_config.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_main.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_output.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_polars_transform.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_vault.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.16.0 → mycli-2.17.0}/test/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.0
|
|
4
4
|
Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -31,7 +31,7 @@ License-File: AUTHORS.rst
|
|
|
31
31
|
Requires-Dist: click~=8.3.3
|
|
32
32
|
Requires-Dist: clickdc~=0.1.1
|
|
33
33
|
Requires-Dist: cryptography~=50.0.0
|
|
34
|
-
Requires-Dist: Pygments~=2.
|
|
34
|
+
Requires-Dist: Pygments~=2.21.0
|
|
35
35
|
Requires-Dist: prompt_toolkit<4.0.0,>=3.0.41
|
|
36
36
|
Requires-Dist: PyMySQL~=1.1.2
|
|
37
37
|
Requires-Dist: sqlparse<0.6.0,>=0.3.0
|
|
@@ -48,10 +48,9 @@ Requires-Dist: keyring~=25.7.0
|
|
|
48
48
|
Requires-Dist: jinja2~=3.1.6
|
|
49
49
|
Requires-Dist: yaspin~=3.4.0
|
|
50
50
|
Provides-Extra: llm
|
|
51
|
-
Requires-Dist: llm~=0.
|
|
51
|
+
Requires-Dist: llm~=0.33; extra == "llm"
|
|
52
52
|
Requires-Dist: setuptools==83.*; extra == "llm"
|
|
53
53
|
Requires-Dist: pip~=26.1.2; extra == "llm"
|
|
54
|
-
Requires-Dist: openai~=2.48.0; extra == "llm"
|
|
55
54
|
Provides-Extra: dataframe
|
|
56
55
|
Requires-Dist: polars~=1.42.1; extra == "dataframe"
|
|
57
56
|
Requires-Dist: altair~=6.2.2; extra == "dataframe"
|
|
@@ -61,14 +60,14 @@ Requires-Dist: mycli[dataframe,llm]; extra == "all"
|
|
|
61
60
|
Provides-Extra: dev
|
|
62
61
|
Requires-Dist: behave~=1.3.3; extra == "dev"
|
|
63
62
|
Requires-Dist: coverage~=7.13.4; extra == "dev"
|
|
64
|
-
Requires-Dist: mypy~=2.3.
|
|
63
|
+
Requires-Dist: mypy~=2.3.1; extra == "dev"
|
|
65
64
|
Requires-Dist: pexpect~=4.9.0; extra == "dev"
|
|
66
65
|
Requires-Dist: pytest~=9.1.1; extra == "dev"
|
|
67
66
|
Requires-Dist: pytest-cov~=7.0.0; extra == "dev"
|
|
68
67
|
Requires-Dist: pytest-random-order~=1.2.0; extra == "dev"
|
|
69
68
|
Requires-Dist: tox~=4.35.0; extra == "dev"
|
|
70
69
|
Requires-Dist: pdbpp~=0.11.7; extra == "dev"
|
|
71
|
-
Requires-Dist: ruff~=0.
|
|
70
|
+
Requires-Dist: ruff~=0.16.3; extra == "dev"
|
|
72
71
|
Dynamic: license-file
|
|
73
72
|
|
|
74
73
|
# mycli
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
2.17.0 (2026/08/25)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
--------
|
|
6
|
+
* Sort completion candidates by frecency from history.
|
|
7
|
+
* Add help snippets in special /command completions.
|
|
8
|
+
* Add completions on Polars methods in dataframe transforms.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Internal
|
|
12
|
+
---------
|
|
13
|
+
* Upgrade `pygments` to v2.21.0, removing hacks for `set*` identifiers.
|
|
14
|
+
* Upgrade `llm` dependency to v3.33, removing pin for `openai`.
|
|
15
|
+
* Remove `pygments` cooldown exception as no longer needed.
|
|
16
|
+
* Upgrade `mypy` dev dependency to v2.3.1.
|
|
17
|
+
* Upgrade `ruff` dev dependency to v0.16.3.
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
2.16.0 (2026/08/22)
|
|
2
21
|
==============
|
|
3
22
|
|
|
@@ -17,6 +17,7 @@ from mycli.packages import special
|
|
|
17
17
|
from mycli.packages.batch_utils import statements_from_filehandle
|
|
18
18
|
from mycli.packages.filepaths import dir_path_exists
|
|
19
19
|
from mycli.packages.interactive_utils import confirm_destructive_query
|
|
20
|
+
from mycli.packages.ptoolkit.history import FileHistoryWithTimestamp
|
|
20
21
|
from mycli.packages.special import main as special_main
|
|
21
22
|
from mycli.packages.special.iocommands import expand_favorite_query
|
|
22
23
|
from mycli.packages.special.main import ArgType, SpecialCommandAlias
|
|
@@ -222,6 +223,7 @@ class ClientCommandsMixin:
|
|
|
222
223
|
destructive_keywords: Any
|
|
223
224
|
config: Any
|
|
224
225
|
myclirc_path: str
|
|
226
|
+
prompt_session: Any
|
|
225
227
|
prompt_format: str
|
|
226
228
|
|
|
227
229
|
def refresh_completions(self, reset: bool = False) -> list[SQLResult]: ...
|
|
@@ -235,6 +237,7 @@ class ClientCommandsMixin:
|
|
|
235
237
|
"/use <database>",
|
|
236
238
|
"Change to a new database.",
|
|
237
239
|
aliases=[SpecialCommandAlias("\\u", case_sensitive=False)],
|
|
240
|
+
completion_snippet='change databases',
|
|
238
241
|
)
|
|
239
242
|
special.register_special_command(
|
|
240
243
|
self.manual_reconnect,
|
|
@@ -243,14 +246,16 @@ class ClientCommandsMixin:
|
|
|
243
246
|
"Reconnect to the server, optionally switching databases.",
|
|
244
247
|
case_sensitive=True,
|
|
245
248
|
aliases=[SpecialCommandAlias("\\r", case_sensitive=True)],
|
|
249
|
+
completion_snippet='reconnect to server',
|
|
246
250
|
)
|
|
247
251
|
special.register_special_command(
|
|
248
|
-
self.
|
|
252
|
+
self.rehash,
|
|
249
253
|
"rehash",
|
|
250
254
|
"/rehash",
|
|
251
255
|
"Refresh auto-completions.",
|
|
252
256
|
arg_type=ArgType.NO_ARGUMENT,
|
|
253
257
|
aliases=[SpecialCommandAlias("\\#", case_sensitive=False)],
|
|
258
|
+
completion_snippet='refresh completions',
|
|
254
259
|
)
|
|
255
260
|
special.register_special_command(
|
|
256
261
|
self.change_table_format,
|
|
@@ -259,6 +264,7 @@ class ClientCommandsMixin:
|
|
|
259
264
|
"Change the table format used to output interactive results.",
|
|
260
265
|
case_sensitive=True,
|
|
261
266
|
aliases=[SpecialCommandAlias("\\T", case_sensitive=True)],
|
|
267
|
+
completion_snippet='change interactive output format',
|
|
262
268
|
)
|
|
263
269
|
special.register_special_command(
|
|
264
270
|
self.change_redirect_format,
|
|
@@ -267,6 +273,7 @@ class ClientCommandsMixin:
|
|
|
267
273
|
"Change the table format used to output redirected results.",
|
|
268
274
|
case_sensitive=True,
|
|
269
275
|
aliases=[SpecialCommandAlias("\\Tr", case_sensitive=True)],
|
|
276
|
+
completion_snippet='change redirected output format',
|
|
270
277
|
)
|
|
271
278
|
special.register_special_command(
|
|
272
279
|
self.execute_from_file,
|
|
@@ -274,6 +281,7 @@ class ClientCommandsMixin:
|
|
|
274
281
|
"/source [--special|--show|--page] <file>",
|
|
275
282
|
"Execute queries from a file.",
|
|
276
283
|
aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
|
|
284
|
+
completion_snippet='execute queries from file',
|
|
277
285
|
)
|
|
278
286
|
special.register_special_command(
|
|
279
287
|
self.change_prompt_format,
|
|
@@ -282,12 +290,14 @@ class ClientCommandsMixin:
|
|
|
282
290
|
"Show or change prompt format.",
|
|
283
291
|
case_sensitive=True,
|
|
284
292
|
aliases=[SpecialCommandAlias("\\R", case_sensitive=True)],
|
|
293
|
+
completion_snippet='show or change prompt format',
|
|
285
294
|
)
|
|
286
295
|
special.register_special_command(
|
|
287
296
|
self.config_command,
|
|
288
297
|
r'\config',
|
|
289
298
|
'/config <help|get|search|edit> [key]',
|
|
290
299
|
'Inspect settings from config files.',
|
|
300
|
+
completion_snippet='inspect config file settings',
|
|
291
301
|
)
|
|
292
302
|
|
|
293
303
|
def manual_reconnect(self, arg: str = "", **_) -> Generator[SQLResult, None, None]:
|
|
@@ -302,6 +312,13 @@ class ClientCommandsMixin:
|
|
|
302
312
|
else:
|
|
303
313
|
yield self.change_db(arg).send(None)
|
|
304
314
|
|
|
315
|
+
def rehash(self) -> list[SQLResult]:
|
|
316
|
+
prompt_session = getattr(self, 'prompt_session', None)
|
|
317
|
+
history = getattr(prompt_session, 'history', None)
|
|
318
|
+
if isinstance(history, FileHistoryWithTimestamp):
|
|
319
|
+
history.refresh_frecency()
|
|
320
|
+
return self.refresh_completions()
|
|
321
|
+
|
|
305
322
|
def change_table_format(self, arg: str, **_) -> Generator[SQLResult, None, None]:
|
|
306
323
|
try:
|
|
307
324
|
self.main_formatter.format_name = arg
|
|
@@ -59,6 +59,7 @@ class ClientQueryMixin:
|
|
|
59
59
|
"keyword_casing": self.completer.keyword_casing,
|
|
60
60
|
"indexed_column_suffix": self.completer.indexed_column_suffix,
|
|
61
61
|
"config_property_names": self.completer.config_property_names,
|
|
62
|
+
'frecency_provider': self.completer.frecency_provider,
|
|
62
63
|
},
|
|
63
64
|
)
|
|
64
65
|
|
|
@@ -267,7 +267,7 @@ def refresh_collations(completer: SQLCompleter, executor: SQLExecute) -> None:
|
|
|
267
267
|
|
|
268
268
|
@refresher("special_commands")
|
|
269
269
|
def refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None:
|
|
270
|
-
completer.extend_special_commands(
|
|
270
|
+
completer.extend_special_commands({command: details.completion_snippet or details.description for command, details in COMMANDS.items()})
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
@refresher("show_commands")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pygments.lexer import inherit
|
|
2
2
|
from pygments.lexers.sql import MySqlLexer
|
|
3
|
-
from pygments.token import Keyword
|
|
3
|
+
from pygments.token import Keyword
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MyCliLexer(MySqlLexer):
|
|
@@ -8,8 +8,6 @@ class MyCliLexer(MySqlLexer):
|
|
|
8
8
|
|
|
9
9
|
tokens = {
|
|
10
10
|
"root": [
|
|
11
|
-
# TODO: Remove once Pygments is upgraded above v2.20.0.
|
|
12
|
-
(r"\bset[\w$]+\b", Name),
|
|
13
11
|
(r"\brepair\b", Keyword),
|
|
14
12
|
(r"\boffset\b", Keyword),
|
|
15
13
|
inherit,
|
|
@@ -74,7 +74,7 @@ from mycli.packages.polars_transform import (
|
|
|
74
74
|
prepare_polars_transform,
|
|
75
75
|
run_polars_transform,
|
|
76
76
|
)
|
|
77
|
-
from mycli.packages.ptoolkit.history import FileHistoryWithTimestamp
|
|
77
|
+
from mycli.packages.ptoolkit.history import FRECENCY_HISTORY_ENTRIES, FRECENCY_REFRESH_INTERVAL, FileHistoryWithTimestamp
|
|
78
78
|
from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count
|
|
79
79
|
from mycli.packages.sql_utils import (
|
|
80
80
|
extract_new_password,
|
|
@@ -146,7 +146,13 @@ def complete_while_typing_filter() -> bool:
|
|
|
146
146
|
def _create_history(mycli: 'MyCli') -> FileHistoryWithTimestamp | None:
|
|
147
147
|
history_file = os.path.expanduser(os.environ.get('MYCLI_HISTFILE', mycli.config['main'].get('history_file', '~/.mycli-history')))
|
|
148
148
|
if dir_path_exists(history_file):
|
|
149
|
-
|
|
149
|
+
frecency_history_entries = int(mycli.config['main'].get('frecency_history_entries', FRECENCY_HISTORY_ENTRIES) or 0)
|
|
150
|
+
frecency_refresh_interval = int(mycli.config['main'].get('frecency_refresh_interval', FRECENCY_REFRESH_INTERVAL) or 0)
|
|
151
|
+
return FileHistoryWithTimestamp(
|
|
152
|
+
history_file,
|
|
153
|
+
frecency_history_entries=frecency_history_entries,
|
|
154
|
+
frecency_refresh_interval=frecency_refresh_interval,
|
|
155
|
+
)
|
|
150
156
|
|
|
151
157
|
mycli.echo(
|
|
152
158
|
f'Error: Unable to open the history file "{history_file}". Your query history will not be saved.',
|
|
@@ -1113,10 +1119,12 @@ def main_repl(mycli: 'MyCli') -> None:
|
|
|
1113
1119
|
|
|
1114
1120
|
mycli.configure_pager()
|
|
1115
1121
|
_configure_editor(mycli)
|
|
1122
|
+
history = _create_history(mycli)
|
|
1123
|
+
if history is not None:
|
|
1124
|
+
mycli.completer.frecency_provider = lambda: history.frecency
|
|
1116
1125
|
if mycli.smart_completion and not mycli.sandbox_mode:
|
|
1117
1126
|
mycli.refresh_completions()
|
|
1118
1127
|
|
|
1119
|
-
history = _create_history(mycli)
|
|
1120
1128
|
key_bindings = mycli_bindings(mycli)
|
|
1121
1129
|
_show_startup_banner(mycli, sqlexecute)
|
|
1122
1130
|
_build_prompt_session(mycli, state, history, key_bindings)
|
|
@@ -54,6 +54,14 @@ destructive_keywords = DROP SHUTDOWN DELETE TRUNCATE ALTER UPDATE
|
|
|
54
54
|
# interactive query history location.
|
|
55
55
|
history_file = ~/.mycli-history
|
|
56
56
|
|
|
57
|
+
# Number of recent history entries used to calculate frecency. Set to 0 or
|
|
58
|
+
# leave empty to disable frecency calculation entirely.
|
|
59
|
+
frecency_history_entries = 1000
|
|
60
|
+
|
|
61
|
+
# Number of REPL entries between frecency recalculation. Set to 0 to disable
|
|
62
|
+
# periodic recalculation.
|
|
63
|
+
frecency_refresh_interval = 50
|
|
64
|
+
|
|
57
65
|
# log_file location.
|
|
58
66
|
log_file = ~/.mycli.log
|
|
59
67
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
import inspect
|
|
8
|
+
from io import StringIO
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import tokenize
|
|
12
|
+
from types import ModuleType, UnionType
|
|
13
|
+
from typing import Any, Union, cast, get_args, get_origin
|
|
14
|
+
|
|
15
|
+
import sqlglot
|
|
16
|
+
|
|
17
|
+
from mycli.packages.polars_transform import _pipeline_operator_indexes
|
|
18
|
+
|
|
19
|
+
_ATTRIBUTE_PATTERN = re.compile(r'(?s)(.*)\.([A-Za-z_][A-Za-z0-9_]*)?\s*$')
|
|
20
|
+
_ANNOTATION_NAME_PATTERN = re.compile(r'[A-Za-z_][A-Za-z0-9_]*')
|
|
21
|
+
_SENTINEL = '__mycli_polars_completion__'
|
|
22
|
+
_OPEN_TO_CLOSE = {'(': ')', '[': ']', '{': '}'}
|
|
23
|
+
_CLOSE = frozenset(_OPEN_TO_CLOSE.values())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class PolarsCompletion:
|
|
28
|
+
text: str
|
|
29
|
+
display: str
|
|
30
|
+
display_meta: str
|
|
31
|
+
start_position: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def complete_polars_transform(command: str) -> list[PolarsCompletion] | None:
|
|
35
|
+
"""Return safe attribute completions for a Polars transform expression."""
|
|
36
|
+
try:
|
|
37
|
+
tokens = sqlglot.tokenize(command)
|
|
38
|
+
pipe_index, output_index = _pipeline_operator_indexes(command, tokens, require_operands=False)
|
|
39
|
+
except sqlglot.errors.TokenError:
|
|
40
|
+
return None
|
|
41
|
+
if pipe_index is None:
|
|
42
|
+
return None
|
|
43
|
+
if output_index is not None:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
pipe = tokens[pipe_index + 1]
|
|
47
|
+
expression = command[pipe.end + 1 :].strip()
|
|
48
|
+
match = _ATTRIBUTE_PATTERN.fullmatch(expression)
|
|
49
|
+
if match is None:
|
|
50
|
+
return []
|
|
51
|
+
prefix = match.group(2) or ''
|
|
52
|
+
source = f'{match.group(1)}.{_SENTINEL}'
|
|
53
|
+
closed_source = _close_open_delimiters(source)
|
|
54
|
+
if closed_source is None:
|
|
55
|
+
return []
|
|
56
|
+
try:
|
|
57
|
+
parsed = ast.parse(closed_source, mode='eval')
|
|
58
|
+
except SyntaxError:
|
|
59
|
+
return []
|
|
60
|
+
target = next(
|
|
61
|
+
(node for node in ast.walk(parsed) if isinstance(node, ast.Attribute) and node.attr == _SENTINEL),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
if target is None:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
polars = _load_polars()
|
|
68
|
+
if polars is None:
|
|
69
|
+
return []
|
|
70
|
+
owner = _infer_type(target.value, polars)
|
|
71
|
+
if owner is None:
|
|
72
|
+
return []
|
|
73
|
+
return [
|
|
74
|
+
PolarsCompletion(
|
|
75
|
+
text=f'{name}(' if is_callable else name,
|
|
76
|
+
display=name,
|
|
77
|
+
display_meta=metadata,
|
|
78
|
+
start_position=-len(prefix),
|
|
79
|
+
)
|
|
80
|
+
for name, is_callable, metadata in _members(owner, polars)
|
|
81
|
+
if name.startswith(prefix)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _close_open_delimiters(source: str) -> str | None:
|
|
86
|
+
stack: list[str] = []
|
|
87
|
+
try:
|
|
88
|
+
tokens = tokenize.generate_tokens(StringIO(source).readline)
|
|
89
|
+
for token in tokens:
|
|
90
|
+
if token.type != tokenize.OP:
|
|
91
|
+
continue
|
|
92
|
+
if token.string in _OPEN_TO_CLOSE:
|
|
93
|
+
stack.append(token.string)
|
|
94
|
+
elif token.string in _CLOSE:
|
|
95
|
+
if not stack or _OPEN_TO_CLOSE[stack.pop()] != token.string:
|
|
96
|
+
return None
|
|
97
|
+
except (IndentationError, tokenize.TokenError):
|
|
98
|
+
pass
|
|
99
|
+
return source + ''.join(_OPEN_TO_CLOSE[value] for value in reversed(stack))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@lru_cache(maxsize=1)
|
|
103
|
+
def _load_polars() -> Any | None:
|
|
104
|
+
try:
|
|
105
|
+
import polars as pl
|
|
106
|
+
except ImportError:
|
|
107
|
+
return None
|
|
108
|
+
return pl
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _infer_type(node: ast.AST, polars: Any) -> Any | None:
|
|
112
|
+
if isinstance(node, ast.Name):
|
|
113
|
+
if node.id == 'df':
|
|
114
|
+
return polars.DataFrame
|
|
115
|
+
if node.id == 'pl':
|
|
116
|
+
return polars
|
|
117
|
+
return None
|
|
118
|
+
if isinstance(node, ast.Subscript):
|
|
119
|
+
owner = _infer_type(node.value, polars)
|
|
120
|
+
if owner is polars.DataFrame:
|
|
121
|
+
return polars.Series
|
|
122
|
+
if owner is polars.Expr:
|
|
123
|
+
return polars.Expr
|
|
124
|
+
return None
|
|
125
|
+
if isinstance(node, ast.Attribute):
|
|
126
|
+
owner = _infer_type(node.value, polars)
|
|
127
|
+
member = _static_member(owner, node.attr)
|
|
128
|
+
if member is None:
|
|
129
|
+
return None
|
|
130
|
+
if isinstance(member, property):
|
|
131
|
+
return _return_type(member.fget, owner, polars)
|
|
132
|
+
if inspect.isclass(member) or isinstance(member, ModuleType):
|
|
133
|
+
return member
|
|
134
|
+
return None
|
|
135
|
+
if isinstance(node, ast.Call):
|
|
136
|
+
if isinstance(node.func, ast.Attribute):
|
|
137
|
+
owner = _infer_type(node.func.value, polars)
|
|
138
|
+
member = _static_member(owner, node.func.attr)
|
|
139
|
+
return _call_return_type(member, owner, polars)
|
|
140
|
+
return None
|
|
141
|
+
if isinstance(node, (ast.BinOp, ast.BoolOp, ast.Compare, ast.UnaryOp)):
|
|
142
|
+
inferred = [_infer_type(child, polars) for child in ast.iter_child_nodes(node)]
|
|
143
|
+
if polars.Expr in inferred:
|
|
144
|
+
return polars.Expr
|
|
145
|
+
if polars.Series in inferred:
|
|
146
|
+
return polars.Series
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _static_member(owner: Any | None, name: str) -> Any | None:
|
|
151
|
+
if owner is None:
|
|
152
|
+
return None
|
|
153
|
+
try:
|
|
154
|
+
member = inspect.getattr_static(owner, name)
|
|
155
|
+
except AttributeError:
|
|
156
|
+
return None
|
|
157
|
+
if isinstance(member, (classmethod, staticmethod)):
|
|
158
|
+
return member.__func__
|
|
159
|
+
return member
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _call_return_type(member: Any | None, owner: Any, polars: Any) -> Any | None:
|
|
163
|
+
if member is None:
|
|
164
|
+
return None
|
|
165
|
+
if inspect.isclass(member):
|
|
166
|
+
return member
|
|
167
|
+
result = _return_type(member, owner, polars)
|
|
168
|
+
if result is not None:
|
|
169
|
+
return result
|
|
170
|
+
call = _static_member(type(member), '__call__')
|
|
171
|
+
return _return_type(call, owner, polars)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _return_type(callable_object: Any | None, owner: Any, polars: Any) -> Any | None:
|
|
175
|
+
if callable_object is None:
|
|
176
|
+
return None
|
|
177
|
+
if inspect.isroutine(callable_object):
|
|
178
|
+
annotations = getattr(callable_object, '__annotations__', {})
|
|
179
|
+
else:
|
|
180
|
+
try:
|
|
181
|
+
annotations = inspect.getattr_static(callable_object, '__annotations__')
|
|
182
|
+
except AttributeError:
|
|
183
|
+
annotations = {}
|
|
184
|
+
if not isinstance(annotations, Mapping):
|
|
185
|
+
return None
|
|
186
|
+
annotation = annotations.get('return')
|
|
187
|
+
if annotation is None or annotation is inspect.Signature.empty:
|
|
188
|
+
return None
|
|
189
|
+
if annotation is Any:
|
|
190
|
+
return None
|
|
191
|
+
if inspect.isclass(annotation):
|
|
192
|
+
return annotation if _is_polars_type(annotation) else None
|
|
193
|
+
arguments = get_args(annotation)
|
|
194
|
+
if arguments:
|
|
195
|
+
origin = cast(Any, get_origin(annotation))
|
|
196
|
+
if origin not in (Union, UnionType):
|
|
197
|
+
return None
|
|
198
|
+
for argument in arguments:
|
|
199
|
+
resolved = _resolve_annotation(argument, callable_object, owner, polars)
|
|
200
|
+
if resolved is not None:
|
|
201
|
+
return resolved
|
|
202
|
+
return _resolve_annotation(annotation, callable_object, owner, polars)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _resolve_annotation(annotation: Any, callable_object: Any, owner: Any, polars: Any) -> Any | None:
|
|
206
|
+
if inspect.isclass(annotation):
|
|
207
|
+
return annotation if _is_polars_type(annotation) else None
|
|
208
|
+
if not isinstance(annotation, str):
|
|
209
|
+
return None
|
|
210
|
+
if annotation in ('Self', 'typing.Self'):
|
|
211
|
+
return owner
|
|
212
|
+
if '[' in annotation:
|
|
213
|
+
return None
|
|
214
|
+
namespace = getattr(callable_object, '__globals__', {}) if inspect.isroutine(callable_object) else {}
|
|
215
|
+
for name in _ANNOTATION_NAME_PATTERN.findall(annotation):
|
|
216
|
+
candidate = namespace.get(name)
|
|
217
|
+
if not inspect.isclass(candidate):
|
|
218
|
+
candidate = _static_member(polars, name)
|
|
219
|
+
if not inspect.isclass(candidate):
|
|
220
|
+
candidate = _polars_types_by_name(polars).get(name)
|
|
221
|
+
if inspect.isclass(candidate) and _is_polars_type(candidate):
|
|
222
|
+
return candidate
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _is_polars_type(value: type[Any]) -> bool:
|
|
227
|
+
return value.__module__ == 'polars' or value.__module__.startswith('polars.')
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@lru_cache(maxsize=1)
|
|
231
|
+
def _polars_types_by_name(polars: Any) -> Mapping[str, type[Any]]:
|
|
232
|
+
"""Index classes in loaded Polars modules without importing annotation paths."""
|
|
233
|
+
candidates: dict[str, type[Any] | None] = {}
|
|
234
|
+
modules = [
|
|
235
|
+
module
|
|
236
|
+
for module_name, module in tuple(sys.modules.items())
|
|
237
|
+
if module is not None and (module is polars or module_name.startswith('polars.'))
|
|
238
|
+
]
|
|
239
|
+
for module in modules:
|
|
240
|
+
for value in vars(module).values():
|
|
241
|
+
if not inspect.isclass(value) or not _is_polars_type(value):
|
|
242
|
+
continue
|
|
243
|
+
name = value.__name__
|
|
244
|
+
existing = candidates.get(name, value)
|
|
245
|
+
candidates[name] = value if existing is value else None
|
|
246
|
+
return {name: value for name, value in candidates.items() if value is not None}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache(maxsize=None)
|
|
250
|
+
def _members(owner: Any, polars: Any) -> tuple[tuple[str, bool, str], ...]:
|
|
251
|
+
members: list[tuple[str, bool, str]] = []
|
|
252
|
+
for name in dir(owner):
|
|
253
|
+
if name.startswith('_'):
|
|
254
|
+
continue
|
|
255
|
+
member = _static_member(owner, name)
|
|
256
|
+
if member is None:
|
|
257
|
+
continue
|
|
258
|
+
if isinstance(member, property):
|
|
259
|
+
members.append((name, False, _property_metadata(member, owner, polars)))
|
|
260
|
+
elif callable(member):
|
|
261
|
+
members.append((name, True, _signature_metadata(member)))
|
|
262
|
+
return tuple(members)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _property_metadata(member: property, owner: Any, polars: Any) -> str:
|
|
266
|
+
result = _return_type(member.fget, owner, polars)
|
|
267
|
+
return result.__name__ if inspect.isclass(result) else 'property'
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _signature_metadata(member: Callable[..., Any]) -> str:
|
|
271
|
+
callable_object: Any = member
|
|
272
|
+
try:
|
|
273
|
+
signature = inspect.signature(callable_object)
|
|
274
|
+
except (TypeError, ValueError):
|
|
275
|
+
callable_object = _static_member(type(member), '__call__')
|
|
276
|
+
try:
|
|
277
|
+
signature = inspect.signature(callable_object)
|
|
278
|
+
except (TypeError, ValueError):
|
|
279
|
+
return '()'
|
|
280
|
+
parameters = [parameter for parameter in signature.parameters.values() if parameter.name not in ('self', 'cls')]
|
|
281
|
+
rendered: list[str] = []
|
|
282
|
+
for parameter in parameters[:4]:
|
|
283
|
+
text = parameter.name
|
|
284
|
+
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
285
|
+
text = f'*{text}'
|
|
286
|
+
elif parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
|
287
|
+
text = f'**{text}'
|
|
288
|
+
elif parameter.default is not inspect.Parameter.empty:
|
|
289
|
+
text = f'{text}=...'
|
|
290
|
+
rendered.append(text)
|
|
291
|
+
if len(parameters) > 4:
|
|
292
|
+
rendered.append('...')
|
|
293
|
+
return f"({', '.join(rendered)})"
|
|
@@ -66,30 +66,7 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
|
|
|
66
66
|
except sqlglot.errors.TokenError as exc:
|
|
67
67
|
raise PolarsTransformError(f'Unable to parse Polars transform: {exc}') from exc
|
|
68
68
|
|
|
69
|
-
pipe_index
|
|
70
|
-
parquet_index: int | None = None
|
|
71
|
-
for index, token in enumerate(tokens[:-1]):
|
|
72
|
-
following = tokens[index + 1]
|
|
73
|
-
if token.token_type != sqlglot.TokenType.DOT:
|
|
74
|
-
continue
|
|
75
|
-
if token.start == 0 or not command[token.start - 1].isspace():
|
|
76
|
-
continue
|
|
77
|
-
if following.token_type not in (sqlglot.TokenType.PIPE, sqlglot.TokenType.GT):
|
|
78
|
-
continue
|
|
79
|
-
if following.end + 1 >= len(command):
|
|
80
|
-
if following.token_type == sqlglot.TokenType.PIPE:
|
|
81
|
-
raise PolarsTransformError('Polars transforms require a Python expression.')
|
|
82
|
-
raise PolarsTransformError('File saves require a destination path.')
|
|
83
|
-
if not command[following.end + 1].isspace():
|
|
84
|
-
continue
|
|
85
|
-
if following.token_type == sqlglot.TokenType.PIPE:
|
|
86
|
-
if pipe_index is not None:
|
|
87
|
-
raise PolarsTransformError('Polars transforms support only one ".|" operator.')
|
|
88
|
-
pipe_index = index
|
|
89
|
-
else:
|
|
90
|
-
if parquet_index is not None:
|
|
91
|
-
raise PolarsTransformError('File saves support only one ".>" operator.')
|
|
92
|
-
parquet_index = index
|
|
69
|
+
pipe_index, parquet_index = _pipeline_operator_indexes(command, tokens)
|
|
93
70
|
|
|
94
71
|
if pipe_index is None and parquet_index is None:
|
|
95
72
|
return None
|
|
@@ -138,6 +115,41 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
|
|
|
138
115
|
return PolarsPipeline(sql=sql, expression=expression, output_path=output_path, output_mode=output_mode)
|
|
139
116
|
|
|
140
117
|
|
|
118
|
+
def _pipeline_operator_indexes(
|
|
119
|
+
command: str,
|
|
120
|
+
tokens: list[sqlglot.Token],
|
|
121
|
+
*,
|
|
122
|
+
require_operands: bool = True,
|
|
123
|
+
) -> tuple[int | None, int | None]:
|
|
124
|
+
"""Locate transform and redirect operators in tokenized command text."""
|
|
125
|
+
|
|
126
|
+
pipe_index: int | None = None
|
|
127
|
+
parquet_index: int | None = None
|
|
128
|
+
for index, token in enumerate(tokens[:-1]):
|
|
129
|
+
following = tokens[index + 1]
|
|
130
|
+
if token.token_type != sqlglot.TokenType.DOT:
|
|
131
|
+
continue
|
|
132
|
+
if token.start == 0 or not command[token.start - 1].isspace():
|
|
133
|
+
continue
|
|
134
|
+
if following.token_type not in (sqlglot.TokenType.PIPE, sqlglot.TokenType.GT):
|
|
135
|
+
continue
|
|
136
|
+
if following.end + 1 >= len(command) and require_operands:
|
|
137
|
+
if following.token_type == sqlglot.TokenType.PIPE:
|
|
138
|
+
raise PolarsTransformError('Polars transforms require a Python expression.')
|
|
139
|
+
raise PolarsTransformError('File saves require a destination path.')
|
|
140
|
+
if following.end + 1 < len(command) and not command[following.end + 1].isspace():
|
|
141
|
+
continue
|
|
142
|
+
if following.token_type == sqlglot.TokenType.PIPE:
|
|
143
|
+
if pipe_index is not None:
|
|
144
|
+
raise PolarsTransformError('Polars transforms support only one ".|" operator.')
|
|
145
|
+
pipe_index = index
|
|
146
|
+
else:
|
|
147
|
+
if parquet_index is not None:
|
|
148
|
+
raise PolarsTransformError('File saves support only one ".>" operator.')
|
|
149
|
+
parquet_index = index
|
|
150
|
+
return pipe_index, parquet_index
|
|
151
|
+
|
|
152
|
+
|
|
141
153
|
def _parse_output_path(path: str) -> str:
|
|
142
154
|
if path[0] in ('\'', '"'):
|
|
143
155
|
if len(path) < 2 or path[-1] != path[0]:
|