From c248dbd993dbc6511af35080a74f1e6e8ddc27b4 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 23 Nov 2024 14:49:15 -0800 Subject: [PATCH 01/14] Remove note from docs about 3.8 and deterministic, closes #646 --- docs/python-api.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/python-api.rst b/docs/python-api.rst index 7b11c225..c6bf7762 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -2711,7 +2711,7 @@ By default, the name of the Python function will be used as the name of the SQL print(db.execute('select rev("hello")').fetchone()[0]) -Python 3.8 added the ability to register `deterministic SQLite functions `__, allowing you to indicate that a function will return the exact same result for any given inputs and hence allowing SQLite to apply some performance optimizations. You can mark a function as deterministic using ``deterministic=True``, like this: +If a function will return the exact same result for any given inputs you can register it as a `deterministic SQLite function `__ allowing SQLite to apply some performance optimizations: .. code-block:: python @@ -2719,8 +2719,6 @@ Python 3.8 added the ability to register `deterministic SQLite functions Date: Mon, 24 Nov 2025 08:47:41 -0800 Subject: [PATCH 02/14] Link to JSON Lines instead ndjson.org has an expired domain. --- docs/cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli.rst b/docs/cli.rst index 4fa6042e..5f3b7c18 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1128,7 +1128,7 @@ You can insert binary data into a BLOB column by first encoding it using base64 Inserting newline-delimited JSON -------------------------------- -You can also import `newline-delimited JSON `__ using the ``--nl`` option: +You can also import newline-delimited JSON (see `JSON Lines `__) using the ``--nl`` option: .. code-block:: bash From dc36b32130125d180d00507c942a42a93778dce2 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 7 May 2025 17:54:29 -0700 Subject: [PATCH 03/14] Small documentation updates to fix warnings --- docs/changelog.rst | 2 +- docs/cli-reference.rst | 1 - docs/cli.rst | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b4af0f9e..abe2cc58 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -111,7 +111,7 @@ This release introduces a new :ref:`plugin system `. Read more about th 3.32 (2023-05-21) ----------------- -- New experimental ``sqlite-utils tui`` interface for interactively building command-line invocations, powered by `Trogon `__. This requires an optional dependency, installed using ``sqlite-utils install trogon``. There is a screenshot :ref:`in the documentation `. (:issue:`545`) +- New experimental ``sqlite-utils tui`` interface for interactively building command-line invocations, powered by `Trogon `__. This requires an optional dependency, installed using ``sqlite-utils install trogon``. (:issue:`545`) - ``sqlite-utils analyze-tables`` command (:ref:`documentation `) now has a ``--common-limit 20`` option for changing the number of common/least-common values shown for each column. (:issue:`544`) - ``sqlite-utils analyze-tables --no-most`` and ``--no-least`` options for disabling calculation of most-common and least-common values. - If a column contains only ``null`` values, ``analyze-tables`` will no longer attempt to calculate the most common and least common values for that column. (:issue:`547`) diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index b0572642..d0a7c053 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -65,7 +65,6 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command. "create-spatial-index": "cli_spatialite_indexes", "install": "cli_install", "uninstall": "cli_uninstall", - "tui": "cli_tui", } commands.sort(key = lambda command: go_first.index(command) if command in go_first else 999) cog.out("\n") diff --git a/docs/cli.rst b/docs/cli.rst index 5f3b7c18..19ea6d3e 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1285,7 +1285,7 @@ You can set the ``SQLITE_UTILS_DETECT_TYPES`` environment variable if you want ` If a CSV or TSV file includes empty cells, like this one: -.. code-block:: csv +:: name,age,weight Cleo,6, From 3a14edce5d3e325130fde498402282f691b467d0 Mon Sep 17 00:00:00 2001 From: Alex Chan Date: Wed, 1 Oct 2025 21:37:05 +0100 Subject: [PATCH 04/14] Add a type hint for `db.close()` (#663) Closes #662 --- sqlite_utils/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index ef8c40e6..f524e13a 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -368,7 +368,7 @@ def __init__( pm.hook.prepare_connection(conn=self.conn) self.strict = strict - def close(self): + def close(self) -> None: "Close the SQLite connection, and the underlying database file" self.conn.close() From 151a3491d2fae7f7336ab5a8b107375abc465196 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 08:49:24 -0800 Subject: [PATCH 05/14] Add pip as a dependency to ensure plugin installation, closes #687 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e8797ba8..b0d93c6c 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ def get_long_description(): "tabulate", "python-dateutil", "pluggy", + "pip", ], extras_require={ "test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"], From 95f720b18c8aed708a129c6ff685bb0aca695134 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 23 Nov 2025 21:46:51 -0800 Subject: [PATCH 06/14] --functions can take filenames, can be used multiple times (#681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #659 The --functions option now accepts: - File paths ending in .py (e.g., --functions my_funcs.py) - Multiple invocations (e.g., --functions foo.py --functions 'def bar(): ...') - Inline Python code (existing behavior) Implementation follows the same pattern as llm's --functions flag (simonw/llm@a880c123). Changes: - Added multiple=True to --functions Click option in query, bulk, and memory commands - Modified _register_functions() to detect and read .py files - Updated _maybe_register_functions() to iterate over multiple function sources - Removed unused bytes/bytearray handling - Added comprehensive tests for file paths and multiple invocations - Updated documentation with examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Shorter help for --functions --------- Co-authored-by: Claude --- docs/cli-reference.rst | 6 ++-- docs/cli.rst | 16 +++++++++ sqlite_utils/cli.py | 40 ++++++++++++++++++---- tests/test_cli.py | 71 ++++++++++++++++++++++++++++++++++++++++ tests/test_cli_bulk.py | 26 +++++++++++++++ tests/test_cli_memory.py | 16 +++++++++ 6 files changed, 166 insertions(+), 9 deletions(-) diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index d0a7c053..df1fc3d6 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -131,7 +131,7 @@ See :ref:`cli_query`. -r, --raw Raw output, first column of first row --raw-lines Raw output, first column of each row -p, --param ... Named :parameters for SQL query - --functions TEXT Python code defining one or more custom SQL + --functions TEXT Python code or file path defining custom SQL functions --load-extension TEXT Path to SQLite extension, with optional :entrypoint @@ -174,7 +174,7 @@ See :ref:`cli_memory`. sqlite-utils memory animals.csv --schema Options: - --functions TEXT Python code defining one or more custom SQL + --functions TEXT Python code or file path defining custom SQL functions --attach ... Additional databases to attach - specify alias and filepath @@ -374,7 +374,7 @@ See :ref:`cli_bulk`. Options: --batch-size INTEGER Commit every X records - --functions TEXT Python code defining one or more custom SQL functions + --functions TEXT Python code or file path defining custom SQL functions --flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes {"a_b": 1} --nl Expect newline-delimited JSON diff --git a/docs/cli.rst b/docs/cli.rst index 19ea6d3e..8cf8df31 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -368,6 +368,22 @@ This example defines a function which extracts the domain from a URL: Every callable object defined in the block will be registered as a SQL function with the same name, with the exception of functions with names that begin with an underscore. +You can also pass the path to a Python file containing function definitions: + +.. code-block:: bash + + sqlite-utils query sites.db "select url, domain(url) from urls" --functions functions.py + +The ``--functions`` option can be used multiple times to load functions from multiple sources: + +.. code-block:: bash + + sqlite-utils query sites.db "select url, domain(url), extract_path(url) from urls" \ + --functions domain_funcs.py \ + --functions 'def extract_path(url): + from urllib.parse import urlparse + return urlparse(url).path' + .. _cli_query_extensions: SQLite extensions diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index cc2d86eb..d2eedbb0 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -962,7 +962,7 @@ def insert_upsert_implementation( db = sqlite_utils.Database(path) _load_extensions(db, load_extension) if functions: - _register_functions(db, functions) + _maybe_register_functions(db, functions) if (delimiter or quotechar or sniff or no_headers) and not tsv: csv = True if (nl + csv + tsv) >= 2: @@ -1370,7 +1370,9 @@ def upsert( @click.argument("file", type=click.File("rb"), required=True) @click.option("--batch-size", type=int, default=100, help="Commit every X records") @click.option( - "--functions", help="Python code defining one or more custom SQL functions" + "--functions", + help="Python code or file path defining custom SQL functions", + multiple=True, ) @import_options @load_extension_option @@ -1759,7 +1761,9 @@ def drop_view(path, view, ignore, load_extension): help="Named :parameters for SQL query", ) @click.option( - "--functions", help="Python code defining one or more custom SQL functions" + "--functions", + help="Python code or file path defining custom SQL functions", + multiple=True, ) @load_extension_option def query( @@ -1796,7 +1800,7 @@ def query( db.register_fts4_bm25() if functions: - _register_functions(db, functions) + _maybe_register_functions(db, functions) _execute_query( db, @@ -1824,7 +1828,9 @@ def query( ) @click.argument("sql") @click.option( - "--functions", help="Python code defining one or more custom SQL functions" + "--functions", + help="Python code or file path defining custom SQL functions", + multiple=True, ) @click.option( "--attach", @@ -1996,7 +2002,7 @@ def memory( db.register_fts4_bm25() if functions: - _register_functions(db, functions) + _maybe_register_functions(db, functions) if return_db: return db @@ -3281,6 +3287,13 @@ def _load_extensions(db, load_extension): def _register_functions(db, functions): # Register any Python functions as SQL functions: + # Check if this is a file path + if "\n" not in functions and functions.endswith(".py"): + try: + functions = pathlib.Path(functions).read_text() + except FileNotFoundError: + raise click.ClickException("File not found: {}".format(functions)) + sqlite3.enable_callback_tracebacks(True) globals = {} try: @@ -3291,3 +3304,18 @@ def _register_functions(db, functions): for name, value in globals.items(): if callable(value) and not name.startswith("_"): db.register_function(value, name=name) + + +def _value_or_none(value): + if getattr(value, "__class__", None).__name__ == "Sentinel": + return None + return value + + +def _maybe_register_functions(db, functions_list): + if not functions_list: + return + for functions in functions_list: + functions = _value_or_none(functions) + if isinstance(functions, str) and functions.strip(): + _register_functions(db, functions) diff --git a/tests/test_cli.py b/tests/test_cli.py index 4e564f13..4c3ca715 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -806,6 +806,77 @@ def test_hidden_functions_are_hidden(db_path): assert "_two" not in functions +def test_query_functions_from_file(db_path, tmp_path): + # Create a temporary file with function definitions + functions_file = tmp_path / "my_functions.py" + functions_file.write_text(TEST_FUNCTIONS) + + result = CliRunner().invoke( + cli.cli, + [ + db_path, + "select zero(), one(1), two(1, 2)", + "--functions", + str(functions_file), + ], + ) + assert result.exit_code == 0 + assert json.loads(result.output.strip()) == [ + {"zero()": 0, "one(1)": 1, "two(1, 2)": 3} + ] + + +def test_query_functions_file_not_found(db_path): + result = CliRunner().invoke( + cli.cli, + [ + db_path, + "select zero()", + "--functions", + "nonexistent.py", + ], + ) + assert result.exit_code == 1 + assert "File not found: nonexistent.py" in result.output + + +def test_query_functions_multiple_invocations(db_path): + # Test using --functions multiple times + result = CliRunner().invoke( + cli.cli, + [ + db_path, + "select triple(2), quadruple(2)", + "--functions", + "def triple(x):\n return x * 3", + "--functions", + "def quadruple(x):\n return x * 4", + ], + ) + assert result.exit_code == 0 + assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}] + + +def test_query_functions_file_and_inline(db_path, tmp_path): + # Test combining file and inline code + functions_file = tmp_path / "file_funcs.py" + functions_file.write_text("def triple(x):\n return x * 3") + + result = CliRunner().invoke( + cli.cli, + [ + db_path, + "select triple(2), quadruple(2)", + "--functions", + str(functions_file), + "--functions", + "def quadruple(x):\n return x * 4", + ], + ) + assert result.exit_code == 0 + assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}] + + LOREM_IPSUM_COMPRESSED = ( b"x\x9c\xed\xd1\xcdq\x03!\x0c\x05\xe0\xbb\xabP\x01\x1eW\x91\xdc|M\x01\n\xc8\x8e" b"f\xf83H\x1e\x97\x1f\x91M\x8e\xe9\xe0\xdd\x96\x05\x84\xf4\xbek\x9fRI\xc7\xf2J" diff --git a/tests/test_cli_bulk.py b/tests/test_cli_bulk.py index 909ed096..514f4acc 100644 --- a/tests/test_cli_bulk.py +++ b/tests/test_cli_bulk.py @@ -45,6 +45,32 @@ def test_cli_bulk(test_db_and_path): ] == list(db["example"].rows) +def test_cli_bulk_multiple_functions(test_db_and_path): + db, db_path = test_db_and_path + result = CliRunner().invoke( + cli.cli, + [ + "bulk", + db_path, + "insert into example (id, name) values (:id, myupper(mylower(:name)))", + "-", + "--nl", + "--functions", + "myupper = lambda s: s.upper()", + "--functions", + "mylower = lambda s: s.lower()", + ], + input='{"id": 3, "name": "ThReE"}\n{"id": 4, "name": "FoUr"}\n', + ) + assert result.exit_code == 0, result.output + assert [ + {"id": 1, "name": "One"}, + {"id": 2, "name": "Two"}, + {"id": 3, "name": "THREE"}, + {"id": 4, "name": "FOUR"}, + ] == list(db["example"].rows) + + def test_cli_bulk_batch_size(test_db_and_path): db, db_path = test_db_and_path proc = subprocess.Popen( diff --git a/tests/test_cli_memory.py b/tests/test_cli_memory.py index ac0a177d..dfe9915e 100644 --- a/tests/test_cli_memory.py +++ b/tests/test_cli_memory.py @@ -307,6 +307,22 @@ def test_memory_functions(): assert result.output.strip() == '[{"hello()": "Hello"}]' +def test_memory_functions_multiple(): + result = CliRunner().invoke( + cli.cli, + [ + "memory", + "select triple(2), quadruple(2)", + "--functions", + "def triple(x):\n return x * 3", + "--functions", + "def quadruple(x):\n return x * 4", + ], + ) + assert result.exit_code == 0 + assert result.output.strip() == '[{"triple(2)": 6, "quadruple(2)": 8}]' + + def test_memory_return_db(tmpdir): # https://github.com/simonw/sqlite-utils/issues/643 from sqlite_utils.cli import cli From 811d7f712d422bce7d7605dbe1b03ed65a08cf38 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 09:15:06 -0800 Subject: [PATCH 07/14] click>=8.3.1 to avoid Sentinel issues Refs #666, #688 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b0d93c6c..63b56848 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def get_long_description(): package_data={"sqlite_utils": ["py.typed"]}, install_requires=[ "sqlite-fts4", - "click", + "click>=8.3.1", "click-default-group>=1.2.3", "tabulate", "python-dateutil", From f35d4c9e2c17a74654f8f6dc9ff04f2cfef2cba3 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 09:44:34 -0800 Subject: [PATCH 08/14] A bunch of minor fixes --- docs/changelog.rst | 2 +- docs/cli-reference.rst | 1 + docs/cli.rst | 2 +- sqlite_utils/cli.py | 16 +++++----------- sqlite_utils/db.py | 11 +++++++++-- tests/test_cli.py | 4 ++-- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index abe2cc58..b4af0f9e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -111,7 +111,7 @@ This release introduces a new :ref:`plugin system `. Read more about th 3.32 (2023-05-21) ----------------- -- New experimental ``sqlite-utils tui`` interface for interactively building command-line invocations, powered by `Trogon `__. This requires an optional dependency, installed using ``sqlite-utils install trogon``. (:issue:`545`) +- New experimental ``sqlite-utils tui`` interface for interactively building command-line invocations, powered by `Trogon `__. This requires an optional dependency, installed using ``sqlite-utils install trogon``. There is a screenshot :ref:`in the documentation `. (:issue:`545`) - ``sqlite-utils analyze-tables`` command (:ref:`documentation `) now has a ``--common-limit 20`` option for changing the number of common/least-common values shown for each column. (:issue:`544`) - ``sqlite-utils analyze-tables --no-most`` and ``--no-least`` options for disabling calculation of most-common and least-common values. - If a column contains only ``null`` values, ``analyze-tables`` will no longer attempt to calculate the most common and least common values for that column. (:issue:`547`) diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index df1fc3d6..2c52c0ab 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -65,6 +65,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command. "create-spatial-index": "cli_spatialite_indexes", "install": "cli_install", "uninstall": "cli_uninstall", + "tui": "cli_tui", } commands.sort(key = lambda command: go_first.index(command) if command in go_first else 999) cog.out("\n") diff --git a/docs/cli.rst b/docs/cli.rst index 8cf8df31..f6a11ef4 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1301,7 +1301,7 @@ You can set the ``SQLITE_UTILS_DETECT_TYPES`` environment variable if you want ` If a CSV or TSV file includes empty cells, like this one: -:: +.. code-block:: csv name,age,weight Cleo,6, diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index d2eedbb0..9c7128ee 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -962,7 +962,7 @@ def insert_upsert_implementation( db = sqlite_utils.Database(path) _load_extensions(db, load_extension) if functions: - _maybe_register_functions(db, functions) + _register_functions_from_multiple(db, functions) if (delimiter or quotechar or sniff or no_headers) and not tsv: csv = True if (nl + csv + tsv) >= 2: @@ -1800,7 +1800,7 @@ def query( db.register_fts4_bm25() if functions: - _maybe_register_functions(db, functions) + _register_functions_from_multiple(db, functions) _execute_query( db, @@ -2002,7 +2002,7 @@ def memory( db.register_fts4_bm25() if functions: - _maybe_register_functions(db, functions) + _register_functions_from_multiple(db, functions) if return_db: return db @@ -3306,16 +3306,10 @@ def _register_functions(db, functions): db.register_function(value, name=name) -def _value_or_none(value): - if getattr(value, "__class__", None).__name__ == "Sentinel": - return None - return value - - -def _maybe_register_functions(db, functions_list): +def _register_functions_from_multiple(db, functions_list): + """Register functions from multiple --functions arguments.""" if not functions_list: return for functions in functions_list: - functions = _value_or_none(functions) if isinstance(functions, str) and functions.strip(): _register_functions(db, functions) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index f524e13a..8939a688 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -237,36 +237,43 @@ class Default: class AlterError(Exception): "Error altering table" + pass class NoObviousTable(Exception): "Could not tell which table this operation refers to" + pass class NoTable(Exception): "Specified table does not exist" + pass class BadPrimaryKey(Exception): "Table does not have a single obvious primary key" + pass class NotFoundError(Exception): "Record not found" + pass class PrimaryKeyRequired(Exception): "Primary key needs to be specified" + pass class InvalidColumns(Exception): "Specified columns do not exist" + pass @@ -3203,7 +3210,7 @@ def insert( :param not_null: Set of strings specifying columns that should be ``NOT NULL``. :param defaults: Dictionary specifying default values for specific columns. :param hash_id: Name of a column to create and use as a primary key, where the - value of thet primary key will be derived as a SHA1 hash of the other column values + value of that primary key will be derived as a SHA1 hash of the other column values in the record. ``hash_id="id"`` is a common column name used for this. :param alter: Boolean, should any missing columns be added automatically? :param ignore: Boolean, if a record already exists with this primary key, ignore this insert. @@ -3852,7 +3859,7 @@ def jsonify_if_needed(value): def resolve_extracts( - extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]] + extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]], ) -> dict: if extracts is None: extracts = {} diff --git a/tests/test_cli.py b/tests/test_cli.py index 4c3ca715..f32ddb41 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -987,7 +987,7 @@ def test_query_json_with_json_cols(db_path): @pytest.mark.parametrize( "content,is_binary", - [(b"\x00\x0Fbinary", True), ("this is text", False), (1, False), (1.5, False)], + [(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)], ) def test_query_raw(db_path, content, is_binary): Database(db_path)["files"].insert({"content": content}) @@ -1002,7 +1002,7 @@ def test_query_raw(db_path, content, is_binary): @pytest.mark.parametrize( "content,is_binary", - [(b"\x00\x0Fbinary", True), ("this is text", False), (1, False), (1.5, False)], + [(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)], ) def test_query_raw_lines(db_path, content, is_binary): Database(db_path)["files"].insert_all({"content": content} for _ in range(3)) From 56a8e44d0d3852d18c866efde1c1191fd9851c3e Mon Sep 17 00:00:00 2001 From: fry69 <142489379+fry69@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:03:01 +0200 Subject: [PATCH 09/14] test: fix Python 3.14 datetime deprecation --- sqlite_utils/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 9c7128ee..33fe3aa2 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1,7 +1,7 @@ import base64 import click from click_default_group import DefaultGroup # type: ignore -from datetime import datetime +from datetime import datetime, timezone import hashlib import pathlib from runpy import run_module @@ -3209,8 +3209,8 @@ def __init__(self, exception, path): "ctime": lambda p: p.stat().st_ctime, "mtime_int": lambda p: int(p.stat().st_mtime), "ctime_int": lambda p: int(p.stat().st_ctime), - "mtime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_mtime).isoformat(), - "ctime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_ctime).isoformat(), + "mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc).replace(tzinfo=None).isoformat(), + "ctime_iso": lambda p: datetime.fromtimestamp(p.stat().st_ctime, timezone.utc).replace(tzinfo=None).isoformat(), "size": lambda p: p.stat().st_size, "stem": lambda p: p.stem, "suffix": lambda p: p.suffix, From fe3a60d486ab128c598944c7ba9050a58a313f7c Mon Sep 17 00:00:00 2001 From: fry69 <142489379+fry69@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:28:12 +0200 Subject: [PATCH 10/14] test: suppress PytestUnraisableExceptionWarning in dateparse_errors --- tests/test_recipes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index eca39874..ff042253 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -64,6 +64,7 @@ def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected): @pytest.mark.parametrize("fn", ("parsedate", "parsedatetime")) @pytest.mark.parametrize("errors", (None, recipes.SET_NULL, recipes.IGNORE)) +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") def test_dateparse_errors(fresh_db, fn, errors): fresh_db["example"].insert_all( [ From 6e8547e6fc15b5a2b990839d8146dc742b039d2f Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 09:49:53 -0800 Subject: [PATCH 11/14] Update Actions, Python >=3.10, refs #688 --- .github/workflows/publish.yml | 20 +++++--------------- .github/workflows/spellcheck.yml | 9 ++------- .github/workflows/test-coverage.yml | 9 ++------- .github/workflows/test.yml | 16 +++------------- setup.py | 6 ++---- 5 files changed, 14 insertions(+), 46 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0d36f88d..0f13226e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 @@ -17,13 +17,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v4 - name: Configure pip caching - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + cache: pip + cache-dependency-path: setup.py - name: Install dependencies run: | pip install -e '.[test]' @@ -39,13 +34,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' - - uses: actions/cache@v4 - name: Configure pip caching - with: - path: ~/.cache/pip - key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-publish-pip- + cache: pip + cache-dependency-path: setup.py - name: Install dependencies run: | pip install setuptools wheel twine diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index a3d55325..88431202 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -11,13 +11,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.12" - - uses: actions/cache@v4 - name: Configure pip caching - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + cache: pip + cache-dependency-path: setup.py - name: Install dependencies run: | pip install -e '.[docs]' diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index a710e412..0aa89862 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -17,13 +17,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: actions/cache@v4 - name: Configure pip caching - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + cache: pip + cache-dependency-path: setup.py - name: Install SpatiaLite run: sudo apt-get install libsqlite3-mod-spatialite - name: Install Python dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34c8cfbc..6297cd6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,15 +10,10 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] numpy: [0, 1] os: [ubuntu-latest, macos-latest, windows-latest, macos-14] - # Skip 3.8 and 3.9 on macos-14 - it only has 3.10+ exclude: - - python-version: "3.8" - os: macos-14 - - python-version: "3.9" - os: macos-14 - python-version: "3.13" numpy: 1 steps: @@ -28,13 +23,8 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - uses: actions/cache@v4 - name: Configure pip caching - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + cache: pip + cache-dependency-path: setup.py - name: Install dependencies run: | pip install -e '.[test,mypy,flake8]' diff --git a/setup.py b/setup.py index 63b56848..4fcea11e 100644 --- a/setup.py +++ b/setup.py @@ -65,20 +65,18 @@ def get_long_description(): "Issues": "https://github.com/simonw/sqlite-utils/issues", "CI": "https://github.com/simonw/sqlite-utils/actions", }, - python_requires=">=3.8", + python_requires=">=3.10", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: End Users/Desktop", "Topic :: Database", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], # Needed to bundle py.typed so mypy can see it: zip_safe=False, From 26181c8724f4ec8aae5c22f7d2b170ef4b2d1594 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 09:51:11 -0800 Subject: [PATCH 12/14] Applied Black --- sqlite_utils/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 33fe3aa2..b5821f97 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -3209,8 +3209,12 @@ def __init__(self, exception, path): "ctime": lambda p: p.stat().st_ctime, "mtime_int": lambda p: int(p.stat().st_mtime), "ctime_int": lambda p: int(p.stat().st_ctime), - "mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc).replace(tzinfo=None).isoformat(), - "ctime_iso": lambda p: datetime.fromtimestamp(p.stat().st_ctime, timezone.utc).replace(tzinfo=None).isoformat(), + "mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc) + .replace(tzinfo=None) + .isoformat(), + "ctime_iso": lambda p: datetime.fromtimestamp(p.stat().st_ctime, timezone.utc) + .replace(tzinfo=None) + .isoformat(), "size": lambda p: p.stat().st_size, "stem": lambda p: p.stem, "suffix": lambda p: p.suffix, From 5ec562ed1145775084383e1b72043e94c42a7183 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 10:14:47 -0800 Subject: [PATCH 13/14] Run cog with --diff --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6297cd6d..5d32c06e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,4 +54,4 @@ jobs: run: black . --check - name: Check if cog needs to be run run: | - cog --check README.md docs/*.rst + cog --check --diff README.md docs/*.rst From 7d7e17371f0c8b954c079e696adbc466e8b85bdc Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 24 Nov 2025 10:21:29 -0800 Subject: [PATCH 14/14] Ran cog --- docs/cli-reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 2c52c0ab..aa9058f7 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -1497,7 +1497,7 @@ See :ref:`cli_spatialite`. paths. To load it from a specific path, use --load-extension. Options: - -t, --type [POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION|GEOMETRY] + -t, --type [point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection|geometry] Specify a geometry type for this column. [default: GEOMETRY] --srid INTEGER Spatial Reference ID. See