Skip to content

Commit

Permalink
Replace pendulum with aniso8601 (#146)
Browse files Browse the repository at this point in the history
* Remove pendulum dependency

* Move ISO8601 interval parsing

* Replace pendulum with aniso8601 for ISO8601 interval parsing

* Better defaults for interval

* Ensure we get correct data from ISO8601 parser

* More tests

* Require timezone in ISO8601 intervals, and require non-negative durations
  • Loading branch information
jschlyter authored Jan 15, 2025
1 parent eb2dbda commit 33092c9
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 138 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.12"
- "3.13"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
13 changes: 6 additions & 7 deletions aggrec/aggregates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from urllib.parse import urljoin

import bson
import pendulum
import pymongo
from bson.objectid import ObjectId
from fastapi import APIRouter, Header, HTTPException, Request, Response, status
Expand All @@ -20,7 +19,7 @@
from aggrec.helpers import RequestVerifier

from .db_models import AggregateMetadata
from .helpers import pendulum_as_datetime, rfc_3339_datetime_now
from .helpers import parse_iso8601_interval, rfc_3339_datetime_now
from .settings import Settings

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -285,14 +284,14 @@ async def create_aggregate(
s3_bucket = request.app.settings.s3.get_bucket_name()

if aggregate_interval:
period = pendulum.parse(aggregate_interval)
if not isinstance(period, pendulum.Interval):
try:
aggregate_interval_start, aggregate_interval_timedelta = parse_iso8601_interval(aggregate_interval)
aggregate_interval_duration = aggregate_interval_timedelta.total_seconds()
except ValueError as exc:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
"Invalid Aggregate-Interval: must be an ISO 8601 time interval (e.g., '2024-01-01T12:00:00Z/PT1M')",
)
aggregate_interval_start = pendulum_as_datetime(period.start)
aggregate_interval_duration = period.start.diff(period.end).in_seconds()
) from exc
else:
aggregate_interval_start = None
aggregate_interval_duration = None
Expand Down
9 changes: 6 additions & 3 deletions aggrec/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
import json
import logging
import uuid
from datetime import datetime, timezone
from urllib.parse import urljoin

import cryptography.hazmat.primitives.asymmetric.ec as ec
import cryptography.hazmat.primitives.asymmetric.rsa as rsa
import http_sf
import pendulum
import requests
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from http_message_signatures import HTTPMessageSigner, HTTPSignatureKeyResolver, algorithms

DEFAULT_AGGREGATE_INTERVAL_DURATION = "PT1M"
DEFAULT_CONTENT_TYPE = "application/vnd.apache.parquet"
DEFAULT_COVERED_COMPONENT_IDS = [
"content-type",
Expand Down Expand Up @@ -48,7 +49,9 @@ def main() -> None:

parser = argparse.ArgumentParser(description="Aggregate Sender")

default_interval = f"{pendulum.now().to_iso8601_string()}/PT1M"
default_interval = (
f"{datetime.now(tz=timezone.utc).isoformat(timespec='seconds')}/{DEFAULT_AGGREGATE_INTERVAL_DURATION}"
)

parser.add_argument(
"aggregate",
Expand All @@ -58,7 +61,7 @@ def main() -> None:
parser.add_argument(
"--interval",
metavar="interval",
help="Aggregate interval",
help=f"Aggregate interval (default {default_interval})",
default=default_interval,
)
parser.add_argument(
Expand Down
30 changes: 17 additions & 13 deletions aggrec/helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import hashlib
import logging
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone

import aniso8601
import http_sf
import pendulum
from fastapi import HTTPException, Request, status
from http_message_signatures import (
HTTPMessageVerifier,
Expand Down Expand Up @@ -114,14 +114,18 @@ def rfc_3339_datetime_now() -> str:
return str(datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))


def pendulum_as_datetime(dt: pendulum.DateTime) -> datetime:
return datetime(
year=dt.year,
month=dt.month,
day=dt.day,
hour=dt.hour,
minute=dt.minute,
second=dt.second,
microsecond=dt.microsecond,
tzinfo=dt.tzinfo,
)
def parse_iso8601_interval(interval: str) -> tuple[datetime, timedelta]:
"""Parse ISO8601 interval and return resulting datetime and timedelta"""
t1, t2 = aniso8601.parse_interval(interval)
if not isinstance(t1, datetime) or not isinstance(t2, datetime):
raise ValueError("Invalid interval format")
if t1.tzinfo is None:
raise ValueError("Start time must include timezone")
if t2.tzinfo is None:
raise ValueError("End time must include timezone")
t1 = t1.astimezone(timezone.utc)
t2 = t2.astimezone(timezone.utc)
duration = timedelta(seconds=(t2 - t1).total_seconds())
if duration.total_seconds() < 0:
raise ValueError("Duration cannot be negative")
return t1, duration
129 changes: 16 additions & 113 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ pydantic-settings = "^2.7.0"
werkzeug = "^3.0.4"
boto3 = "^1.26.133"
aiomqtt = "^2.2.0"
pendulum = "^3"
pyyaml = "^6.0.1"
http-sf = "^1.0.2"
redis = "^5.1.1"
aiobotocore = ">=2.15.2"
aniso8601 = "^10.0.0"

[tool.poetry.group.dev.dependencies]
pytest = "^8.2.0"
Expand Down
Loading

0 comments on commit 33092c9

Please sign in to comment.