Repository: ofek/pypinfo

Issue:
Allow YYYY-MM-DD dates in --start-date and --end-date
It'd be handy to be able to use `YYYY-MM-DD` dates as the start and end date. For example:

```console
$ pypinfo --start-date 2018-01-01 --end-date 2018-01-31 pillow pyversion
```

Rather than having to work it out:

```console
$ pypinfo --start-date -43 --end-date -14 pillow pyversion
```

It wouldn't necessarily have to reuse `--start-date` and `--end-date`, but that's probably clearest and easiest (if not negative integer, it's a date).

What do you think?


Discussion:
hugovk: Passing CI build: https://travis-ci.org/hugovk/pypinfo/builds/379256544
ofek: Thanks so much!

Patch:
diff --git a/pypinfo/core.py b/pypinfo/core.py
index f1ba663..5c633a4 100644
--- a/pypinfo/core.py
+++ b/pypinfo/core.py
@@ -12,13 +12,17 @@ FROM = """\
 FROM
   TABLE_DATE_RANGE(
     [the-psf:pypi.downloads],
-    DATE_ADD(CURRENT_TIMESTAMP(), {}, "day"),
-    DATE_ADD(CURRENT_TIMESTAMP(), {}, "day")
+    {},
+    {}
   )
 """
+DATE_ADD = 'DATE_ADD(CURRENT_TIMESTAMP(), {}, "day")'
+START_TIMESTAMP = 'TIMESTAMP("{} 00:00:00")'
+END_TIMESTAMP = 'TIMESTAMP("{} 23:59:59")'
 START_DATE = '-31'
 END_DATE = '-1'
 DEFAULT_LIMIT = '10'
+YYYY_MM_DD = re.compile("^[0-9]{4}-[01][0-9]-[0-3][0-9]$")
 
 
 def create_config():
@@ -42,6 +46,28 @@ def create_client(creds_file=None):
     return Client.from_service_account_json(creds_file, project=project)
 
 
+def validate_date(date):
+    valid = False
+    try:
+        if int(date) < 0:
+            valid = True
+    except ValueError:
+        if YYYY_MM_DD.match(date):
+            valid = True
+
+    if not valid:
+        raise ValueError('Dates must be negative integers or YYYY-MM-DD in the past.')
+    return valid
+
+
+def format_date(date, timestamp_format):
+    try:
+        date = DATE_ADD.format(int(date))
+    except ValueError:
+        date = timestamp_format.format(date)
+    return date
+
+
 def build_query(project, all_fields, start_date=None, end_date=None,
                 days=None, limit=None, where=None, order=None, pip=None):
     project = normalize(project)
@@ -53,11 +79,18 @@ def build_query(project, all_fields, start_date=None, end_date=None,
     if days:
         start_date = str(int(end_date) - int(days))
 
-    if int(start_date) > 0 or int(end_date) > 0:
-        raise ValueError('Dates must be in the past (negative).')
+    validate_date(start_date)
+    validate_date(end_date)
+
+    try:
+        if int(start_date) >= int(end_date):
+            raise ValueError('End date must be greater than start date.')
+    except ValueError:
+        # Not integers, must be yyyy-mm-dd
+        pass
 
-    if int(start_date) >= int(end_date):
-        raise ValueError('End date must be greater than start date.')
+    start_date = format_date(start_date, START_TIMESTAMP)
+    end_date = format_date(end_date, END_TIMESTAMP)
 
     fields = []
     used_fields = set()


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 0ec1b9a..121b80b 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,3 +1,6 @@
+import copy
+import pytest
+
 from pypinfo import core
 
 ROWS = [
@@ -16,7 +19,7 @@ ROWS = [
 
 def test_tabulate_default():
     # Arrange
-    rows = list(ROWS)
+    rows = copy.deepcopy(ROWS)
     expected = """\
 | python_version | percent | download_count |
 | -------------- | ------- | -------------- |
@@ -40,7 +43,7 @@ def test_tabulate_default():
 
 def test_tabulate_markdown():
     # Arrange
-    rows = list(ROWS)
+    rows = copy.deepcopy(ROWS)
     expected = """\
 | python_version | percent | download_count |
 | -------------- | ------: | -------------: |
@@ -60,3 +63,50 @@ def test_tabulate_markdown():
 
     # Assert
     assert tabulated == expected
+
+
+def test_validate_date_negative_number():
+    # Act
+    valid = core.validate_date("-1")
+
+    # Assert
+    assert valid
+
+
+def test_validate_date_positive_number():
+    # Act / Assert
+    with pytest.raises(ValueError):
+        core.validate_date("1")
+
+
+def test_validate_date_yyyy_mm_dd():
+    # Act
+    valid = core.validate_date("2018-05-15")
+
+    # Assert
+    assert valid
+
+
+def test_validate_date_other_string():
+    # Act / Assert
+    with pytest.raises(ValueError):
+        core.validate_date("somthing invalid")
+
+
+def test_format_date_negative_number():
+    # Arrange
+    dummy_format = "dummy format {}"
+
+    # Act
+    date = core.format_date("-1", dummy_format)
+
+    # Assert
+    assert date == 'DATE_ADD(CURRENT_TIMESTAMP(), -1, "day")'
+
+
+def test_format_date_yyy_mm_dd():
+    # Act
+    date = core.format_date("2018-05-15", core.START_TIMESTAMP)
+
+    # Assert
+    assert date == 'TIMESTAMP("2018-05-15 00:00:00")'