hugovk: Passing CI build: https://travis-ci.org/hugovk/pypinfo/builds/379256544
ofek: Thanks so much!
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()

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?

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")'

pip install -e .
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
binary==1.0.1
cachetools==5.5.2
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
google-api-core==2.24.2
google-auth==2.38.0
google-cloud-bigquery==3.31.0
google-cloud-core==2.4.3
google-crc32c==1.7.1
google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
grpcio==1.71.0
grpcio-status==1.71.0
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
proto-plus==1.26.1
protobuf==5.29.4
pyasn1==0.6.1
pyasn1_modules==0.4.2
-e git+https://github.com/ofek/pypinfo.git@72ea0a3e5669757e3c625ea2f1f0e3463d11db86#egg=pypinfo
pytest @ file:///croot/pytest_1738938843180/work
python-dateutil==2.9.0.post0
requests==2.32.3
rsa==4.9
six==1.17.0
tinydb==4.8.2
tinyrecord==0.2.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
urllib3==2.3.0

name: pypinfo
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - binary==1.0.1
      - cachetools==5.5.2
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - click==8.1.8
      - google-api-core==2.24.2
      - google-auth==2.38.0
      - google-cloud-bigquery==3.31.0
      - google-cloud-core==2.4.3
      - google-crc32c==1.7.1
      - google-resumable-media==2.7.2
      - googleapis-common-protos==1.69.2
      - grpcio==1.71.0
      - grpcio-status==1.71.0
      - idna==3.10
      - proto-plus==1.26.1
      - protobuf==5.29.4
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - python-dateutil==2.9.0.post0
      - requests==2.32.3
      - rsa==4.9
      - six==1.17.0
      - tinydb==4.8.2
      - tinyrecord==0.2.0
      - urllib3==2.3.0
prefix: /opt/conda/envs/pypinfo

tests/test_core.py::test_validate_date_negative_number tests/test_core.py::test_validate_date_positive_number tests/test_core.py::test_validate_date_yyyy_mm_dd tests/test_core.py::test_validate_date_other_string tests/test_core.py::test_format_date_negative_number tests/test_core.py::test_format_date_yyy_mm_dd
tests/test_core.py::test_tabulate_default tests/test_core.py::test_tabulate_markdown
pypinfo/core.py
pypinfo/core.py