sqlite-dateutil 0.1a0__py3-none-pyemscripten_2025_0_wasm32.whl
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.
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Python helpers for the sqlite-dateutil SQLite extension."""
|
|
2
|
+
import os.path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import sqlite3
|
|
7
|
+
except ModuleNotFoundError as e:
|
|
8
|
+
if sys.platform == "emscripten":
|
|
9
|
+
raise ModuleNotFoundError(
|
|
10
|
+
"sqlite-dateutil needs Pyodide's sqlite3 package: run "
|
|
11
|
+
"`await micropip.install('sqlite3')` (or pyodide.loadPackage('sqlite3') "
|
|
12
|
+
"from JavaScript) before importing sqlite_dateutil"
|
|
13
|
+
) from e
|
|
14
|
+
raise
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1a0"
|
|
17
|
+
__version_info__ = tuple(__version__.split("."))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def loadable_path() -> str:
|
|
21
|
+
"""Path to the bundled extension, without its file suffix (as SQLite expects)."""
|
|
22
|
+
return os.path.normpath(os.path.join(os.path.dirname(__file__), "dateutil_parse"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if sys.platform == "emscripten":
|
|
26
|
+
# Pyodide's sqlite3 module is built without load_extension(). Its SQLite
|
|
27
|
+
# is still reachable with ctypes, so register the extension with
|
|
28
|
+
# sqlite3_auto_extension(): every connection opened after this import
|
|
29
|
+
# gets the functions. Pyodide 314+ links SQLite into the main module;
|
|
30
|
+
# older releases ship it inside the separate _sqlite3 shared library.
|
|
31
|
+
#
|
|
32
|
+
# SQLite passes the init function its table of API functions, but a
|
|
33
|
+
# SQLite built without load-extension support (Pyodide 0.28/0.29) passes
|
|
34
|
+
# NULL. So hand the extension the address of every function it uses
|
|
35
|
+
# first; tools/pyodide_shim.c falls back to those when the table is NULL.
|
|
36
|
+
import ctypes
|
|
37
|
+
|
|
38
|
+
def _find_sqlite():
|
|
39
|
+
import _sqlite3
|
|
40
|
+
|
|
41
|
+
candidates = [None]
|
|
42
|
+
if getattr(_sqlite3, "__file__", None):
|
|
43
|
+
candidates.append(_sqlite3.__file__)
|
|
44
|
+
for candidate in candidates:
|
|
45
|
+
lib = ctypes.CDLL(candidate)
|
|
46
|
+
if hasattr(lib, "sqlite3_auto_extension"):
|
|
47
|
+
return lib
|
|
48
|
+
raise ImportError("sqlite-dateutil could not find SQLite's C API in this Pyodide")
|
|
49
|
+
|
|
50
|
+
def _address(func):
|
|
51
|
+
return ctypes.cast(func, ctypes.c_void_p).value
|
|
52
|
+
|
|
53
|
+
_sqlite = _find_sqlite()
|
|
54
|
+
_lib = ctypes.CDLL(loadable_path() + ".so")
|
|
55
|
+
_lib.dateutil_pyodide_set_api.argtypes = [ctypes.c_char_p, ctypes.c_void_p]
|
|
56
|
+
_lib.dateutil_pyodide_set_api.restype = ctypes.c_int
|
|
57
|
+
_names = ctypes.string_at( # a C char array, so take its address
|
|
58
|
+
ctypes.addressof(ctypes.c_char.in_dll(_lib, "dateutil_pyodide_api_names")))
|
|
59
|
+
for _name in _names.decode().strip(",").split(","):
|
|
60
|
+
_fn = getattr(_sqlite, "sqlite3_" + _name, None)
|
|
61
|
+
if _fn is None or _lib.dateutil_pyodide_set_api(_name.encode(), _address(_fn)) != 0:
|
|
62
|
+
raise ImportError("sqlite-dateutil could not find sqlite3_" + _name)
|
|
63
|
+
_sqlite.sqlite3_auto_extension.argtypes = [ctypes.c_void_p]
|
|
64
|
+
_sqlite.sqlite3_auto_extension.restype = ctypes.c_int
|
|
65
|
+
_rc = _sqlite.sqlite3_auto_extension(_address(_lib.dateutil_pyodide_init))
|
|
66
|
+
if _rc != 0:
|
|
67
|
+
raise ImportError("sqlite3_auto_extension() failed with code %d" % _rc)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load(conn: sqlite3.Connection) -> None:
|
|
71
|
+
"""Load the extension into conn.
|
|
72
|
+
|
|
73
|
+
Extension loading is switched on for the call and back off afterwards.
|
|
74
|
+
In Pyodide the extension is registered when this module is imported, so
|
|
75
|
+
connections opened after the import already have the functions.
|
|
76
|
+
"""
|
|
77
|
+
if sys.platform == "emscripten":
|
|
78
|
+
try:
|
|
79
|
+
conn.execute("select dateutil_parse('')")
|
|
80
|
+
except sqlite3.OperationalError:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"In Pyodide, sqlite_dateutil must be imported before the "
|
|
83
|
+
"connection is opened: this connection predates the import"
|
|
84
|
+
) from None
|
|
85
|
+
return
|
|
86
|
+
conn.enable_load_extension(True)
|
|
87
|
+
try:
|
|
88
|
+
conn.load_extension(loadable_path())
|
|
89
|
+
finally:
|
|
90
|
+
conn.enable_load_extension(False)
|
|
Binary file
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlite-dateutil
|
|
3
|
+
Version: 0.1a0
|
|
4
|
+
Summary: A SQLite extension porting dateutil.parser.parse() to C
|
|
5
|
+
Author: Simon Willison
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Project-URL: Homepage, https://github.com/simonw/sqlite-dateutil
|
|
9
|
+
Project-URL: Issues, https://github.com/simonw/sqlite-dateutil/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/simonw/sqlite-dateutil/releases
|
|
11
|
+
Project-URL: CI, https://github.com/simonw/sqlite-dateutil/actions
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# sqlite-dateutil
|
|
15
|
+
|
|
16
|
+
A SQLite extension, written in C, that ports [python-dateutil](https://github.com/dateutil/dateutil)'s
|
|
17
|
+
`dateutil.parser.parse()` to a set of SQL functions.
|
|
18
|
+
|
|
19
|
+
```sql
|
|
20
|
+
select dateutil_parse('Thu, 25 Sep 2003 10:49:41 -0300');
|
|
21
|
+
-- 2003-09-25T10:49:41-03:00
|
|
22
|
+
|
|
23
|
+
select dateutil_parse_fuzzy('Today is January 1, 2047 at 8:21:00AM');
|
|
24
|
+
-- 2047-01-01T08:21:00
|
|
25
|
+
|
|
26
|
+
select dateutil_parse('10/09/2003'), dateutil_parse_dayfirst('10/09/2003');
|
|
27
|
+
-- 2003-10-09T00:00:00 | 2003-09-10T00:00:00
|
|
28
|
+
|
|
29
|
+
select dateutil_parse('not a date');
|
|
30
|
+
-- NULL
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| SQL function | Python equivalent |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `dateutil_parse(text)` | `parse(text).isoformat()` |
|
|
36
|
+
| `dateutil_parse_fuzzy(text)` | `parse(text, fuzzy=True).isoformat()` |
|
|
37
|
+
| `dateutil_parse_dayfirst(text)` | `parse(text, dayfirst=True).isoformat()` |
|
|
38
|
+
| `dateutil_parse_fuzzy_dayfirst(text)` | `parse(text, fuzzy=True, dayfirst=True).isoformat()` |
|
|
39
|
+
|
|
40
|
+
The function names match the [datasette-dateutil](https://github.com/simonw/datasette-dateutil) plugin.
|
|
41
|
+
Where dateutil raises an exception, these functions return `NULL`. Missing fields come from
|
|
42
|
+
today's local date at midnight, as in dateutil (so `dateutil_parse('10:30')` is today at 10:30).
|
|
43
|
+
That also means the functions aren't marked deterministic.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
The extension is packaged for Python, with prebuilt wheels for macOS (Apple Silicon and
|
|
48
|
+
Intel), Linux (x86_64 and aarch64) and Windows (x64):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install sqlite-dateutil
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import sqlite3
|
|
56
|
+
import sqlite_dateutil
|
|
57
|
+
|
|
58
|
+
db = sqlite3.connect(":memory:")
|
|
59
|
+
sqlite_dateutil.load(db)
|
|
60
|
+
print(db.execute("select dateutil_parse('Sep 25 2003 5pm')").fetchone()[0])
|
|
61
|
+
# 2003-09-25T17:00:00
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`load()` turns extension loading on just long enough to load the extension, then turns it
|
|
65
|
+
off again. `sqlite_dateutil.loadable_path()` gives the path to the bundled library for use
|
|
66
|
+
with other tools.
|
|
67
|
+
|
|
68
|
+
### Pyodide
|
|
69
|
+
|
|
70
|
+
There are also wheels for [Pyodide](https://pyodide.org/):
|
|
71
|
+
|
|
72
|
+
| Pyodide | Wheel tag |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| 314.x | `pyemscripten_2026_0_wasm32` |
|
|
75
|
+
| 0.28.x, 0.29.x | `pyodide_2025_0_wasm32` |
|
|
76
|
+
|
|
77
|
+
Pyodide's `sqlite3` module can't load extensions, so in Pyodide `import sqlite_dateutil`
|
|
78
|
+
registers the functions with SQLite's `sqlite3_auto_extension()` instead. Every connection
|
|
79
|
+
opened **after the import** has them, and `load(conn)` just checks that:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import micropip
|
|
83
|
+
await micropip.install("sqlite-dateutil")
|
|
84
|
+
# Pyodide 0.28/0.29 only: sqlite3 is a separate package there
|
|
85
|
+
# await micropip.install("sqlite3")
|
|
86
|
+
|
|
87
|
+
import sqlite3
|
|
88
|
+
import sqlite_dateutil # import before connecting
|
|
89
|
+
|
|
90
|
+
db = sqlite3.connect(":memory:")
|
|
91
|
+
print(db.execute("select dateutil_parse('Sep 25 2003 5pm')").fetchone()[0])
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
All of these Pyodide releases link in SQLite 3.39.0, older than the headers in
|
|
95
|
+
`vendor/sqlite`, but the extension only uses APIs that SQLite had long before that version.
|
|
96
|
+
Pyodide 0.28/0.29 build SQLite without load-extension support, so SQLite hands extensions no
|
|
97
|
+
table of its API functions. `tools/pyodide_shim.c` gets those function addresses from Python
|
|
98
|
+
instead.
|
|
99
|
+
|
|
100
|
+
There are also plugins that add the functions to every connection:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
datasette install datasette-sqlite-dateutil
|
|
104
|
+
sqlite-utils install sqlite-utils-sqlite-dateutil
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Building
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv run python tools/build.py # or just: make
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This builds `dateutil_parse.dylib` on macOS, `.so` on Linux, or `.dll` on Windows. The
|
|
114
|
+
build script uses only the Python standard library and picks the compiler from `$CC`, or
|
|
115
|
+
falls back to `cc`. On Windows it uses MSVC `cl` from a Native Tools prompt, or MinGW `gcc` /
|
|
116
|
+
`clang`. It compiles against the SQLite headers in `vendor/sqlite/`, never the system
|
|
117
|
+
headers, because some system copies (notably the macOS SDK's `sqlite3ext.h`) turn off the
|
|
118
|
+
loadable-extension API.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
sqlite> .load ./dateutil_parse
|
|
122
|
+
sqlite> select dateutil_parse('Sep 25 2003 5pm');
|
|
123
|
+
2003-09-25T17:00:00
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import sqlite3
|
|
128
|
+
db = sqlite3.connect(":memory:")
|
|
129
|
+
db.enable_load_extension(True)
|
|
130
|
+
db.load_extension("./dateutil_parse")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The extension is `dateutil_parse.c` plus the generated `dateutil_unicode.h`. It has no
|
|
134
|
+
dependencies beyond the SQLite headers. The entry point is `sqlite3_dateutilparse_init`.
|
|
135
|
+
|
|
136
|
+
## Tests
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv run pytest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
That is all you need: uv creates the environment, and the test suite compiles the extension
|
|
143
|
+
first whenever it is missing or older than its sources. The project is set to use only
|
|
144
|
+
uv-managed Pythons, because some system Pythons, such as Apple's `/usr/bin/python3`, are
|
|
145
|
+
built without `enable_load_extension()`. The sanitizer tests are skipped when ASan/UBSan
|
|
146
|
+
isn't available (for example with MSVC).
|
|
147
|
+
|
|
148
|
+
The test suite (`tests/`) is differential: for every input it checks that the SQL functions
|
|
149
|
+
return exactly what `dateutil.parser.parse(...).isoformat()` returns, or `NULL` where dateutil
|
|
150
|
+
raises. It covers:
|
|
151
|
+
|
|
152
|
+
- 190 hand-picked inputs (ISO 8601, RFC 2822, Unix `date`, numeric ambiguity, am/pm,
|
|
153
|
+
`10h36m`, `GMT+3`, `-0300 (BRST)`, fuzzy sentences, NUL bytes, Unicode whitespace and
|
|
154
|
+
punctuation, and many inputs that should fail), across all four functions.
|
|
155
|
+
- 40,000 generated inputs (token soup plus datetime-shaped strings with out-of-range parts).
|
|
156
|
+
- The parser core built with **AddressSanitizer + UBSan** (`tools/du_cli.c`). This runs the
|
|
157
|
+
same corpus against fixed default dates (Jan 31, Feb 29, 9999-12-28, ...) to test month-end
|
|
158
|
+
clamping and weekday arithmetic, plus 20,000 random byte strings with invalid UTF-8.
|
|
159
|
+
- Property-based tests with [Hypothesis](https://hypothesis.readthedocs.io/) (`tests/test_properties.py`):
|
|
160
|
+
arbitrary Unicode, token soup, real datetimes rendered in about 20 `strftime` formats, and fuzzy
|
|
161
|
+
sentences must all match dateutil. Any result must be valid ISO 8601 that parses back to itself,
|
|
162
|
+
and arbitrary BLOBs must never raise an error. Failures shrink to a minimal input. Run them for
|
|
163
|
+
longer with `HYPOTHESIS_PROFILE=thorough uv run pytest tests/test_properties.py`
|
|
164
|
+
(20,000 examples per property).
|
|
165
|
+
- Limits on input size and running time, behavior in SQL queries, and the known differences below.
|
|
166
|
+
|
|
167
|
+
The parser core is also fuzzed with libFuzzer + ASan + UBSan on Linux, for 3 minutes on every
|
|
168
|
+
push, and for 30 minutes daily if `main` has changed in the last 24 hours. See [fuzz/README.md](fuzz/README.md).
|
|
169
|
+
|
|
170
|
+
During development, a further 1.35 million generated comparisons found zero mismatches with
|
|
171
|
+
dateutil 2.9.0.
|
|
172
|
+
|
|
173
|
+
## Safety and performance
|
|
174
|
+
|
|
175
|
+
- **Bounded memory, no overflow.** Every token lookahead is bounds-checked. Integer parsing
|
|
176
|
+
is strict and saturates at 10^15, so arithmetic can't overflow. Output goes into a
|
|
177
|
+
fixed buffer through `snprintf`. There is no recursion.
|
|
178
|
+
- **Linear time.** One pass tokenizes and one pass parses. Inputs up to 256 bytes need no heap
|
|
179
|
+
allocation. Longer inputs use one allocation, sized to the input.
|
|
180
|
+
- **Size cap.** Inputs over 64 KiB return `NULL` (compile with `-DDATEUTIL_MAX_INPUT=...`
|
|
181
|
+
to change this).
|
|
182
|
+
- **Arbitrary bytes are safe.** Embedded NULs are skipped, as dateutil does. Invalid UTF-8
|
|
183
|
+
is handled.
|
|
184
|
+
- **Exact numbers.** dateutil uses `Decimal` to avoid float rounding (its GH #427). This port
|
|
185
|
+
does the same arithmetic on the decimal digits, so `1.7h` is 1:42, not 1:41.
|
|
186
|
+
- Registered as `SQLITE_INNOCUOUS`.
|
|
187
|
+
|
|
188
|
+
One machine, 100,000 mixed inputs with `fuzzy=True`: **~1.1 million rows/second**,
|
|
189
|
+
about 38x faster than calling dateutil from Python.
|
|
190
|
+
|
|
191
|
+
## Known differences from dateutil
|
|
192
|
+
|
|
193
|
+
These are deliberate:
|
|
194
|
+
|
|
195
|
+
1. **No `tzinfos`, and no local time zone names.** As in dateutil without `tzinfos`, a zone
|
|
196
|
+
name it can't resolve (e.g. `EST`) gives a naive result. dateutil also maps names in
|
|
197
|
+
`time.tzname` (the server's own zone) to `tzlocal()`. This port doesn't, so results never
|
|
198
|
+
depend on the server's `TZ` setting. `UTC`, `GMT`, `Z` and numeric offsets work as in dateutil.
|
|
199
|
+
2. **Only ASCII digits form numbers.** Characters are classified exactly as Python's
|
|
200
|
+
`isalpha()`, `isdigit()` and `isspace()` do (table generated by `tools/gen_unicode.py`).
|
|
201
|
+
Python's `float()`/`int()` also accept other Unicode decimal digits, such as full-width
|
|
202
|
+
`2003`. Those inputs return `NULL` here.
|
|
203
|
+
3. **Numeric extremes.** dateutil's `Decimal` arithmetic uses 28 significant digits.
|
|
204
|
+
Fractions longer than that may round differently. Absurdly long numbers make dateutil raise
|
|
205
|
+
`decimal.InvalidOperation`; here they return `NULL`.
|
|
206
|
+
4. **Inputs over 64 KiB** return `NULL`.
|
|
207
|
+
|
|
208
|
+
## Layout
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
dateutil_parse.c the extension (port of dateutil/parser/_parser.py)
|
|
212
|
+
dateutil_unicode.h generated character-class table
|
|
213
|
+
tools/gen_unicode.py regenerates the table from Python's Unicode data
|
|
214
|
+
tools/du_cli.c SQLite-free driver used for sanitizer tests
|
|
215
|
+
tools/build.py cross-platform build script (used by the tests)
|
|
216
|
+
tools/build_wheels.py packages the built extension as wheels, plus the plugin wheels
|
|
217
|
+
fuzz/ libFuzzer harness, dictionary and seed generator
|
|
218
|
+
vendor/sqlite/ SQLite 3.53.2 public-domain headers
|
|
219
|
+
tests/ pytest differential test suite, plus wheel packaging tests
|
|
220
|
+
tests/pyodide/ installs each Pyodide wheel with micropip under Node and checks it
|
|
221
|
+
tools/pyodide_shim.c Pyodide entry point that copes with SQLite built without extension support
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Releasing
|
|
225
|
+
|
|
226
|
+
Bump `version` in `pyproject.toml`, then publish a GitHub release with that version as its
|
|
227
|
+
tag. `.github/workflows/release.yml` runs the tests, builds a wheel on each platform
|
|
228
|
+
(in the `manylinux_2_28` images on Linux), installs and tests each one, then uploads them and
|
|
229
|
+
the two plugin wheels to PyPI through trusted publishing.
|
|
230
|
+
|
|
231
|
+
To build wheels locally:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
uv run python tools/build_wheels.py # this platform, into dist/
|
|
235
|
+
uv run python tools/build_wheels.py --plugins # datasette- and sqlite-utils- plugins
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
On macOS the wheel targets macOS 11 (arm64) or 10.13 (x86_64), unless
|
|
239
|
+
`MACOSX_DEPLOYMENT_TARGET` says otherwise. The script refuses to package a binary built for
|
|
240
|
+
a newer macOS than its wheel tag claims.
|
|
241
|
+
|
|
242
|
+
Each Pyodide wheel needs the exact Emscripten version its Pyodide ABI uses (see
|
|
243
|
+
`PYODIDE_TARGETS` in `tools/build_wheels.py`): 4.0.9 for ABI `2025_0` (Pyodide 0.28/0.29),
|
|
244
|
+
5.0.3 for `2026_0` (Pyodide 314). With that Emscripten active:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
uv run python tools/build_wheels.py --pyodide 2026_0 # or 2025_0
|
|
248
|
+
cd tests/pyodide && npm ci && node test.mjs ../../dist/sqlite_dateutil-*_2026_0_wasm32.whl
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
emsdk keeps one active version per checkout, so use a separate emsdk directory for each, or
|
|
252
|
+
point `EMCC` at the right `upstream/emscripten/emcc`.
|
|
253
|
+
|
|
254
|
+
`tests/pyodide/test.mjs` picks the Pyodide release from the wheel's tag, installs the wheel
|
|
255
|
+
with micropip under Node, and checks every hand-picked case against python-dateutil running
|
|
256
|
+
in that Pyodide. On 0.28/0.29 it also checks that importing without `sqlite3` explains what
|
|
257
|
+
to do. CI runs it for every ABI on every push. Adding a Pyodide ABI means a new
|
|
258
|
+
`PYODIDE_TARGETS` entry, a matching entry in the `test.yml` Pyodide matrix (with its emsdk
|
|
259
|
+
commit), a `pyodide-<ABI>` alias in `tests/pyodide/package.json`, and the wheel count and
|
|
260
|
+
publish step in `release.yml`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
sqlite_dateutil/__init__.py,sha256=NsFA4QJeBzz2f4TuXRNErUlgWAyeHplDhnkKTFgGR1w,3750
|
|
2
|
+
sqlite_dateutil/dateutil_parse.so,sha256=zI0V5HTuft5wt8iisbafz2KSRcv4SSkl-qJEi7nc2zw,29123
|
|
3
|
+
sqlite_dateutil-0.1a0.dist-info/METADATA,sha256=h736pGihN8KjKAzfp-JXeZV3vOwisYp1UVl-Qtx5Sbw,11618
|
|
4
|
+
sqlite_dateutil-0.1a0.dist-info/WHEEL,sha256=fkZMRkFAhBJx2T5h6b_I-BPERjJ7QXcdK2IhrJyF1wM,132
|
|
5
|
+
sqlite_dateutil-0.1a0.dist-info/RECORD,,
|