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.
Files changed (77) hide show
  1. {sqlite_utils-3.38/sqlite_utils.egg-info → sqlite_utils-3.39}/PKG-INFO +18 -7
  2. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/changelog.rst +11 -0
  3. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/cli-reference.rst +4 -4
  4. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/cli.rst +17 -1
  5. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/python-api.rst +1 -3
  6. {sqlite_utils-3.38 → sqlite_utils-3.39}/setup.py +5 -6
  7. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/cli.py +35 -9
  8. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/db.py +10 -3
  9. {sqlite_utils-3.38 → sqlite_utils-3.39/sqlite_utils.egg-info}/PKG-INFO +18 -7
  10. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/requires.txt +2 -1
  11. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli.py +73 -2
  12. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_bulk.py +26 -0
  13. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_memory.py +16 -0
  14. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_recipes.py +1 -0
  15. {sqlite_utils-3.38 → sqlite_utils-3.39}/LICENSE +0 -0
  16. {sqlite_utils-3.38 → sqlite_utils-3.39}/MANIFEST.in +0 -0
  17. {sqlite_utils-3.38 → sqlite_utils-3.39}/README.md +0 -0
  18. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/contributing.rst +0 -0
  19. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/index.rst +0 -0
  20. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/installation.rst +0 -0
  21. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/plugins.rst +0 -0
  22. {sqlite_utils-3.38 → sqlite_utils-3.39}/docs/reference.rst +0 -0
  23. {sqlite_utils-3.38 → sqlite_utils-3.39}/setup.cfg +0 -0
  24. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/__init__.py +0 -0
  25. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/__main__.py +0 -0
  26. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/hookspecs.py +0 -0
  27. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/plugins.py +0 -0
  28. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/py.typed +0 -0
  29. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/recipes.py +0 -0
  30. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils/utils.py +0 -0
  31. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/SOURCES.txt +0 -0
  32. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/dependency_links.txt +0 -0
  33. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/entry_points.txt +0 -0
  34. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/not-zip-safe +0 -0
  35. {sqlite_utils-3.38 → sqlite_utils-3.39}/sqlite_utils.egg-info/top_level.txt +0 -0
  36. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/__init__.py +0 -0
  37. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/conftest.py +0 -0
  38. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_analyze.py +0 -0
  39. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_analyze_tables.py +0 -0
  40. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_attach.py +0 -0
  41. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_convert.py +0 -0
  42. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_cli_insert.py +0 -0
  43. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_column_affinity.py +0 -0
  44. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_constructor.py +0 -0
  45. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_conversions.py +0 -0
  46. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_convert.py +0 -0
  47. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_create.py +0 -0
  48. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_create_view.py +0 -0
  49. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_default_value.py +0 -0
  50. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_delete.py +0 -0
  51. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_docs.py +0 -0
  52. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_duplicate.py +0 -0
  53. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_enable_counts.py +0 -0
  54. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_extract.py +0 -0
  55. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_extracts.py +0 -0
  56. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_fts.py +0 -0
  57. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_get.py +0 -0
  58. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_gis.py +0 -0
  59. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_hypothesis.py +0 -0
  60. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_insert_files.py +0 -0
  61. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_introspect.py +0 -0
  62. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_lookup.py +0 -0
  63. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_m2m.py +0 -0
  64. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_plugins.py +0 -0
  65. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_query.py +0 -0
  66. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_recreate.py +0 -0
  67. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_register_function.py +0 -0
  68. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_rows.py +0 -0
  69. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_rows_from_file.py +0 -0
  70. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_sniff.py +0 -0
  71. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_suggest_column_types.py +0 -0
  72. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_tracer.py +0 -0
  73. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_transform.py +0 -0
  74. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_update.py +0 -0
  75. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_upsert.py +0 -0
  76. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_utils.py +0 -0
  77. {sqlite_utils-3.38 → sqlite_utils-3.39}/tests/test_wal.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sqlite-utils
3
- Version: 3.38
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
- Requires-Python: >=3.8
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 defining one or more custom SQL
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 defining one or more custom SQL
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 defining one or more custom SQL functions
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 [POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION|GEOMETRY]
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 `newline-delimited JSON <http://ndjson.org/>`__ using the ``--nl`` option:
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
- Python 3.8 added the ability to register `deterministic SQLite functions <https://sqlite.org/deterministic.html>`__, 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:
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.38"
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.8",
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
- _register_functions(db, functions)
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", help="Python code defining one or more custom SQL 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", help="Python code defining one or more custom SQL 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
- _register_functions(db, functions)
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", help="Python code defining one or more custom SQL 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
- _register_functions(db, functions)
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.utcfromtimestamp(p.stat().st_mtime).isoformat(),
3207
- "ctime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_ctime).isoformat(),
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 thet primary key will be derived as a SHA1 hash of the other column values
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
1
+ Metadata-Version: 2.4
2
2
  Name: sqlite-utils
3
- Version: 3.38
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
- Requires-Python: >=3.8
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
 
@@ -1,9 +1,10 @@
1
1
  sqlite-fts4
2
- click
2
+ click>=8.3.1
3
3
  click-default-group>=1.2.3
4
4
  tabulate
5
5
  python-dateutil
6
6
  pluggy
7
+ pip
7
8
 
8
9
  [docs]
9
10
  furo
@@ -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\x0Fbinary", True), ("this is text", False), (1, False), (1.5, False)],
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\x0Fbinary", True), ("this is text", False), (1, False), (1.5, False)],
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