sqlite-utils 3.38__tar.gz → 3.39__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.
- {sqlite_utils-3.38/sqlite_utils.egg-info → sqlite_utils-3.39}/PKG-INFO +18 -7
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/changelog.rst +11 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/cli-reference.rst +4 -4
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/cli.rst +17 -1
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/python-api.rst +1 -3
- {sqlite_utils-3.38 → sqlite_utils-3.39}/setup.py +5 -6
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/cli.py +35 -9
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/db.py +10 -3
- {sqlite_utils-3.38 → sqlite_utils-3.39/sqlite_utils.egg-info}/PKG-INFO +18 -7
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/requires.txt +2 -1
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli.py +73 -2
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_bulk.py +26 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_memory.py +16 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_recipes.py +1 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/LICENSE +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/MANIFEST.in +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/README.md +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/contributing.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/index.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/installation.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/plugins.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/reference.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/setup.cfg +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/__init__.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/__main__.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/hookspecs.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/plugins.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/py.typed +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/recipes.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/utils.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/SOURCES.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/dependency_links.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/entry_points.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/not-zip-safe +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/top_level.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/__init__.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/conftest.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_analyze.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_analyze_tables.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_attach.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_convert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_insert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_column_affinity.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_constructor.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_conversions.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_convert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_create.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_create_view.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_default_value.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_delete.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_docs.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_duplicate.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_enable_counts.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_extract.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_extracts.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_fts.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_get.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_gis.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_hypothesis.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_insert_files.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_introspect.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_lookup.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_m2m.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_plugins.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_query.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_recreate.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_register_function.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_rows.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_rows_from_file.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_sniff.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_suggest_column_types.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_tracer.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_transform.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_update.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_upsert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_utils.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_wal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.39
|
|
4
4
|
Summary: CLI tool and Python library for manipulating SQLite databases
|
|
5
5
|
Home-page: https://github.com/simonw/sqlite-utils
|
|
6
6
|
Author: Simon Willison
|
|
@@ -15,22 +15,21 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
17
17
|
Classifier: Topic :: Database
|
|
18
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
26
24
|
Description-Content-Type: text/markdown
|
|
27
25
|
License-File: LICENSE
|
|
28
26
|
Requires-Dist: sqlite-fts4
|
|
29
|
-
Requires-Dist: click
|
|
27
|
+
Requires-Dist: click>=8.3.1
|
|
30
28
|
Requires-Dist: click-default-group>=1.2.3
|
|
31
29
|
Requires-Dist: tabulate
|
|
32
30
|
Requires-Dist: python-dateutil
|
|
33
31
|
Requires-Dist: pluggy
|
|
32
|
+
Requires-Dist: pip
|
|
34
33
|
Provides-Extra: test
|
|
35
34
|
Requires-Dist: pytest; extra == "test"
|
|
36
35
|
Requires-Dist: black>=24.1.1; extra == "test"
|
|
@@ -54,6 +53,18 @@ Provides-Extra: flake8
|
|
|
54
53
|
Requires-Dist: flake8; extra == "flake8"
|
|
55
54
|
Provides-Extra: tui
|
|
56
55
|
Requires-Dist: trogon; extra == "tui"
|
|
56
|
+
Dynamic: author
|
|
57
|
+
Dynamic: classifier
|
|
58
|
+
Dynamic: description
|
|
59
|
+
Dynamic: description-content-type
|
|
60
|
+
Dynamic: home-page
|
|
61
|
+
Dynamic: license
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
Dynamic: project-url
|
|
64
|
+
Dynamic: provides-extra
|
|
65
|
+
Dynamic: requires-dist
|
|
66
|
+
Dynamic: requires-python
|
|
67
|
+
Dynamic: summary
|
|
57
68
|
|
|
58
69
|
# sqlite-utils
|
|
59
70
|
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
Changelog
|
|
5
5
|
===========
|
|
6
6
|
|
|
7
|
+
.. _v3_39:
|
|
8
|
+
|
|
9
|
+
3.39 (2025-11-24)
|
|
10
|
+
-----------------
|
|
11
|
+
|
|
12
|
+
- Fixed a bug with ``sqlite-utils install`` when the tool had been installed using ``uv``. (:issue:`687`)
|
|
13
|
+
- The ```--functions``` argument now optionally accepts a path to a Python file as an alternative to a string full of code, and can be specified multiple times - see :ref:`cli_query_functions`. (:issue:`659`)
|
|
14
|
+
- ``sqlite-utils`` now requires on Python 3.10 or higher.
|
|
15
|
+
|
|
16
|
+
`sqlite-utils 4.0a1 <https://sqlite-utils.datasette.io/en/latest/changelog.html#a1-2025-11-23>`__ is now available as an alpha with some `minor breaking changes <https://simonwillison.net/2025/Nov/24/sqlite-utils-40a1/>`__.
|
|
17
|
+
|
|
7
18
|
.. _v3_38:
|
|
8
19
|
|
|
9
20
|
3.38 (2024-11-23)
|
|
@@ -132,7 +132,7 @@ See :ref:`cli_query`.
|
|
|
132
132
|
-r, --raw Raw output, first column of first row
|
|
133
133
|
--raw-lines Raw output, first column of each row
|
|
134
134
|
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
|
135
|
-
--functions TEXT Python code
|
|
135
|
+
--functions TEXT Python code or file path defining custom SQL
|
|
136
136
|
functions
|
|
137
137
|
--load-extension TEXT Path to SQLite extension, with optional
|
|
138
138
|
:entrypoint
|
|
@@ -175,7 +175,7 @@ See :ref:`cli_memory`.
|
|
|
175
175
|
sqlite-utils memory animals.csv --schema
|
|
176
176
|
|
|
177
177
|
Options:
|
|
178
|
-
--functions TEXT Python code
|
|
178
|
+
--functions TEXT Python code or file path defining custom SQL
|
|
179
179
|
functions
|
|
180
180
|
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
|
181
181
|
filepath
|
|
@@ -375,7 +375,7 @@ See :ref:`cli_bulk`.
|
|
|
375
375
|
|
|
376
376
|
Options:
|
|
377
377
|
--batch-size INTEGER Commit every X records
|
|
378
|
-
--functions TEXT Python code
|
|
378
|
+
--functions TEXT Python code or file path defining custom SQL functions
|
|
379
379
|
--flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes
|
|
380
380
|
{"a_b": 1}
|
|
381
381
|
--nl Expect newline-delimited JSON
|
|
@@ -1497,7 +1497,7 @@ See :ref:`cli_spatialite`.
|
|
|
1497
1497
|
paths. To load it from a specific path, use --load-extension.
|
|
1498
1498
|
|
|
1499
1499
|
Options:
|
|
1500
|
-
-t, --type [
|
|
1500
|
+
-t, --type [point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection|geometry]
|
|
1501
1501
|
Specify a geometry type for this column.
|
|
1502
1502
|
[default: GEOMETRY]
|
|
1503
1503
|
--srid INTEGER Spatial Reference ID. See
|
|
@@ -368,6 +368,22 @@ This example defines a function which extracts the domain from a URL:
|
|
|
368
368
|
|
|
369
369
|
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.
|
|
370
370
|
|
|
371
|
+
You can also pass the path to a Python file containing function definitions:
|
|
372
|
+
|
|
373
|
+
.. code-block:: bash
|
|
374
|
+
|
|
375
|
+
sqlite-utils query sites.db "select url, domain(url) from urls" --functions functions.py
|
|
376
|
+
|
|
377
|
+
The ``--functions`` option can be used multiple times to load functions from multiple sources:
|
|
378
|
+
|
|
379
|
+
.. code-block:: bash
|
|
380
|
+
|
|
381
|
+
sqlite-utils query sites.db "select url, domain(url), extract_path(url) from urls" \
|
|
382
|
+
--functions domain_funcs.py \
|
|
383
|
+
--functions 'def extract_path(url):
|
|
384
|
+
from urllib.parse import urlparse
|
|
385
|
+
return urlparse(url).path'
|
|
386
|
+
|
|
371
387
|
.. _cli_query_extensions:
|
|
372
388
|
|
|
373
389
|
SQLite extensions
|
|
@@ -1128,7 +1144,7 @@ You can insert binary data into a BLOB column by first encoding it using base64
|
|
|
1128
1144
|
Inserting newline-delimited JSON
|
|
1129
1145
|
--------------------------------
|
|
1130
1146
|
|
|
1131
|
-
You can also import
|
|
1147
|
+
You can also import newline-delimited JSON (see `JSON Lines <https://jsonlines.org/>`__) using the ``--nl`` option:
|
|
1132
1148
|
|
|
1133
1149
|
.. code-block:: bash
|
|
1134
1150
|
|
|
@@ -2711,7 +2711,7 @@ By default, the name of the Python function will be used as the name of the SQL
|
|
|
2711
2711
|
|
|
2712
2712
|
print(db.execute('select rev("hello")').fetchone()[0])
|
|
2713
2713
|
|
|
2714
|
-
|
|
2714
|
+
If a function will return the exact same result for any given inputs you can register it as a `deterministic SQLite function <https://sqlite.org/deterministic.html>`__ allowing SQLite to apply some performance optimizations:
|
|
2715
2715
|
|
|
2716
2716
|
.. code-block:: python
|
|
2717
2717
|
|
|
@@ -2719,8 +2719,6 @@ Python 3.8 added the ability to register `deterministic SQLite functions <https:
|
|
|
2719
2719
|
def reverse_string(s):
|
|
2720
2720
|
return "".join(reversed(list(s)))
|
|
2721
2721
|
|
|
2722
|
-
If you run this on a version of Python prior to 3.8 your code will still work, but the ``deterministic=True`` parameter will be ignored.
|
|
2723
|
-
|
|
2724
2722
|
By default registering a function with the same name and number of arguments will have no effect - the ``Database`` instance keeps track of functions that have already been registered and skips registering them if ``@db.register_function`` is called a second time.
|
|
2725
2723
|
|
|
2726
2724
|
If you want to deliberately replace the registered function with a new implementation, use the ``replace=True`` argument:
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
import io
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
VERSION = "3.
|
|
5
|
+
VERSION = "3.39"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_long_description():
|
|
@@ -25,11 +25,12 @@ setup(
|
|
|
25
25
|
package_data={"sqlite_utils": ["py.typed"]},
|
|
26
26
|
install_requires=[
|
|
27
27
|
"sqlite-fts4",
|
|
28
|
-
"click",
|
|
28
|
+
"click>=8.3.1",
|
|
29
29
|
"click-default-group>=1.2.3",
|
|
30
30
|
"tabulate",
|
|
31
31
|
"python-dateutil",
|
|
32
32
|
"pluggy",
|
|
33
|
+
"pip",
|
|
33
34
|
],
|
|
34
35
|
extras_require={
|
|
35
36
|
"test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
|
|
@@ -64,20 +65,18 @@ setup(
|
|
|
64
65
|
"Issues": "https://github.com/simonw/sqlite-utils/issues",
|
|
65
66
|
"CI": "https://github.com/simonw/sqlite-utils/actions",
|
|
66
67
|
},
|
|
67
|
-
python_requires=">=3.
|
|
68
|
+
python_requires=">=3.10",
|
|
68
69
|
classifiers=[
|
|
69
70
|
"Development Status :: 5 - Production/Stable",
|
|
70
71
|
"Intended Audience :: Developers",
|
|
71
72
|
"Intended Audience :: Science/Research",
|
|
72
73
|
"Intended Audience :: End Users/Desktop",
|
|
73
74
|
"Topic :: Database",
|
|
74
|
-
"License :: OSI Approved :: Apache Software License",
|
|
75
|
-
"Programming Language :: Python :: 3.8",
|
|
76
|
-
"Programming Language :: Python :: 3.9",
|
|
77
75
|
"Programming Language :: Python :: 3.10",
|
|
78
76
|
"Programming Language :: Python :: 3.11",
|
|
79
77
|
"Programming Language :: Python :: 3.12",
|
|
80
78
|
"Programming Language :: Python :: 3.13",
|
|
79
|
+
"Programming Language :: Python :: 3.14",
|
|
81
80
|
],
|
|
82
81
|
# Needed to bundle py.typed so mypy can see it:
|
|
83
82
|
zip_safe=False,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import click
|
|
3
3
|
from click_default_group import DefaultGroup # type: ignore
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
import hashlib
|
|
6
6
|
import pathlib
|
|
7
7
|
from runpy import run_module
|
|
@@ -962,7 +962,7 @@ def insert_upsert_implementation(
|
|
|
962
962
|
db = sqlite_utils.Database(path)
|
|
963
963
|
_load_extensions(db, load_extension)
|
|
964
964
|
if functions:
|
|
965
|
-
|
|
965
|
+
_register_functions_from_multiple(db, functions)
|
|
966
966
|
if (delimiter or quotechar or sniff or no_headers) and not tsv:
|
|
967
967
|
csv = True
|
|
968
968
|
if (nl + csv + tsv) >= 2:
|
|
@@ -1370,7 +1370,9 @@ def upsert(
|
|
|
1370
1370
|
@click.argument("file", type=click.File("rb"), required=True)
|
|
1371
1371
|
@click.option("--batch-size", type=int, default=100, help="Commit every X records")
|
|
1372
1372
|
@click.option(
|
|
1373
|
-
"--functions",
|
|
1373
|
+
"--functions",
|
|
1374
|
+
help="Python code or file path defining custom SQL functions",
|
|
1375
|
+
multiple=True,
|
|
1374
1376
|
)
|
|
1375
1377
|
@import_options
|
|
1376
1378
|
@load_extension_option
|
|
@@ -1759,7 +1761,9 @@ def drop_view(path, view, ignore, load_extension):
|
|
|
1759
1761
|
help="Named :parameters for SQL query",
|
|
1760
1762
|
)
|
|
1761
1763
|
@click.option(
|
|
1762
|
-
"--functions",
|
|
1764
|
+
"--functions",
|
|
1765
|
+
help="Python code or file path defining custom SQL functions",
|
|
1766
|
+
multiple=True,
|
|
1763
1767
|
)
|
|
1764
1768
|
@load_extension_option
|
|
1765
1769
|
def query(
|
|
@@ -1796,7 +1800,7 @@ def query(
|
|
|
1796
1800
|
db.register_fts4_bm25()
|
|
1797
1801
|
|
|
1798
1802
|
if functions:
|
|
1799
|
-
|
|
1803
|
+
_register_functions_from_multiple(db, functions)
|
|
1800
1804
|
|
|
1801
1805
|
_execute_query(
|
|
1802
1806
|
db,
|
|
@@ -1824,7 +1828,9 @@ def query(
|
|
|
1824
1828
|
)
|
|
1825
1829
|
@click.argument("sql")
|
|
1826
1830
|
@click.option(
|
|
1827
|
-
"--functions",
|
|
1831
|
+
"--functions",
|
|
1832
|
+
help="Python code or file path defining custom SQL functions",
|
|
1833
|
+
multiple=True,
|
|
1828
1834
|
)
|
|
1829
1835
|
@click.option(
|
|
1830
1836
|
"--attach",
|
|
@@ -1996,7 +2002,7 @@ def memory(
|
|
|
1996
2002
|
db.register_fts4_bm25()
|
|
1997
2003
|
|
|
1998
2004
|
if functions:
|
|
1999
|
-
|
|
2005
|
+
_register_functions_from_multiple(db, functions)
|
|
2000
2006
|
|
|
2001
2007
|
if return_db:
|
|
2002
2008
|
return db
|
|
@@ -3203,8 +3209,12 @@ FILE_COLUMNS = {
|
|
|
3203
3209
|
"ctime": lambda p: p.stat().st_ctime,
|
|
3204
3210
|
"mtime_int": lambda p: int(p.stat().st_mtime),
|
|
3205
3211
|
"ctime_int": lambda p: int(p.stat().st_ctime),
|
|
3206
|
-
"mtime_iso": lambda p: datetime.
|
|
3207
|
-
|
|
3212
|
+
"mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc)
|
|
3213
|
+
.replace(tzinfo=None)
|
|
3214
|
+
.isoformat(),
|
|
3215
|
+
"ctime_iso": lambda p: datetime.fromtimestamp(p.stat().st_ctime, timezone.utc)
|
|
3216
|
+
.replace(tzinfo=None)
|
|
3217
|
+
.isoformat(),
|
|
3208
3218
|
"size": lambda p: p.stat().st_size,
|
|
3209
3219
|
"stem": lambda p: p.stem,
|
|
3210
3220
|
"suffix": lambda p: p.suffix,
|
|
@@ -3281,6 +3291,13 @@ def _load_extensions(db, load_extension):
|
|
|
3281
3291
|
|
|
3282
3292
|
def _register_functions(db, functions):
|
|
3283
3293
|
# Register any Python functions as SQL functions:
|
|
3294
|
+
# Check if this is a file path
|
|
3295
|
+
if "\n" not in functions and functions.endswith(".py"):
|
|
3296
|
+
try:
|
|
3297
|
+
functions = pathlib.Path(functions).read_text()
|
|
3298
|
+
except FileNotFoundError:
|
|
3299
|
+
raise click.ClickException("File not found: {}".format(functions))
|
|
3300
|
+
|
|
3284
3301
|
sqlite3.enable_callback_tracebacks(True)
|
|
3285
3302
|
globals = {}
|
|
3286
3303
|
try:
|
|
@@ -3291,3 +3308,12 @@ def _register_functions(db, functions):
|
|
|
3291
3308
|
for name, value in globals.items():
|
|
3292
3309
|
if callable(value) and not name.startswith("_"):
|
|
3293
3310
|
db.register_function(value, name=name)
|
|
3311
|
+
|
|
3312
|
+
|
|
3313
|
+
def _register_functions_from_multiple(db, functions_list):
|
|
3314
|
+
"""Register functions from multiple --functions arguments."""
|
|
3315
|
+
if not functions_list:
|
|
3316
|
+
return
|
|
3317
|
+
for functions in functions_list:
|
|
3318
|
+
if isinstance(functions, str) and functions.strip():
|
|
3319
|
+
_register_functions(db, functions)
|
|
@@ -237,36 +237,43 @@ if pd:
|
|
|
237
237
|
|
|
238
238
|
class AlterError(Exception):
|
|
239
239
|
"Error altering table"
|
|
240
|
+
|
|
240
241
|
pass
|
|
241
242
|
|
|
242
243
|
|
|
243
244
|
class NoObviousTable(Exception):
|
|
244
245
|
"Could not tell which table this operation refers to"
|
|
246
|
+
|
|
245
247
|
pass
|
|
246
248
|
|
|
247
249
|
|
|
248
250
|
class NoTable(Exception):
|
|
249
251
|
"Specified table does not exist"
|
|
252
|
+
|
|
250
253
|
pass
|
|
251
254
|
|
|
252
255
|
|
|
253
256
|
class BadPrimaryKey(Exception):
|
|
254
257
|
"Table does not have a single obvious primary key"
|
|
258
|
+
|
|
255
259
|
pass
|
|
256
260
|
|
|
257
261
|
|
|
258
262
|
class NotFoundError(Exception):
|
|
259
263
|
"Record not found"
|
|
264
|
+
|
|
260
265
|
pass
|
|
261
266
|
|
|
262
267
|
|
|
263
268
|
class PrimaryKeyRequired(Exception):
|
|
264
269
|
"Primary key needs to be specified"
|
|
270
|
+
|
|
265
271
|
pass
|
|
266
272
|
|
|
267
273
|
|
|
268
274
|
class InvalidColumns(Exception):
|
|
269
275
|
"Specified columns do not exist"
|
|
276
|
+
|
|
270
277
|
pass
|
|
271
278
|
|
|
272
279
|
|
|
@@ -368,7 +375,7 @@ class Database:
|
|
|
368
375
|
pm.hook.prepare_connection(conn=self.conn)
|
|
369
376
|
self.strict = strict
|
|
370
377
|
|
|
371
|
-
def close(self):
|
|
378
|
+
def close(self) -> None:
|
|
372
379
|
"Close the SQLite connection, and the underlying database file"
|
|
373
380
|
self.conn.close()
|
|
374
381
|
|
|
@@ -3203,7 +3210,7 @@ class Table(Queryable):
|
|
|
3203
3210
|
:param not_null: Set of strings specifying columns that should be ``NOT NULL``.
|
|
3204
3211
|
:param defaults: Dictionary specifying default values for specific columns.
|
|
3205
3212
|
:param hash_id: Name of a column to create and use as a primary key, where the
|
|
3206
|
-
value of
|
|
3213
|
+
value of that primary key will be derived as a SHA1 hash of the other column values
|
|
3207
3214
|
in the record. ``hash_id="id"`` is a common column name used for this.
|
|
3208
3215
|
:param alter: Boolean, should any missing columns be added automatically?
|
|
3209
3216
|
:param ignore: Boolean, if a record already exists with this primary key, ignore this insert.
|
|
@@ -3852,7 +3859,7 @@ def jsonify_if_needed(value):
|
|
|
3852
3859
|
|
|
3853
3860
|
|
|
3854
3861
|
def resolve_extracts(
|
|
3855
|
-
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]]
|
|
3862
|
+
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]],
|
|
3856
3863
|
) -> dict:
|
|
3857
3864
|
if extracts is None:
|
|
3858
3865
|
extracts = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.39
|
|
4
4
|
Summary: CLI tool and Python library for manipulating SQLite databases
|
|
5
5
|
Home-page: https://github.com/simonw/sqlite-utils
|
|
6
6
|
Author: Simon Willison
|
|
@@ -15,22 +15,21 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
17
17
|
Classifier: Topic :: Database
|
|
18
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
26
24
|
Description-Content-Type: text/markdown
|
|
27
25
|
License-File: LICENSE
|
|
28
26
|
Requires-Dist: sqlite-fts4
|
|
29
|
-
Requires-Dist: click
|
|
27
|
+
Requires-Dist: click>=8.3.1
|
|
30
28
|
Requires-Dist: click-default-group>=1.2.3
|
|
31
29
|
Requires-Dist: tabulate
|
|
32
30
|
Requires-Dist: python-dateutil
|
|
33
31
|
Requires-Dist: pluggy
|
|
32
|
+
Requires-Dist: pip
|
|
34
33
|
Provides-Extra: test
|
|
35
34
|
Requires-Dist: pytest; extra == "test"
|
|
36
35
|
Requires-Dist: black>=24.1.1; extra == "test"
|
|
@@ -54,6 +53,18 @@ Provides-Extra: flake8
|
|
|
54
53
|
Requires-Dist: flake8; extra == "flake8"
|
|
55
54
|
Provides-Extra: tui
|
|
56
55
|
Requires-Dist: trogon; extra == "tui"
|
|
56
|
+
Dynamic: author
|
|
57
|
+
Dynamic: classifier
|
|
58
|
+
Dynamic: description
|
|
59
|
+
Dynamic: description-content-type
|
|
60
|
+
Dynamic: home-page
|
|
61
|
+
Dynamic: license
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
Dynamic: project-url
|
|
64
|
+
Dynamic: provides-extra
|
|
65
|
+
Dynamic: requires-dist
|
|
66
|
+
Dynamic: requires-python
|
|
67
|
+
Dynamic: summary
|
|
57
68
|
|
|
58
69
|
# sqlite-utils
|
|
59
70
|
|
|
@@ -806,6 +806,77 @@ def test_hidden_functions_are_hidden(db_path):
|
|
|
806
806
|
assert "_two" not in functions
|
|
807
807
|
|
|
808
808
|
|
|
809
|
+
def test_query_functions_from_file(db_path, tmp_path):
|
|
810
|
+
# Create a temporary file with function definitions
|
|
811
|
+
functions_file = tmp_path / "my_functions.py"
|
|
812
|
+
functions_file.write_text(TEST_FUNCTIONS)
|
|
813
|
+
|
|
814
|
+
result = CliRunner().invoke(
|
|
815
|
+
cli.cli,
|
|
816
|
+
[
|
|
817
|
+
db_path,
|
|
818
|
+
"select zero(), one(1), two(1, 2)",
|
|
819
|
+
"--functions",
|
|
820
|
+
str(functions_file),
|
|
821
|
+
],
|
|
822
|
+
)
|
|
823
|
+
assert result.exit_code == 0
|
|
824
|
+
assert json.loads(result.output.strip()) == [
|
|
825
|
+
{"zero()": 0, "one(1)": 1, "two(1, 2)": 3}
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def test_query_functions_file_not_found(db_path):
|
|
830
|
+
result = CliRunner().invoke(
|
|
831
|
+
cli.cli,
|
|
832
|
+
[
|
|
833
|
+
db_path,
|
|
834
|
+
"select zero()",
|
|
835
|
+
"--functions",
|
|
836
|
+
"nonexistent.py",
|
|
837
|
+
],
|
|
838
|
+
)
|
|
839
|
+
assert result.exit_code == 1
|
|
840
|
+
assert "File not found: nonexistent.py" in result.output
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def test_query_functions_multiple_invocations(db_path):
|
|
844
|
+
# Test using --functions multiple times
|
|
845
|
+
result = CliRunner().invoke(
|
|
846
|
+
cli.cli,
|
|
847
|
+
[
|
|
848
|
+
db_path,
|
|
849
|
+
"select triple(2), quadruple(2)",
|
|
850
|
+
"--functions",
|
|
851
|
+
"def triple(x):\n return x * 3",
|
|
852
|
+
"--functions",
|
|
853
|
+
"def quadruple(x):\n return x * 4",
|
|
854
|
+
],
|
|
855
|
+
)
|
|
856
|
+
assert result.exit_code == 0
|
|
857
|
+
assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}]
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def test_query_functions_file_and_inline(db_path, tmp_path):
|
|
861
|
+
# Test combining file and inline code
|
|
862
|
+
functions_file = tmp_path / "file_funcs.py"
|
|
863
|
+
functions_file.write_text("def triple(x):\n return x * 3")
|
|
864
|
+
|
|
865
|
+
result = CliRunner().invoke(
|
|
866
|
+
cli.cli,
|
|
867
|
+
[
|
|
868
|
+
db_path,
|
|
869
|
+
"select triple(2), quadruple(2)",
|
|
870
|
+
"--functions",
|
|
871
|
+
str(functions_file),
|
|
872
|
+
"--functions",
|
|
873
|
+
"def quadruple(x):\n return x * 4",
|
|
874
|
+
],
|
|
875
|
+
)
|
|
876
|
+
assert result.exit_code == 0
|
|
877
|
+
assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}]
|
|
878
|
+
|
|
879
|
+
|
|
809
880
|
LOREM_IPSUM_COMPRESSED = (
|
|
810
881
|
b"x\x9c\xed\xd1\xcdq\x03!\x0c\x05\xe0\xbb\xabP\x01\x1eW\x91\xdc|M\x01\n\xc8\x8e"
|
|
811
882
|
b"f\xf83H\x1e\x97\x1f\x91M\x8e\xe9\xe0\xdd\x96\x05\x84\xf4\xbek\x9fRI\xc7\xf2J"
|
|
@@ -916,7 +987,7 @@ def test_query_json_with_json_cols(db_path):
|
|
|
916
987
|
|
|
917
988
|
@pytest.mark.parametrize(
|
|
918
989
|
"content,is_binary",
|
|
919
|
-
[(b"\x00\
|
|
990
|
+
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
|
920
991
|
)
|
|
921
992
|
def test_query_raw(db_path, content, is_binary):
|
|
922
993
|
Database(db_path)["files"].insert({"content": content})
|
|
@@ -931,7 +1002,7 @@ def test_query_raw(db_path, content, is_binary):
|
|
|
931
1002
|
|
|
932
1003
|
@pytest.mark.parametrize(
|
|
933
1004
|
"content,is_binary",
|
|
934
|
-
[(b"\x00\
|
|
1005
|
+
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
|
935
1006
|
)
|
|
936
1007
|
def test_query_raw_lines(db_path, content, is_binary):
|
|
937
1008
|
Database(db_path)["files"].insert_all({"content": content} for _ in range(3))
|
|
@@ -45,6 +45,32 @@ def test_cli_bulk(test_db_and_path):
|
|
|
45
45
|
] == list(db["example"].rows)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def test_cli_bulk_multiple_functions(test_db_and_path):
|
|
49
|
+
db, db_path = test_db_and_path
|
|
50
|
+
result = CliRunner().invoke(
|
|
51
|
+
cli.cli,
|
|
52
|
+
[
|
|
53
|
+
"bulk",
|
|
54
|
+
db_path,
|
|
55
|
+
"insert into example (id, name) values (:id, myupper(mylower(:name)))",
|
|
56
|
+
"-",
|
|
57
|
+
"--nl",
|
|
58
|
+
"--functions",
|
|
59
|
+
"myupper = lambda s: s.upper()",
|
|
60
|
+
"--functions",
|
|
61
|
+
"mylower = lambda s: s.lower()",
|
|
62
|
+
],
|
|
63
|
+
input='{"id": 3, "name": "ThReE"}\n{"id": 4, "name": "FoUr"}\n',
|
|
64
|
+
)
|
|
65
|
+
assert result.exit_code == 0, result.output
|
|
66
|
+
assert [
|
|
67
|
+
{"id": 1, "name": "One"},
|
|
68
|
+
{"id": 2, "name": "Two"},
|
|
69
|
+
{"id": 3, "name": "THREE"},
|
|
70
|
+
{"id": 4, "name": "FOUR"},
|
|
71
|
+
] == list(db["example"].rows)
|
|
72
|
+
|
|
73
|
+
|
|
48
74
|
def test_cli_bulk_batch_size(test_db_and_path):
|
|
49
75
|
db, db_path = test_db_and_path
|
|
50
76
|
proc = subprocess.Popen(
|
|
@@ -307,6 +307,22 @@ def test_memory_functions():
|
|
|
307
307
|
assert result.output.strip() == '[{"hello()": "Hello"}]'
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
def test_memory_functions_multiple():
|
|
311
|
+
result = CliRunner().invoke(
|
|
312
|
+
cli.cli,
|
|
313
|
+
[
|
|
314
|
+
"memory",
|
|
315
|
+
"select triple(2), quadruple(2)",
|
|
316
|
+
"--functions",
|
|
317
|
+
"def triple(x):\n return x * 3",
|
|
318
|
+
"--functions",
|
|
319
|
+
"def quadruple(x):\n return x * 4",
|
|
320
|
+
],
|
|
321
|
+
)
|
|
322
|
+
assert result.exit_code == 0
|
|
323
|
+
assert result.output.strip() == '[{"triple(2)": 6, "quadruple(2)": 8}]'
|
|
324
|
+
|
|
325
|
+
|
|
310
326
|
def test_memory_return_db(tmpdir):
|
|
311
327
|
# https://github.com/simonw/sqlite-utils/issues/643
|
|
312
328
|
from sqlite_utils.cli import cli
|
|
@@ -64,6 +64,7 @@ def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected):
|
|
|
64
64
|
|
|
65
65
|
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
|
66
66
|
@pytest.mark.parametrize("errors", (None, recipes.SET_NULL, recipes.IGNORE))
|
|
67
|
+
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
|
|
67
68
|
def test_dateparse_errors(fresh_db, fn, errors):
|
|
68
69
|
fresh_db["example"].insert_all(
|
|
69
70
|
[
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|