Skip to content

Commit

Permalink
Initial working version
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Sep 28, 2020
0 parents commit abd2a35
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 0 deletions.
58 changes: 58 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Publish Python Package

on:
release:
types: [created]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install -e '.[test]'
- name: Run tests
run: |
pytest
deploy:
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.8'
- uses: actions/cache@v2
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-publish-pip-
- name: Install dependencies
run: |
pip install setuptools wheel twine
- name: Publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test

on: [push]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install -e '.[test]'
- name: Run tests
run: |
pytest
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.venv
__pycache__/
*.py[cod]
*$py.class
venv
.eggs
.pytest_cache
*.egg-info
.DS_Store
.vscode
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# datasette-dateutil

[![PyPI](https://img.shields.io/pypi/v/datasette-dateutil.svg)](https://pypi.org/project/datasette-dateutil/)
[![Changelog](https://img.shields.io/github/v/release/simonw/datasette-dateutil?include_prereleases&label=changelog)](https://github.com/simonw/datasette-dateutil/releases)
[![Tests](https://github.com/simonw/datasette-dateutil/workflows/Test/badge.svg)](https://github.com/simonw/datasette-dateutil/actions?query=workflow%3ATest)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-dateutil/blob/main/LICENSE)

dateutil functions for Datasette

## Installation

Install this plugin in the same environment as Datasette.

$ datasette install datasette-dateutil

## Usage

This function adds custom SQL functions that expose functionality from the [dateutil](https://dateutil.readthedocs.io/) Python library.

Once installed, the following SQL functions become available:

- `dateutil_parse(text)` - returns an ISO8601 date string parsed from the text, or `null` if the input could not be parsed. `dateutil_parse("10 october 2020 3pm")` returns `2020-10-10T15:00:00`.
- `dateutil_parse_fuzzy(text)` - same as `dateutil_parse()` but this also works against strings that contain a date somewhere within them - that date will be returned, or `null` if no dates could be found. `dateutil_parse_fuzzy("This is due 10 september")` returns `2020-09-10T00:00:00` (but will start returning the 2021 version of that if the year is 2021).
- `dateutil_easter(year)` - returns the date for Easter in that year, for example `dateutil_easter("2020")` returns `2020-04-12`.

The `dateutil_parse()` and `dateutil_parse_fuzzy()` functions both follow the American convention of assuming that `1/2/2020` lists the month first, evaluating this example to the 2nd of January.

If you want to assume that the day comes first, use these two functions instead:

- `dateutil_parse_dayfirst(text)`
- `dateutil_parse_fuzzy_dayfirst(text)`

## Development

To set up this plugin locally, first checkout the code. Then create a new virtual environment:

cd datasette-dateutil
python3 -mvenv venv
source venv/bin/activate

Or if you are using `pipenv`:

pipenv shell

Now install the dependencies and tests:

pip install -e '.[test]'

To run the tests:

pytest
51 changes: 51 additions & 0 deletions datasette_dateutil/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from datasette import hookimpl
from dateutil.parser import parse, ParserError
from dateutil.easter import easter


def _dateutil_parse_shared(s, **kwargs):
if not kwargs.get("dayfirst"):
kwargs["dayfirst"] = False
if not s:
return None
try:
return parse(s, **kwargs).isoformat()
except ParserError:
return None


def dateutil_parse(s):
return _dateutil_parse_shared(s)


def dateutil_parse_fuzzy(s):
return _dateutil_parse_shared(s, fuzzy=True)


def dateutil_parse_dayfirst(s):
return _dateutil_parse_shared(s, dayfirst=True)


def dateutil_parse_fuzzy_dayfirst(s):
return _dateutil_parse_shared(s, fuzzy=True, dayfirst=True)


def dateutil_easter(year):
year = str(year) if year else None
if not year or not year.isdigit():
return None
try:
return easter(int(year)).isoformat()
except Exception as e:
return None


@hookimpl
def prepare_connection(conn):
conn.create_function("dateutil_parse", 1, dateutil_parse)
conn.create_function("dateutil_parse_fuzzy", 1, dateutil_parse_fuzzy)
conn.create_function("dateutil_parse_dayfirst", 1, dateutil_parse_dayfirst)
conn.create_function(
"dateutil_parse_fuzzy_dayfirst", 1, dateutil_parse_fuzzy_dayfirst
)
conn.create_function("dateutil_easter", 1, dateutil_easter)
34 changes: 34 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from setuptools import setup
import os

VERSION = "0.1"


def get_long_description():
with open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md"),
encoding="utf8",
) as fp:
return fp.read()


setup(
name="datasette-dateutil",
description="dateutil functions for Datasette",
long_description=get_long_description(),
long_description_content_type="text/markdown",
author="Simon Willison",
url="https://github.com/simonw/datasette-dateutil",
project_urls={
"Issues": "https://github.com/simonw/datasette-dateutil/issues",
"CI": "https://github.com/simonw/datasette-dateutil/actions",
"Changelog": "https://github.com/simonw/datasette-dateutil/releases",
},
license="Apache License, Version 2.0",
version=VERSION,
packages=["datasette_dateutil"],
entry_points={"datasette": ["dateutil = datasette_dateutil"]},
install_requires=["datasette", "python-dateutil"],
extras_require={"test": ["pytest", "pytest-asyncio", "httpx"]},
tests_require=["datasette-dateutil[test]"],
)
39 changes: 39 additions & 0 deletions tests/test_dateutil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from datasette.app import Datasette
import pytest
import httpx


@pytest.mark.asyncio
async def test_plugin_is_installed():
app = Datasette([], memory=True).app()
async with httpx.AsyncClient(app=app) as client:
response = await client.get("http://localhost/-/plugins.json")
assert 200 == response.status_code
installed_plugins = {p["name"] for p in response.json()}
assert "datasette-dateutil" in installed_plugins


@pytest.mark.asyncio
@pytest.mark.parametrize("sql,expected", [
("select dateutil_parse('1st october 2009')", "2009-10-01T00:00:00"),
("select dateutil_parse('invalid')", None),
("select dateutil_parse('due on 1st october 2009')", None),
("select dateutil_parse_fuzzy('due on 1st october 2009')", "2009-10-01T00:00:00"),
("select dateutil_parse_fuzzy('due on')", None),
("select dateutil_parse_dayfirst('1/2/2020')", "2020-02-01T00:00:00"),
("select dateutil_parse('1/2/2020')", "2020-01-02T00:00:00"),
("select dateutil_parse_fuzzy('due on 1/2/2003')", "2003-01-02T00:00:00"),
("select dateutil_parse_fuzzy_dayfirst('due on 1/2/2003')", "2003-02-01T00:00:00"),
("select dateutil_easter(2020)", "2020-04-12"),
("select dateutil_easter('invalid')", None),
])
async def test_dateutil_sql_functions(sql, expected):
app = Datasette([], memory=True).app()
async with httpx.AsyncClient(app=app) as client:
response = await client.get("http://localhost/:memory:.json", params={
"sql": sql,
"_shape": "array",
})
assert 200 == response.status_code
actual = list(response.json()[0].values())[0]
assert actual == expected

0 comments on commit abd2a35

Please sign in to comment.