sqlite-utils 3.36__tar.gz → 3.38a0__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.36/sqlite_utils.egg-info → sqlite_utils-3.38a0}/PKG-INFO +4 -4
  2. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/changelog.rst +9 -0
  3. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/cli-reference.rst +19 -18
  4. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/cli.rst +6 -2
  5. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/plugins.rst +20 -0
  6. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/python-api.rst +3 -2
  7. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/setup.py +4 -4
  8. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/cli.py +25 -14
  9. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/db.py +38 -26
  10. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/utils.py +1 -4
  11. {sqlite-utils-3.36 → sqlite_utils-3.38a0/sqlite_utils.egg-info}/PKG-INFO +4 -4
  12. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils.egg-info/requires.txt +1 -1
  13. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_cli.py +16 -10
  14. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_cli_insert.py +10 -5
  15. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_cli_memory.py +14 -1
  16. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_insert_files.py +10 -2
  17. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/LICENSE +0 -0
  18. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/MANIFEST.in +0 -0
  19. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/README.md +0 -0
  20. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/contributing.rst +0 -0
  21. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/index.rst +0 -0
  22. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/installation.rst +0 -0
  23. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/docs/reference.rst +0 -0
  24. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/setup.cfg +0 -0
  25. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/__init__.py +0 -0
  26. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/__main__.py +0 -0
  27. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/hookspecs.py +0 -0
  28. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/plugins.py +0 -0
  29. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/py.typed +0 -0
  30. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils/recipes.py +0 -0
  31. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils.egg-info/SOURCES.txt +0 -0
  32. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils.egg-info/dependency_links.txt +0 -0
  33. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils.egg-info/entry_points.txt +0 -0
  34. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils.egg-info/not-zip-safe +0 -0
  35. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/sqlite_utils.egg-info/top_level.txt +0 -0
  36. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/__init__.py +0 -0
  37. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/conftest.py +0 -0
  38. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_analyze.py +0 -0
  39. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_analyze_tables.py +0 -0
  40. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_attach.py +0 -0
  41. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_cli_bulk.py +0 -0
  42. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_cli_convert.py +0 -0
  43. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_column_affinity.py +0 -0
  44. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_constructor.py +0 -0
  45. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_conversions.py +0 -0
  46. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_convert.py +0 -0
  47. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_create.py +0 -0
  48. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_create_view.py +0 -0
  49. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_default_value.py +0 -0
  50. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_delete.py +0 -0
  51. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_docs.py +0 -0
  52. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_duplicate.py +0 -0
  53. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_enable_counts.py +0 -0
  54. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_extract.py +0 -0
  55. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_extracts.py +0 -0
  56. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_fts.py +0 -0
  57. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_get.py +0 -0
  58. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_gis.py +0 -0
  59. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_hypothesis.py +0 -0
  60. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_introspect.py +0 -0
  61. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_lookup.py +0 -0
  62. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_m2m.py +0 -0
  63. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_plugins.py +0 -0
  64. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_query.py +0 -0
  65. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_recipes.py +0 -0
  66. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_recreate.py +0 -0
  67. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_register_function.py +0 -0
  68. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_rows.py +0 -0
  69. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_rows_from_file.py +0 -0
  70. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_sniff.py +0 -0
  71. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_suggest_column_types.py +0 -0
  72. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_tracer.py +0 -0
  73. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_transform.py +0 -0
  74. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_update.py +0 -0
  75. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_upsert.py +0 -0
  76. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_utils.py +0 -0
  77. {sqlite-utils-3.36 → sqlite_utils-3.38a0}/tests/test_wal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlite-utils
3
- Version: 3.36
3
+ Version: 3.38a0
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
@@ -16,13 +16,13 @@ Classifier: Intended Audience :: Science/Research
16
16
  Classifier: Intended Audience :: End Users/Desktop
17
17
  Classifier: Topic :: Database
18
18
  Classifier: License :: OSI Approved :: Apache Software License
19
- Classifier: Programming Language :: Python :: 3.7
20
19
  Classifier: Programming Language :: Python :: 3.8
21
20
  Classifier: Programming Language :: Python :: 3.9
22
21
  Classifier: Programming Language :: Python :: 3.10
23
22
  Classifier: Programming Language :: Python :: 3.11
24
23
  Classifier: Programming Language :: Python :: 3.12
25
- Requires-Python: >=3.7
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Requires-Python: >=3.8
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: sqlite-fts4
@@ -33,7 +33,7 @@ Requires-Dist: python-dateutil
33
33
  Requires-Dist: pluggy
34
34
  Provides-Extra: test
35
35
  Requires-Dist: pytest; extra == "test"
36
- Requires-Dist: black; extra == "test"
36
+ Requires-Dist: black>=24.1.1; extra == "test"
37
37
  Requires-Dist: hypothesis; extra == "test"
38
38
  Requires-Dist: cogapp; extra == "test"
39
39
  Provides-Extra: docs
@@ -4,6 +4,15 @@
4
4
  Changelog
5
5
  ===========
6
6
 
7
+ .. _v3_37:
8
+
9
+ 3.37 (2024-07-18)
10
+ -----------------
11
+
12
+ - The ``create-table`` and ``insert-files`` commands all now accept multiple ``--pk`` options for compound primary keys. (:issue:`620`)
13
+ - Now tested against Python 3.13 pre-release. (`#619 <https://github.com/simonw/sqlite-utils/pull/619>`__)
14
+ - Fixed a crash that can occur in environments with a broken ``numpy`` installation, producing a ``module 'numpy' has no attribute 'int8'``. (:issue:`632`)
15
+
7
16
  .. _v3_36:
8
17
 
9
18
  3.36 (2023-12-07)
@@ -85,6 +85,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command.
85
85
  cog.out("::\n\n")
86
86
  result = CliRunner().invoke(cli.cli, [command, "--help"])
87
87
  output = result.output.replace("Usage: cli ", "Usage: sqlite-utils ")
88
+ output = output.replace('\b', '')
88
89
  cog.out(textwrap.indent(output, ' '))
89
90
  cog.out("\n\n")
90
91
  .. ]]]
@@ -603,9 +604,9 @@ See :ref:`cli_convert`.
603
604
 
604
605
  Convert columns using Python code you supply. For example:
605
606
 
606
- sqlite-utils convert my.db mytable mycolumn \
607
- '"\n".join(textwrap.wrap(value, 10))' \
608
- --import=textwrap
607
+ sqlite-utils convert my.db mytable mycolumn \
608
+ '"\n".join(textwrap.wrap(value, 10))' \
609
+ --import=textwrap
609
610
 
610
611
  "value" is a variable with the column value to be converted.
611
612
 
@@ -615,30 +616,30 @@ See :ref:`cli_convert`.
615
616
 
616
617
  r.jsonsplit(value, delimiter=',', type=<class 'str'>)
617
618
 
618
- Convert a string like a,b,c into a JSON array ["a", "b", "c"]
619
+ Convert a string like a,b,c into a JSON array ["a", "b", "c"]
619
620
 
620
621
  r.parsedate(value, dayfirst=False, yearfirst=False, errors=None)
621
622
 
622
- Parse a date and convert it to ISO date format: yyyy-mm-dd
623
- 
624
- - dayfirst=True: treat xx as the day in xx/yy/zz
625
- - yearfirst=True: treat xx as the year in xx/yy/zz
626
- - errors=r.IGNORE to ignore values that cannot be parsed
627
- - errors=r.SET_NULL to set values that cannot be parsed to null
623
+ Parse a date and convert it to ISO date format: yyyy-mm-dd
624
+
625
+ - dayfirst=True: treat xx as the day in xx/yy/zz
626
+ - yearfirst=True: treat xx as the year in xx/yy/zz
627
+ - errors=r.IGNORE to ignore values that cannot be parsed
628
+ - errors=r.SET_NULL to set values that cannot be parsed to null
628
629
 
629
630
  r.parsedatetime(value, dayfirst=False, yearfirst=False, errors=None)
630
631
 
631
- Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
632
- 
633
- - dayfirst=True: treat xx as the day in xx/yy/zz
634
- - yearfirst=True: treat xx as the year in xx/yy/zz
635
- - errors=r.IGNORE to ignore values that cannot be parsed
636
- - errors=r.SET_NULL to set values that cannot be parsed to null
632
+ Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
633
+
634
+ - dayfirst=True: treat xx as the day in xx/yy/zz
635
+ - yearfirst=True: treat xx as the year in xx/yy/zz
636
+ - errors=r.IGNORE to ignore values that cannot be parsed
637
+ - errors=r.SET_NULL to set values that cannot be parsed to null
637
638
 
638
639
  You can use these recipes like so:
639
640
 
640
- sqlite-utils convert my.db mytable mycolumn \
641
- 'r.jsonsplit(value, delimiter=":")'
641
+ sqlite-utils convert my.db mytable mycolumn \
642
+ 'r.jsonsplit(value, delimiter=":")'
642
643
 
643
644
  Options:
644
645
  --import TEXT Python modules to import
@@ -1088,11 +1088,13 @@ You can import all three records into an automatically created ``dogs`` table an
1088
1088
 
1089
1089
  sqlite-utils insert dogs.db dogs dogs.json --pk=id
1090
1090
 
1091
+ Pass ``--pk`` multiple times to define a compound primary key.
1092
+
1091
1093
  You can skip inserting any records that have a primary key that already exists using ``--ignore``:
1092
1094
 
1093
1095
  .. code-block:: bash
1094
1096
 
1095
- sqlite-utils insert dogs.db dogs dogs.json --ignore
1097
+ sqlite-utils insert dogs.db dogs dogs.json --pk=id --ignore
1096
1098
 
1097
1099
  You can delete all the existing rows in the table before inserting the new records using ``--truncate``:
1098
1100
 
@@ -1909,6 +1911,8 @@ This will create a table called ``mytable`` with two columns - an integer ``id``
1909
1911
 
1910
1912
  You can pass as many column-name column-type pairs as you like. Valid types are ``integer``, ``text``, ``float`` and ``blob``.
1911
1913
 
1914
+ Pass ``--pk`` more than once for a compound primary key that covers multiple columns.
1915
+
1912
1916
  You can specify columns that should be NOT NULL using ``--not-null colname``. You can specify default values for columns using ``--default colname defaultvalue``.
1913
1917
 
1914
1918
  .. code-block:: bash
@@ -2080,7 +2084,7 @@ Every option for this table (with the exception of ``--pk-none``) can be specifi
2080
2084
  ``--drop-foreign-key column``
2081
2085
  Drop the specified foreign key.
2082
2086
 
2083
- ``--add-foregn-key column other_table other_column``
2087
+ ``--add-foreign-key column other_table other_column``
2084
2088
  Add a foreign key constraint to ``column`` pointing to ``other_table.other_column``.
2085
2089
 
2086
2090
  If you want to see the SQL that will be executed to make the change without actually executing it, add the ``--sql`` flag. For example:
@@ -115,6 +115,26 @@ Example implementation:
115
115
  "Say hello world"
116
116
  click.echo("Hello world!")
117
117
 
118
+ New commands implemented by plugins can invoke existing commands using the `context.invoke <https://click.palletsprojects.com/en/stable/api/#click.Context.invoke>`__ mechanism.
119
+
120
+ As a special niche feature, if your plugin needs to import some files and then act against an in-memory database containing those files you can forward to the :ref:`sqlite-utils memory command <cli_memory>` and pass it ``return_db=True``:
121
+
122
+ .. code-block:: python
123
+
124
+ @cli.command()
125
+ @click.pass_context
126
+ @click.argument(
127
+ "paths",
128
+ type=click.Path(file_okay=True, dir_okay=False, allow_dash=True),
129
+ required=False,
130
+ nargs=-1,
131
+ )
132
+ def show_schema_for_files(ctx, paths):
133
+ from sqlite_utils.cli import memory
134
+ db = ctx.invoke(memory, paths=paths, return_db=True)
135
+ # Now do something with that database
136
+ click.echo(db.schema)
137
+
118
138
  .. _plugins_hooks_prepare_connection:
119
139
 
120
140
  prepare_connection(conn)
@@ -890,7 +890,8 @@ You can delete all records in a table that match a specific WHERE statement usin
890
890
 
891
891
  >>> db = sqlite_utils.Database("dogs.db")
892
892
  >>> # Delete every dog with age less than 3
893
- >>> db["dogs"].delete_where("age < ?", [3])
893
+ >>> with db.conn:
894
+ >>> db["dogs"].delete_where("age < ?", [3])
894
895
 
895
896
  Calling ``table.delete_where()`` with no other arguments will delete every row in the table.
896
897
 
@@ -2177,7 +2178,7 @@ The ``.has_counts_triggers`` property shows if a table has been configured with
2177
2178
  >>> db["authors"].has_counts_triggers
2178
2179
  True
2179
2180
 
2180
- .. _python_api_introspection_supports_strict
2181
+ .. _python_api_introspection_supports_strict:
2181
2182
 
2182
2183
  db.supports_strict
2183
2184
  ------------------
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
  import io
3
3
  import os
4
4
 
5
- VERSION = "3.36"
5
+ VERSION = "3.38a0"
6
6
 
7
7
 
8
8
  def get_long_description():
@@ -32,7 +32,7 @@ setup(
32
32
  "pluggy",
33
33
  ],
34
34
  extras_require={
35
- "test": ["pytest", "black", "hypothesis", "cogapp"],
35
+ "test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
36
36
  "docs": [
37
37
  "furo",
38
38
  "sphinx-autobuild",
@@ -64,7 +64,7 @@ setup(
64
64
  "Issues": "https://github.com/simonw/sqlite-utils/issues",
65
65
  "CI": "https://github.com/simonw/sqlite-utils/actions",
66
66
  },
67
- python_requires=">=3.7",
67
+ python_requires=">=3.8",
68
68
  classifiers=[
69
69
  "Development Status :: 5 - Production/Stable",
70
70
  "Intended Audience :: Developers",
@@ -72,12 +72,12 @@ setup(
72
72
  "Intended Audience :: End Users/Desktop",
73
73
  "Topic :: Database",
74
74
  "License :: OSI Approved :: Apache Software License",
75
- "Programming Language :: Python :: 3.7",
76
75
  "Programming Language :: Python :: 3.8",
77
76
  "Programming Language :: Python :: 3.9",
78
77
  "Programming Language :: Python :: 3.10",
79
78
  "Programming Language :: Python :: 3.11",
80
79
  "Programming Language :: Python :: 3.12",
80
+ "Programming Language :: Python :: 3.13",
81
81
  ],
82
82
  # Needed to bundle py.typed so mypy can see it:
83
83
  zip_safe=False,
@@ -1489,7 +1489,7 @@ def create_database(path, enable_wal, init_spatialite, load_extension):
1489
1489
  )
1490
1490
  @click.argument("table")
1491
1491
  @click.argument("columns", nargs=-1, required=True)
1492
- @click.option("--pk", help="Column to use as primary key")
1492
+ @click.option("pks", "--pk", help="Column to use as primary key", multiple=True)
1493
1493
  @click.option(
1494
1494
  "--not-null",
1495
1495
  multiple=True,
@@ -1532,7 +1532,7 @@ def create_table(
1532
1532
  path,
1533
1533
  table,
1534
1534
  columns,
1535
- pk,
1535
+ pks,
1536
1536
  not_null,
1537
1537
  default,
1538
1538
  fk,
@@ -1581,7 +1581,7 @@ def create_table(
1581
1581
  )
1582
1582
  db[table].create(
1583
1583
  coltypes,
1584
- pk=pk,
1584
+ pk=pks[0] if len(pks) == 1 else pks,
1585
1585
  not_null=not_null,
1586
1586
  defaults=dict(default),
1587
1587
  foreign_keys=fk,
@@ -1894,6 +1894,7 @@ def memory(
1894
1894
  save,
1895
1895
  analyze,
1896
1896
  load_extension,
1897
+ return_db=False,
1897
1898
  ):
1898
1899
  """Execute SQL query against an in-memory database, optionally populated by imported data
1899
1900
 
@@ -1922,6 +1923,7 @@ def memory(
1922
1923
  sqlite-utils memory animals.csv --schema
1923
1924
  """
1924
1925
  db = sqlite_utils.Database(memory=True)
1926
+
1925
1927
  # If --dump or --save or --analyze used but no paths detected, assume SQL query is a path:
1926
1928
  if (dump or save or schema or analyze) and not paths:
1927
1929
  paths = [sql]
@@ -1954,6 +1956,7 @@ def memory(
1954
1956
  rows = tracker.wrap(rows)
1955
1957
  if flatten:
1956
1958
  rows = (_flatten(row) for row in rows)
1959
+
1957
1960
  db[file_table].insert_all(rows, alter=True)
1958
1961
  if tracker is not None:
1959
1962
  db[file_table].transform(types=tracker.types)
@@ -1964,6 +1967,7 @@ def memory(
1964
1967
  for view_name in view_names:
1965
1968
  if not db[view_name].exists():
1966
1969
  db.create_view(view_name, "select * from [{}]".format(file_table))
1970
+
1967
1971
  if fp:
1968
1972
  fp.close()
1969
1973
 
@@ -1994,6 +1998,9 @@ def memory(
1994
1998
  if functions:
1995
1999
  _register_functions(db, functions)
1996
2000
 
2001
+ if return_db:
2002
+ return db
2003
+
1997
2004
  _execute_query(
1998
2005
  db,
1999
2006
  sql,
@@ -2594,7 +2601,7 @@ def extract(
2594
2601
  multiple=True,
2595
2602
  help="Column definitions for the table",
2596
2603
  )
2597
- @click.option("--pk", type=str, help="Column to use as primary key")
2604
+ @click.option("pks", "--pk", help="Column to use as primary key", multiple=True)
2598
2605
  @click.option("--alter", is_flag=True, help="Alter table to add missing columns")
2599
2606
  @click.option("--replace", is_flag=True, help="Replace files with matching primary key")
2600
2607
  @click.option("--upsert", is_flag=True, help="Upsert files with matching primary key")
@@ -2611,7 +2618,7 @@ def insert_files(
2611
2618
  table,
2612
2619
  file_or_dir,
2613
2620
  column,
2614
- pk,
2621
+ pks,
2615
2622
  alter,
2616
2623
  replace,
2617
2624
  upsert,
@@ -2641,8 +2648,8 @@ def insert_files(
2641
2648
  column = ["path:path", "content_text:content_text", "size:size"]
2642
2649
  else:
2643
2650
  column = ["path:path", "content:content", "size:size"]
2644
- if not pk:
2645
- pk = "path"
2651
+ if not pks:
2652
+ pks = ["path"]
2646
2653
 
2647
2654
  def yield_paths_and_relative_paths():
2648
2655
  for f_or_d in file_or_dir:
@@ -2712,7 +2719,11 @@ def insert_files(
2712
2719
  try:
2713
2720
  with db.conn:
2714
2721
  db[table].insert_all(
2715
- to_insert(), pk=pk, alter=alter, replace=replace, upsert=upsert
2722
+ to_insert(),
2723
+ pk=pks[0] if len(pks) == 1 else pks,
2724
+ alter=alter,
2725
+ replace=replace,
2726
+ upsert=upsert,
2716
2727
  )
2717
2728
  except UnicodeDecodeErrorForPath as e:
2718
2729
  raise click.ClickException(
@@ -2871,9 +2882,9 @@ def _generate_convert_help():
2871
2882
  Convert columns using Python code you supply. For example:
2872
2883
 
2873
2884
  \b
2874
- sqlite-utils convert my.db mytable mycolumn \\
2875
- '"\\n".join(textwrap.wrap(value, 10))' \\
2876
- --import=textwrap
2885
+ sqlite-utils convert my.db mytable mycolumn \\
2886
+ '"\\n".join(textwrap.wrap(value, 10))' \\
2887
+ --import=textwrap
2877
2888
 
2878
2889
  "value" is a variable with the column value to be converted.
2879
2890
 
@@ -2892,7 +2903,7 @@ def _generate_convert_help():
2892
2903
  for name in recipe_names:
2893
2904
  fn = getattr(recipes, name)
2894
2905
  help += "\n\nr.{}{}\n\n\b{}".format(
2895
- name, str(inspect.signature(fn)), fn.__doc__.rstrip()
2906
+ name, str(inspect.signature(fn)), textwrap.dedent(fn.__doc__.rstrip())
2896
2907
  )
2897
2908
  help += "\n\n"
2898
2909
  help += textwrap.dedent(
@@ -2900,8 +2911,8 @@ def _generate_convert_help():
2900
2911
  You can use these recipes like so:
2901
2912
 
2902
2913
  \b
2903
- sqlite-utils convert my.db mytable mycolumn \\
2904
- 'r.jsonsplit(value, delimiter=":")'
2914
+ sqlite-utils convert my.db mytable mycolumn \\
2915
+ 'r.jsonsplit(value, delimiter=":")'
2905
2916
  """
2906
2917
  ).strip()
2907
2918
  return help
@@ -206,21 +206,25 @@ COLUMN_TYPE_MAPPING = {
206
206
  }
207
207
  # If numpy is available, add more types
208
208
  if np:
209
- COLUMN_TYPE_MAPPING.update(
210
- {
211
- np.int8: "INTEGER",
212
- np.int16: "INTEGER",
213
- np.int32: "INTEGER",
214
- np.int64: "INTEGER",
215
- np.uint8: "INTEGER",
216
- np.uint16: "INTEGER",
217
- np.uint32: "INTEGER",
218
- np.uint64: "INTEGER",
219
- np.float16: "FLOAT",
220
- np.float32: "FLOAT",
221
- np.float64: "FLOAT",
222
- }
223
- )
209
+ try:
210
+ COLUMN_TYPE_MAPPING.update(
211
+ {
212
+ np.int8: "INTEGER",
213
+ np.int16: "INTEGER",
214
+ np.int32: "INTEGER",
215
+ np.int64: "INTEGER",
216
+ np.uint8: "INTEGER",
217
+ np.uint16: "INTEGER",
218
+ np.uint32: "INTEGER",
219
+ np.uint64: "INTEGER",
220
+ np.float16: "FLOAT",
221
+ np.float32: "FLOAT",
222
+ np.float64: "FLOAT",
223
+ }
224
+ )
225
+ except AttributeError:
226
+ # https://github.com/simonw/sqlite-utils/issues/632
227
+ pass
224
228
 
225
229
  # If pandas is available, add more types
226
230
  if pd:
@@ -321,6 +325,8 @@ class Database:
321
325
  execute_plugins: bool = True,
322
326
  strict: bool = False,
323
327
  ):
328
+ self.memory_name = None
329
+ self.memory = False
324
330
  assert (filename_or_conn is not None and (not memory and not memory_name)) or (
325
331
  filename_or_conn is None and (memory or memory_name)
326
332
  ), "Either specify a filename_or_conn or pass memory=True"
@@ -331,8 +337,11 @@ class Database:
331
337
  uri=True,
332
338
  check_same_thread=False,
333
339
  )
340
+ self.memory = True
341
+ self.memory_name = memory_name
334
342
  elif memory or filename_or_conn == ":memory:":
335
343
  self.conn = sqlite3.connect(":memory:")
344
+ self.memory = True
336
345
  elif isinstance(filename_or_conn, (str, pathlib.Path)):
337
346
  if recreate and os.path.exists(filename_or_conn):
338
347
  try:
@@ -457,8 +466,7 @@ class Database:
457
466
  fn_name, arity, fn, **dict(kwargs, deterministic=True)
458
467
  )
459
468
  registered = True
460
- except (sqlite3.NotSupportedError, TypeError):
461
- # TypeError is Python 3.7 "function takes at most 3 arguments"
469
+ except sqlite3.NotSupportedError:
462
470
  pass
463
471
  if not registered:
464
472
  self.conn.create_function(fn_name, arity, fn, **kwargs)
@@ -930,9 +938,9 @@ class Database:
930
938
  " [{column_name}] {column_type}{column_extras}".format(
931
939
  column_name=column_name,
932
940
  column_type=COLUMN_TYPE_MAPPING[column_type],
933
- column_extras=(" " + " ".join(column_extras))
934
- if column_extras
935
- else "",
941
+ column_extras=(
942
+ (" " + " ".join(column_extras)) if column_extras else ""
943
+ ),
936
944
  )
937
945
  )
938
946
  extra_pk = ""
@@ -1481,9 +1489,11 @@ class Table(Queryable):
1481
1489
  def __repr__(self) -> str:
1482
1490
  return "<Table {}{}>".format(
1483
1491
  self.name,
1484
- " (does not exist yet)"
1485
- if not self.exists()
1486
- else " ({})".format(", ".join(c.name for c in self.columns)),
1492
+ (
1493
+ " (does not exist yet)"
1494
+ if not self.exists()
1495
+ else " ({})".format(", ".join(c.name for c in self.columns))
1496
+ ),
1487
1497
  )
1488
1498
 
1489
1499
  @property
@@ -2940,9 +2950,11 @@ class Table(Queryable):
2940
2950
  value = jsonify_if_needed(
2941
2951
  record.get(
2942
2952
  key,
2943
- None
2944
- if key != hash_id
2945
- else hash_record(record, hash_id_columns),
2953
+ (
2954
+ None
2955
+ if key != hash_id
2956
+ else hash_record(record, hash_id_columns)
2957
+ ),
2946
2958
  )
2947
2959
  )
2948
2960
  if key in extracts:
@@ -304,10 +304,7 @@ def rows_from_file(
304
304
  rows = rows_from_file(
305
305
  fp, format=Format.CSV, dialect=csv.excel_tab, encoding=encoding
306
306
  )[0]
307
- return (
308
- _extra_key_strategy(rows, ignore_extras, extras_key),
309
- Format.TSV,
310
- )
307
+ return _extra_key_strategy(rows, ignore_extras, extras_key), Format.TSV
311
308
  elif format is None:
312
309
  # Detect the format, then call this recursively
313
310
  buffered = io.BufferedReader(cast(io.RawIOBase, fp), buffer_size=4096)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sqlite-utils
3
- Version: 3.36
3
+ Version: 3.38a0
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
@@ -16,13 +16,13 @@ Classifier: Intended Audience :: Science/Research
16
16
  Classifier: Intended Audience :: End Users/Desktop
17
17
  Classifier: Topic :: Database
18
18
  Classifier: License :: OSI Approved :: Apache Software License
19
- Classifier: Programming Language :: Python :: 3.7
20
19
  Classifier: Programming Language :: Python :: 3.8
21
20
  Classifier: Programming Language :: Python :: 3.9
22
21
  Classifier: Programming Language :: Python :: 3.10
23
22
  Classifier: Programming Language :: Python :: 3.11
24
23
  Classifier: Programming Language :: Python :: 3.12
25
- Requires-Python: >=3.7
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Requires-Python: >=3.8
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: sqlite-fts4
@@ -33,7 +33,7 @@ Requires-Dist: python-dateutil
33
33
  Requires-Dist: pluggy
34
34
  Provides-Extra: test
35
35
  Requires-Dist: pytest; extra == "test"
36
- Requires-Dist: black; extra == "test"
36
+ Requires-Dist: black>=24.1.1; extra == "test"
37
37
  Requires-Dist: hypothesis; extra == "test"
38
38
  Requires-Dist: cogapp; extra == "test"
39
39
  Provides-Extra: docs
@@ -26,7 +26,7 @@ data-science-types
26
26
 
27
27
  [test]
28
28
  pytest
29
- black
29
+ black>=24.1.1
30
30
  hypothesis
31
31
  cogapp
32
32
 
@@ -1180,6 +1180,14 @@ def test_upsert_alter(db_path, tmpdir):
1180
1180
  ["age", "integer", "--default", "age", "3"],
1181
1181
  ("CREATE TABLE [t] (\n" " [age] INTEGER DEFAULT '3'\n" ")"),
1182
1182
  ),
1183
+ # Compound primary key
1184
+ (
1185
+ ["category", "text", "name", "text", "--pk", "category", "--pk", "name"],
1186
+ (
1187
+ "CREATE TABLE [t] (\n [category] TEXT,\n [name] TEXT,\n"
1188
+ " PRIMARY KEY ([category], [name])\n)"
1189
+ ),
1190
+ ),
1183
1191
  ],
1184
1192
  )
1185
1193
  def test_create_table(args, schema):
@@ -2082,11 +2090,10 @@ def test_schema(tmpdir, options, expected):
2082
2090
  def test_long_csv_column_value(tmpdir):
2083
2091
  db_path = str(tmpdir / "test.db")
2084
2092
  csv_path = str(tmpdir / "test.csv")
2085
- csv_file = open(csv_path, "w")
2086
- long_string = "a" * 131073
2087
- csv_file.write("id,text\n")
2088
- csv_file.write("1,{}\n".format(long_string))
2089
- csv_file.close()
2093
+ with open(csv_path, "w") as csv_file:
2094
+ long_string = "a" * 131073
2095
+ csv_file.write("id,text\n")
2096
+ csv_file.write("1,{}\n".format(long_string))
2090
2097
  result = CliRunner().invoke(
2091
2098
  cli.cli,
2092
2099
  ["insert", db_path, "bigtable", csv_path, "--csv"],
@@ -2110,11 +2117,10 @@ def test_long_csv_column_value(tmpdir):
2110
2117
  def test_import_no_headers(tmpdir, args, tsv):
2111
2118
  db_path = str(tmpdir / "test.db")
2112
2119
  csv_path = str(tmpdir / "test.csv")
2113
- csv_file = open(csv_path, "w")
2114
- sep = "\t" if tsv else ","
2115
- csv_file.write("Cleo{sep}Dog{sep}5\n".format(sep=sep))
2116
- csv_file.write("Tracy{sep}Spider{sep}7\n".format(sep=sep))
2117
- csv_file.close()
2120
+ with open(csv_path, "w") as csv_file:
2121
+ sep = "\t" if tsv else ","
2122
+ csv_file.write("Cleo{sep}Dog{sep}5\n".format(sep=sep))
2123
+ csv_file.write("Tracy{sep}Spider{sep}7\n".format(sep=sep))
2118
2124
  result = CliRunner().invoke(
2119
2125
  cli.cli,
2120
2126
  ["insert", db_path, "creatures", csv_path] + args,
@@ -77,19 +77,24 @@ def test_insert_json_flatten_nl(tmpdir):
77
77
  ]
78
78
 
79
79
 
80
- def test_insert_with_primary_key(db_path, tmpdir):
80
+ @pytest.mark.parametrize(
81
+ "args,expected_pks",
82
+ (
83
+ (["--pk", "id"], ["id"]),
84
+ (["--pk", "id", "--pk", "name"], ["id", "name"]),
85
+ ),
86
+ )
87
+ def test_insert_with_primary_keys(db_path, tmpdir, args, expected_pks):
81
88
  json_path = str(tmpdir / "dog.json")
82
89
  with open(json_path, "w") as fp:
83
90
  fp.write(json.dumps({"id": 1, "name": "Cleo", "age": 4}))
84
- result = CliRunner().invoke(
85
- cli.cli, ["insert", db_path, "dogs", json_path, "--pk", "id"]
86
- )
91
+ result = CliRunner().invoke(cli.cli, ["insert", db_path, "dogs", json_path] + args)
87
92
  assert result.exit_code == 0
88
93
  assert [{"id": 1, "age": 4, "name": "Cleo"}] == list(
89
94
  Database(db_path).query("select * from dogs")
90
95
  )
91
96
  db = Database(db_path)
92
- assert ["id"] == db["dogs"].pks
97
+ assert db["dogs"].pks == expected_pks
93
98
 
94
99
 
95
100
  def test_insert_multiple_with_primary_key(db_path, tmpdir):
@@ -1,5 +1,5 @@
1
+ import click
1
2
  import json
2
-
3
3
  import pytest
4
4
  from click.testing import CliRunner
5
5
 
@@ -305,3 +305,16 @@ def test_memory_functions():
305
305
  )
306
306
  assert result.exit_code == 0
307
307
  assert result.output.strip() == '[{"hello()": "Hello"}]'
308
+
309
+
310
+ def test_memory_return_db(tmpdir):
311
+ # https://github.com/simonw/sqlite-utils/issues/643
312
+ from sqlite_utils.cli import cli
313
+
314
+ path = str(tmpdir / "dogs.csv")
315
+ open(path, "w").write("id,name\n1,Cleo")
316
+
317
+ with click.Context(cli) as ctx:
318
+ db = ctx.invoke(cli.commands["memory"], paths=(path,), return_db=True)
319
+
320
+ assert db.table_names() == ["dogs"]
@@ -7,7 +7,14 @@ import sys
7
7
 
8
8
 
9
9
  @pytest.mark.parametrize("silent", (False, True))
10
- def test_insert_files(silent):
10
+ @pytest.mark.parametrize(
11
+ "pk_args,expected_pks",
12
+ (
13
+ (["--pk", "path"], ["path"]),
14
+ (["--pk", "path", "--pk", "name"], ["path", "name"]),
15
+ ),
16
+ )
17
+ def test_insert_files(silent, pk_args, expected_pks):
11
18
  runner = CliRunner()
12
19
  with runner.isolated_filesystem():
13
20
  tmpdir = pathlib.Path(".")
@@ -42,7 +49,7 @@ def test_insert_files(silent):
42
49
  cli.cli,
43
50
  ["insert-files", db_path, "files", str(tmpdir)]
44
51
  + cols
45
- + ["--pk", "path"]
52
+ + pk_args
46
53
  + (["--silent"] if silent else []),
47
54
  catch_exceptions=False,
48
55
  )
@@ -105,6 +112,7 @@ def test_insert_files(silent):
105
112
  for colname, expected_type in expected_types.items():
106
113
  for row in (one, two, three):
107
114
  assert isinstance(row[colname], expected_type)
115
+ assert set(db["files"].pks) == set(expected_pks)
108
116
 
109
117
 
110
118
  @pytest.mark.parametrize(
File without changes
File without changes
File without changes
File without changes