diff --git a/calendar_utils/icalevents/__init__.py b/calendar_utils/icalevents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7a6441b622147f81d0c2ca014a3e48a99e53b1eb --- /dev/null +++ b/calendar_utils/icalevents/__init__.py @@ -0,0 +1,5 @@ +""" +iCalEvents search all events occurring in a given time frame in an iCal file. +""" + +__all__ = ["icaldownload", "icalparser", "icalevents"] diff --git a/calendar_utils/icalevents/icaldownload.py b/calendar_utils/icalevents/icaldownload.py new file mode 100644 index 0000000000000000000000000000000000000000..6aadeddb706b011bc414b9d10640a73fa9985e31 --- /dev/null +++ b/calendar_utils/icalevents/icaldownload.py @@ -0,0 +1,109 @@ +""" +Downloads an iCal url or reads an iCal file. +""" +from httplib2 import Http +import logging + + +def apple_data_fix(content): + """ + Fix Apple tzdata bug. + + :param content: content to fix + :return: fixed content + """ + return content.replace("TZOFFSETFROM:+5328", "TZOFFSETFROM:+0053") + + +def apple_url_fix(url): + """ + Fix Apple URL. + + :param url: URL to fix + :return: fixed URL + """ + if url.startswith("webcal://"): + url = url.replace("webcal://", "http://", 1) + return url + + +class ICalDownload: + """ + Downloads or reads and decodes iCal sources. + """ + + def __init__(self, http=None, encoding="utf-8"): + # Get logger + logger = logging.getLogger() + + # default http connection to use + if http is None: + try: + http = Http(".cache") + except (PermissionError, OSError) as e: + # Cache disabled if no write permission in working directory + logger.warning( + ( + "Caching is disabled due to a read-only working directory: {}" + ).format(e) + ) + http = Http() + + self.http = http + self.encoding = encoding + + def data_from_url(self, url, apple_fix=False): + """ + Download iCal data from URL. + + :param url: URL to download + :param apple_fix: fix Apple bugs (protocol type and tzdata in iCal) + :return: decoded (and fixed) iCal data + """ + if apple_fix: + url = apple_url_fix(url) + + _, content = self.http.request(url) + + if not content: + raise ConnectionError("Could not get data from %s!" % url) + + return self.decode(content, apple_fix=apple_fix) + + def data_from_file(self, file, apple_fix=False): + """ + Read iCal data from file. + + :param file: file to read + :param apple_fix: fix wrong Apple tzdata in iCal + :return: decoded (and fixed) iCal data + """ + with open(file, mode="rb") as f: + content = f.read() + + if not content: + raise IOError("File %s is not readable or is empty!" % file) + + return self.decode(content, apple_fix=apple_fix) + + def data_from_string(self, string_content, apple_fix=False): + if not string_content: + raise IOError("String content is not readable or is empty!") + + return self.decode(string_content, apple_fix=apple_fix) + + def decode(self, content, apple_fix=False): + """ + Decode content using the set charset. + + :param content: content do decode + :param apple_fix: fix Apple txdata bug + :return: decoded (and fixed) content + """ + content = content.decode(self.encoding) + content = content.replace("\r", "") + + if apple_fix: + content = apple_data_fix(content) + + return content diff --git a/calendar_utils/icalevents/icalevents.py b/calendar_utils/icalevents/icalevents.py new file mode 100644 index 0000000000000000000000000000000000000000..91ac0e2dbac7c24c248b14165012e3279b25bfcc --- /dev/null +++ b/calendar_utils/icalevents/icalevents.py @@ -0,0 +1,171 @@ +from threading import Lock, Thread + +from .icalparser import parse_events, Event +from .icaldownload import ICalDownload + + +# Lock for event data +event_lock = Lock() +# Event data +event_store = {} +# Threads +threads = {} + + +def events( + url=None, + file=None, + string_content=None, + start=None, + end=None, + fix_apple=False, + http=None, + tzinfo=None, + sort=None, + strict=False, +) -> list[Event]: + """ + Get all events form the given iCal URL occurring in the given time range. + + :param url: iCal URL + :param file: iCal file path + :param string_content: iCal content as string + :param start: start date (see dateutils.date) + :param end: end date (see dateutils.date) + :param fix_apple: fix known Apple iCal issues + :param tzinfo: return values in specified tz + :param sort: sort return values + :param strict: return dates, datetimes and datetime with timezones as specified in ical + :sort sorts events by start time + + :return events + """ + found_events = [] + + content = None + ical_download = ICalDownload(http=http) + + if url: + content = ical_download.data_from_url(url, apple_fix=fix_apple) + + if not content and file: + content = ical_download.data_from_file(file, apple_fix=fix_apple) + + if not content and string_content: + content = ical_download.data_from_string(string_content, apple_fix=fix_apple) + + found_events += parse_events( + content, start=start, end=end, tzinfo=tzinfo, sort=sort, strict=strict + ) + + if found_events is not None and sort is True: + found_events.sort() + + return found_events + + +def request_data(key, url, file, string_content, start, end, fix_apple): + """ + Request data, update local data cache and remove this Thread from queue. + + :param key: key for data source to get result later + :param url: iCal URL + :param file: iCal file path + :param string_content: iCal content as string + :param start: start date + :param end: end date + :param fix_apple: fix known Apple iCal issues + """ + data = [] + + try: + data += events( + url=url, + file=file, + string_content=string_content, + start=start, + end=end, + fix_apple=fix_apple, + ) + finally: + update_events(key, data) + request_finished(key) + + +def events_async( + key, url=None, file=None, start=None, string_content=None, end=None, fix_apple=False +): + """ + Trigger an asynchronous data request. + + :param key: key for data source to get result later + :param url: iCal URL + :param file: iCal file path + :param string_content: iCal content as string + :param start: start date + :param end: end date + :param fix_apple: fix known Apple iCal issues + """ + t = Thread( + target=request_data, + args=(key, url, file, string_content, start, end, fix_apple), + ) + + with event_lock: + if key not in threads: + threads[key] = [] + + threads[key].append(t) + + if not threads[key][0].is_alive(): + threads[key][0].start() + + +def request_finished(key): + """ + Remove finished Thread from queue. + + :param key: data source key + """ + with event_lock: + threads[key] = threads[key][1:] + + if threads[key]: + threads[key][0].run() + + +def update_events(key, data): + """ + Set the latest events for a key. + + :param key: key to set + :param data: events for key + """ + with event_lock: + event_store[key] = data + + +def latest_events(key): + """ + Get the latest downloaded events for the given key. + + :return: events for key + """ + with event_lock: + # copy data + res = event_store[key][:] + + return res + + +def all_done(key): + """ + Check if requests for the given key are active. + + :param key: key for requests + :return: True if requests are pending or active + """ + with event_lock: + if threads[key]: + return False + return True diff --git a/calendar_utils/icalevents/icalparser.py b/calendar_utils/icalevents/icalparser.py new file mode 100644 index 0000000000000000000000000000000000000000..80161afcea8508954587c62d419ed19ee21c4bc9 --- /dev/null +++ b/calendar_utils/icalevents/icalparser.py @@ -0,0 +1,565 @@ +""" +Parse iCal data to Events. +""" +# for UID generation +from faulthandler import is_enabled +from random import randint +from datetime import datetime, timedelta, date, tzinfo +from typing import Optional + +from dateutil.rrule import rrulestr +from dateutil.tz import UTC, gettz + +from icalendar import Calendar +from icalendar.prop import vDDDLists, vText +from uuid import uuid4 + +from icalendar.windows_to_olson import WINDOWS_TO_OLSON +from pytz import timezone + + +def now(): + """ + Get current time. + + :return: now as datetime with timezone + """ + return datetime.now(UTC) + + +class Attendee(str): + def __init__(self, address): + self.address = address + + def __repr__(self): + return self.address.encode("utf-8").decode("ascii") + + @property + def params(self): + return self.address.params + + +class Event: + """ + Represents one event (occurrence in case of reoccurring events). + """ + + def __init__(self): + """ + Create a new event occurrence. + """ + self.uid = -1 + self.summary = None + self.description = None + self.start = None + self.end = None + self.all_day = True + self.transparent = False + self.recurring = False + self.location = None + self.private = False + self.created = None + self.last_modified = None + self.sequence = None + self.recurrence_id = None + self.attendee = None + self.organizer = None + self.categories = None + self.floating = None + self.status = None + self.url = None + + def time_left(self, time=None): + """ + timedelta form now to event. + + :return: timedelta from now + """ + time = time or now() + return self.start - time + + def __lt__(self, other): + """ + Events are sorted by start time by default. + + :param other: other event + :return: True if start of this event is smaller than other + """ + if not other or not isinstance(other, Event): + raise ValueError( + "Only events can be compared with each other! Other is %s" % type(other) + ) + else: + # start and end can be dates, datetimes and datetimes with timezoneinfo + if type(self.start) is date and type(other.start) is date: + return self.start < other.start + elif type(self.start) is datetime and type(other.start) is datetime: + if self.start.tzinfo == other.start.tzinfo: + return self.start < other.start + else: + return self.start.astimezone(UTC) < other.start.astimezone(UTC) + elif type(self.start) is date and type(other.start) is datetime: + return self.start < other.start.date() + elif type(self.start) is datetime and type(other.start) is date: + return self.start.date() < other.start + + def __str__(self): + return "%s: %s (%s)" % (self.start, self.summary, self.end - self.start) + + def astimezone(self, tzinfo): + + if type(self.start) is datetime: + self.start = self.start.astimezone(tzinfo) + + if type(self.end) is datetime: + self.end = self.end.astimezone(tzinfo) + + return self + + def copy_to(self, new_start=None, uid=None): + """ + Create a new event equal to this with new start date. + + :param new_start: new start date + :param uid: UID of new event + :return: new event + """ + if not new_start: + new_start = self.start + + if not uid: + uid = "%s_%d" % (self.uid, randint(0, 1000000)) + + ne = Event() + ne.summary = self.summary + ne.description = self.description + ne.start = new_start + + if self.end: + duration = self.end - self.start + ne.end = new_start + duration + + ne.all_day = self.all_day + ne.recurring = self.recurring + ne.location = self.location + ne.attendee = self.attendee + ne.organizer = self.organizer + ne.private = self.private + ne.transparent = self.transparent + ne.uid = uid + ne.created = self.created + ne.last_modified = self.last_modified + ne.categories = self.categories + ne.floating = self.floating + ne.status = self.status + ne.url = self.url + + return ne + + +def encode(value: Optional[vText]) -> Optional[str]: + if value is None: + return None + try: + return str(value) + except UnicodeEncodeError: + return str(value.encode("utf-8")) + + +def create_event(component, utc_default): + """ + Create an event from its iCal representation. + + :param component: iCal component + :return: event + """ + + event = Event() + + event.start = component.get("dtstart").dt + # The RFC specifies that the TZID parameter must be specified for datetime or time + # Otherwise we set a default timezone (if only one is set with VTIMEZONE) or utc + event.floating = type(component.get("dtstart").dt) == date and utc_default + + if component.get("dtend"): + event.end = component.get("dtend").dt + elif component.get("duration"): # compute implicit end as start + duration + event.end = event.start + component.get("duration").dt + else: # compute implicit end as start + 0 + event.end = event.start + + event.summary = encode(component.get("summary")) + event.description = encode(component.get("description")) + event.all_day = type(component.get("dtstart").dt) is date + if component.get("rrule"): + event.recurring = True + event.location = encode(component.get("location")) + + if component.get("attendee"): + event.attendee = component.get("attendee") + if type(event.attendee) is list: + event.attendee = [Attendee(attendee) for attendee in event.attendee] + else: + event.attendee = Attendee(event.attendee) + else: + event.attendee = str(None) + + if component.get("uid"): + event.uid = component.get("uid").encode("utf-8").decode("ascii") + else: + event.uid = str(uuid4()) # Be nice - treat every event as unique + + if component.get("organizer"): + event.organizer = component.get("organizer").encode("utf-8").decode("ascii") + else: + event.organizer = str(None) + + if component.get("class"): + event_class = component.get("class") + event.private = event_class == "PRIVATE" or event_class == "CONFIDENTIAL" + + if component.get("transp"): + event.transparent = component.get("transp") == "TRANSPARENT" + + if component.get("created"): + event.created = component.get("created").dt + + if component.get("RECURRENCE-ID"): + rid = component.get("RECURRENCE-ID").dt + + # Spec defines that if DTSTART is a date RECURRENCE-ID also is to be interpreted as a date + if type(component.get("dtstart").dt) is date: + event.recurrence_id = date(year=rid.year, month=rid.month, day=rid.day) + else: + event.recurrence_id = rid + + if component.get("last-modified"): + event.last_modified = component.get("last-modified").dt + elif event.created: + event.last_modified = event.created + + # sequence can be 0 - test for None instead + if not component.get("sequence") is None: + event.sequence = component.get("sequence") + + if component.get("categories"): + categoriesval = component.get("categories") + categories = component.get("categories").cats if hasattr(categoriesval, "cats") else categoriesval + encoded_categories = list() + for category in categories: + encoded_categories.append(encode(category)) + event.categories = encoded_categories + + if component.get("status"): + event.status = encode(component.get("status")) + + if component.get("url"): + event.url = encode(component.get("url")) + + return event + + +def parse_events( + content, + start=None, + end=None, + default_span=timedelta(days=7), + tzinfo=None, + sort=False, + strict=False, +): + """ + Query the events occurring in a given time range. + + :param content: iCal URL/file content as String + :param start: start date for search, default today (in UTC format) + :param end: end date for search (in UTC format) + :param default_span: default query length (one week) + :return: events as list + """ + if not start: + start = now() + + if not end: + end = start + default_span + + if not content: + raise ValueError("Content is invalid!") + + calendar = Calendar.from_ical(content) + + # > Will be deprecated ======================== + # Calendar.from_ical already parses timezones as specified in the ical. + # We can specify a 'default' tz but this is not according to spec. + # Kept this here to verify tests and backward compatibility + + # Keep track of the timezones defined in the calendar + timezones = {} + + # Parse non standard timezone name + if "X-WR-TIMEZONE" in calendar: + x_wr_timezone = str(calendar["X-WR-TIMEZONE"]) + timezones[x_wr_timezone] = get_timezone(x_wr_timezone) + + for c in calendar.walk("VTIMEZONE"): + name = str(c["TZID"]) + try: + timezones[name] = c.to_tz() + except IndexError: + # This happens if the VTIMEZONE doesn't + # contain start/end times for daylight + # saving time. Get the system pytz + # value from the name as a fallback. + timezones[name] = timezone(name) + + # If there's exactly one timezone in the file, + # assume it applies globally, otherwise UTC + utc_default = False + if len(timezones) == 1: + cal_tz = get_timezone(list(timezones)[0]) + else: + utc_default = True + cal_tz = UTC + # < ========================================== + + found = [] + + def add_if_not_exception(event): + exdate = "%04d%02d%02d" % ( + event.start.year, + event.start.month, + event.start.day, + ) + + if exdate not in exceptions: + found.append(event) + + for component in calendar.walk(): + exceptions = {} + + if "EXDATE" in component: + # Deal with the fact that sometimes it's a list and + # sometimes it's a singleton + exlist = [] + if isinstance(component["EXDATE"], vDDDLists): + exlist = component["EXDATE"].dts + else: + exlist = component["EXDATE"] + for ex in exlist: + exdate = ex.to_ical().decode("UTF-8") + exceptions[exdate[0:8]] = exdate + + if component.name == "VEVENT": + e = create_event(component, utc_default) + + # make rule.between happy and provide from, to points in time that have the same format as dtstart + s = component["dtstart"].dt + if type(s) is date and not e.recurring: + f, t = date(start.year, start.month, start.day), date( + end.year, end.month, end.day + ) + elif type(s) is datetime and s.tzinfo: + f, t = datetime( + start.year, start.month, start.day, tzinfo=s.tzinfo + ), datetime(end.year, end.month, end.day, tzinfo=s.tzinfo) + else: + f, t = datetime(start.year, start.month, start.day), datetime( + end.year, end.month, end.day + ) + + if e.recurring: + rule = parse_rrule(component) + for dt in rule.between(f, t, inc=True): + # Recompute the start time in the current timezone *on* the + # date of *this* occurrence. This handles the case where the + # recurrence has crossed over the daylight savings time boundary. + if type(dt) is datetime and dt.tzinfo: + dtstart = dt.replace(tzinfo=get_timezone(str(dt.tzinfo))) + ecopy = e.copy_to( + dtstart.date() if type(s) is date else dtstart, e.uid + ) + else: + ecopy = e.copy_to(dt.date() if type(s) is date else dt, e.uid) + add_if_not_exception(ecopy) + + elif e.end >= f and e.start <= t: + add_if_not_exception(e) + + result = found.copy() + + # Remove events that are replaced in ical + for event in found: + if not event.recurrence_id and (event.uid, event.start) in [ + (f.uid, f.recurrence_id) for f in found + ]: + result.remove(event) + + # > Will be deprecated ======================== + # We will apply default cal_tz as required by some tests. + # This is just here for backward-compatibility + if not strict: + for event in result: + if type(event.start) is date: + event.start = datetime( + year=event.start.year, + month=event.start.month, + day=event.start.day, + hour=0, + minute=0, + tzinfo=cal_tz, + ) + event.end = datetime( + year=event.end.year, + month=event.end.month, + day=event.end.day, + hour=0, + minute=0, + tzinfo=cal_tz, + ) + elif type(event.start) is datetime: + if event.start.tzinfo: + event.start = event.start.astimezone(cal_tz) + event.end = event.end.astimezone(cal_tz) + else: + event.start = event.start.replace(tzinfo=cal_tz) + event.end = event.end.replace(tzinfo=cal_tz) + + if event.created: + if type(event.created) is date: + event.created = datetime( + year=event.created.year, + month=event.created.month, + day=event.created.day, + hour=0, + minute=0, + tzinfo=cal_tz, + ) + if type(event.created) is datetime: + if event.created.tzinfo: + event.created = event.created.astimezone(cal_tz) + else: + event.created = event.created.replace(tzinfo=cal_tz) + + if event.last_modified: + if type(event.last_modified) is date: + event.last_modified = datetime( + year=event.last_modified.year, + month=event.last_modified.month, + day=event.last_modified.day, + hour=0, + minute=0, + tzinfo=cal_tz, + ) + if type(event.last_modified) is datetime: + if event.last_modified.tzinfo: + event.last_modified = event.last_modified.astimezone(cal_tz) + else: + event.last_modified = event.last_modified.replace(tzinfo=cal_tz) + # < ========================================== + + if sort: + result.sort() + + if tzinfo: + result = [event.astimezone(tzinfo) for event in result] + + return result + + +def parse_rrule(component): + """ + Extract a dateutil.rrule object from an icalendar component. Also includes + the component's dtstart and exdate properties. The rdate and exrule + properties are not yet supported. + + :param component: icalendar component + :return: extracted rrule or rruleset or None + """ + + dtstart = component.get("dtstart").dt + + # component['rrule'] can be both a scalar and a list + rrules = component.get("rrule") + if not isinstance(rrules, list): + rrules = [rrules] + + def conform_until(until, dtstart): + if type(dtstart) is datetime: + # If we have timezone defined adjust for daylight saving time + if type(until) is datetime: + return until + abs( + ( + until.astimezone(dtstart.tzinfo).utcoffset() + if until.tzinfo is not None and dtstart.tzinfo is not None + else None + ) + or timedelta() + ) + + return ( + until.astimezone(UTC) + if type(until) is datetime + else datetime( + year=until.year, month=until.month, day=until.day, tzinfo=UTC + ) + ) + ( + (dtstart.tzinfo.utcoffset(dtstart) if dtstart.tzinfo else None) + or timedelta() + ) + + return until.date() + timedelta(days=1) if type(until) is datetime else until + + for index, rru in enumerate(rrules): + if "UNTIL" in rru: + rrules[index]["UNTIL"] = [ + conform_until(until, dtstart) for until in rrules[index]["UNTIL"] + ] + + rule = rrulestr( + "\n".join(x.to_ical().decode() for x in rrules), + dtstart=dtstart, + forceset=True, + unfold=True, + ) + + if component.get("exdate"): + # Add exdates to the rruleset + for exd in extract_exdates(component): + if type(dtstart) is date: + rule.exdate(exd.replace(tzinfo=None)) + else: + rule.exdate(exd) + + # TODO: What about rdates and exrules? + if component.get("exrule"): + pass + + if component.get("rdate"): + pass + + return rule + + +def extract_exdates(component): + """ + Compile a list of all exception dates stored with a component. + + :param component: icalendar iCal component + :return: list of exception dates + """ + dates = [] + exd_prop = component.get("exdate") + if isinstance(exd_prop, list): + for exd_list in exd_prop: + dates.extend(exd.dt for exd in exd_list.dts) + else: # it must be a vDDDLists + dates.extend(exd.dt for exd in exd_prop.dts) + + return dates + + +def get_timezone(tz_name): + if tz_name in WINDOWS_TO_OLSON: + return gettz(WINDOWS_TO_OLSON[tz_name]) + else: + return gettz(tz_name) diff --git a/requirements/base.in b/requirements/base.in index caecafea0775aebba0cef5cd45879aa7bb0da55f..0dde894005a1b57b1d6821db1e8912c4119d6213 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -13,7 +13,6 @@ pirates<=0.7 whitenoise==5.3.0 opencv-python requests -icalevents ics arrow sentry-sdk diff --git a/requirements/base.txt b/requirements/base.txt index 29d9504c7d87a20a1f3d48f3b32f606522d7e7be..1ea97ca2a4ebaaf91048b6247eed88ffafb4b72d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,57 +1,53 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile base.in # -amqp==5.1.1 +amqp==5.2.0 # via kombu anyascii==0.3.2 # via wagtail -appnope==0.1.3 - # via ipython -arrow==1.2.3 +arrow==1.3.0 # via # -r base.in # ics asgiref==3.7.2 # via django -asttokens==2.2.1 +asttokens==2.4.1 # via stack-data -async-timeout==4.0.2 +async-timeout==4.0.3 # via redis -attrs==23.1.0 +attrs==23.2.0 # via # cattrs # ics # requests-cache -backcall==0.2.0 - # via ipython beautifulsoup4==4.11.2 # via # -r base.in # wagtail -billiard==4.1.0 +billiard==4.2.0 # via celery -bleach==6.0.0 +bleach==6.1.0 # via -r base.in -brotli==1.0.9 +brotli==1.1.0 # via fonttools -cattrs==23.1.2 +cattrs==23.2.3 # via requests-cache -celery==5.3.1 +celery==5.3.6 # via -r base.in -certifi==2023.5.7 +certifi==2024.2.2 # via # requests # sentry-sdk -cffi==1.15.1 +cffi==1.16.0 # via # cryptography # weasyprint -charset-normalizer==3.2.0 +charset-normalizer==3.3.2 # via requests -click==8.1.4 +click==8.1.7 # via # celery # click-didyoumean @@ -63,18 +59,16 @@ click-plugins==1.1.1 # via celery click-repl==0.3.0 # via celery -cryptography==41.0.2 +cryptography==42.0.4 # via # josepy # mozilla-django-oidc # pyopenssl cssselect2==0.7.0 # via weasyprint -datetime==4.9 - # via icalevents decorator==5.1.1 # via ipython -django==4.1.10 +django==4.1.13 # via # -r base.in # django-extensions @@ -96,23 +90,23 @@ django-extensions==3.2.3 # via -r base.in django-filter==22.1 # via wagtail -django-modelcluster==6.0 +django-modelcluster==6.2.1 # via wagtail django-permissionedforms==0.1 # via wagtail django-ranged-response==0.2.0 # via django-simple-captcha -django-redis==5.3.0 +django-redis==5.4.0 # via -r base.in django-settings-export==1.2.1 # via -r base.in -django-simple-captcha==0.5.18 +django-simple-captcha==0.5.20 # via -r base.in django-taggit==3.1.0 # via wagtail -django-treebeard==4.7 +django-treebeard==4.7.1 # via wagtail -django-widget-tweaks==1.4.12 +django-widget-tweaks==1.5.0 # via -r base.in djangorestframework==3.14.0 # via wagtail @@ -120,74 +114,64 @@ draftjs-exporter==2.1.7 # via wagtail et-xmlfile==1.1.0 # via openpyxl -exceptiongroup==1.1.2 - # via cattrs -executing==1.2.0 +executing==2.0.1 # via stack-data -fastjsonschema==2.17.1 +fastjsonschema==2.19.1 # via -r base.in -fonttools[woff]==4.40.0 +fonttools[woff]==4.49.0 # via weasyprint html5lib==1.1 # via # wagtail # weasyprint -httplib2==0.20.4 - # via icalevents -icalendar==4.0.9 - # via icalevents -icalevents==0.1.27 - # via -r base.in ics==0.7.2 # via -r base.in -idna==3.4 +idna==3.6 # via requests -ipython==8.14.0 +ipython==8.21.0 # via -r base.in -jedi==0.18.2 +jedi==0.19.1 # via ipython -josepy==1.13.0 +josepy==1.14.0 # via mozilla-django-oidc -kombu==5.3.1 +kombu==5.3.5 # via celery l18n==2021.3 # via wagtail -markdown==3.4.3 +markdown==3.5.2 # via -r base.in matplotlib-inline==0.1.6 # via ipython -mozilla-django-oidc==2.0.0 +mozilla-django-oidc==3.0.0 # via pirates -numpy==1.25.1 +numpy==1.26.4 # via opencv-python oauthlib==3.2.2 # via # requests-oauthlib # tweepy -opencv-python==4.8.0.74 +opencv-python==4.9.0.80 # via -r base.in openpyxl==3.1.2 # via wagtail parso==0.8.3 # via jedi -pexpect==4.8.0 - # via ipython -pickleshare==0.7.5 +pexpect==4.9.0 # via ipython pillow==9.5.0 # via # django-simple-captcha # wagtail # weasyprint -pirates==0.6.0 +pirates==0.7.0 # via -r base.in -platformdirs==3.8.1 +platformdirs==4.2.0 # via requests-cache -prompt-toolkit==3.0.39 +prompt-toolkit==3.0.43 # via # click-repl # ipython -psycopg2-binary==2.9.6 +psycopg2-binary==2.9.9 # via -r base.in ptyprocess==0.7.0 # via pexpect @@ -195,14 +179,12 @@ pure-eval==0.2.2 # via stack-data pycparser==2.21 # via cffi -pydyf==0.7.0 +pydyf==0.8.0 # via weasyprint -pygments==2.15.1 +pygments==2.17.2 # via ipython -pyopenssl==23.2.0 +pyopenssl==24.0.0 # via josepy -pyparsing==3.1.0 - # via httplib2 pypdf2==3.0.1 # via -r base.in pyphen==0.14.0 @@ -211,20 +193,15 @@ python-dateutil==2.8.2 # via # arrow # celery - # icalendar - # icalevents # ics -pytz==2021.3 +pytz==2024.1 # via - # datetime # django-modelcluster # djangorestframework - # icalendar - # icalevents # l18n -pyyaml==6.0 +pyyaml==6.0.1 # via -r base.in -redis==4.6.0 +redis==5.0.1 # via django-redis requests==2.31.0 # via @@ -234,11 +211,11 @@ requests==2.31.0 # requests-oauthlib # tweepy # wagtail -requests-cache==1.1.0 +requests-cache==1.2.0 # via -r base.in requests-oauthlib==1.3.1 # via tweepy -sentry-sdk==1.28.0 +sentry-sdk==1.40.5 # via -r base.in six==1.16.0 # via @@ -249,13 +226,13 @@ six==1.16.0 # l18n # python-dateutil # url-normalize -soupsieve==2.4.1 +soupsieve==2.5 # via beautifulsoup4 sqlparse==0.4.4 # via django -stack-data==0.6.2 +stack-data==0.6.3 # via ipython -tatsu==5.8.3 +tatsu==5.11.3 # via ics telepath==0.3.1 # via wagtail @@ -263,26 +240,24 @@ tinycss2==1.2.1 # via # cssselect2 # weasyprint -traitlets==5.9.0 +traitlets==5.14.1 # via # ipython # matplotlib-inline tweepy==4.14.0 # via -r base.in -typing-extensions==4.7.1 - # via - # asgiref - # cattrs -tzdata==2023.3 +types-python-dateutil==2.8.19.20240106 + # via arrow +tzdata==2024.1 # via celery url-normalize==1.4.3 # via requests-cache -urllib3==2.0.3 +urllib3==2.2.1 # via # requests # requests-cache # sentry-sdk -vine==5.0.0 +vine==5.1.0 # via # amqp # celery @@ -294,13 +269,13 @@ wagtail==4.2.4 # wagtail-trash wagtail-metadata==4.0.3 # via -r base.in -wagtail-trash==1.0.1 +wagtail-trash==2.0.0 # via -r base.in wand==0.6.13 # via -r base.in -wcwidth==0.2.6 +wcwidth==0.2.13 # via prompt-toolkit -weasyprint==59.0 +weasyprint==61.0 # via -r base.in webencodings==0.5.1 # via @@ -312,10 +287,5 @@ whitenoise==5.3.0 # via -r base.in willow==1.4.1 # via wagtail -zope-interface==6.0 - # via datetime -zopfli==0.2.2 +zopfli==0.2.3 # via fonttools - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements/dev.txt b/requirements/dev.txt index bc904efd9d351aecc3d3fd8a1f0503b53af4aa7b..c4562fce41c3f6512ab53e72615cffe5af54639b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,40 +1,38 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile dev.in # asgiref==3.7.2 # via django -coverage[toml]==7.2.7 +coverage[toml]==7.4.2 # via pytest-cov -django==4.1.10 +django==4.1.13 # via # -r dev.in # django-debug-toolbar -django-debug-toolbar==4.1.0 +django-debug-toolbar==4.3.0 # via -r dev.in -exceptiongroup==1.1.2 - # via pytest -factory-boy==3.2.1 +factory-boy==3.3.0 # via pytest-factoryboy -faker==18.13.0 +faker==23.2.1 # via factory-boy fastdiff==0.3.0 # via snapshottest -freezegun==1.2.2 +freezegun==1.4.0 # via pytest-freezegun inflection==0.5.1 # via pytest-factoryboy iniconfig==2.0.0 # via pytest -packaging==23.1 +packaging==23.2 # via # pytest # pytest-sugar -pluggy==1.2.0 +pluggy==1.4.0 # via pytest -pytest==7.4.0 +pytest==8.0.1 # via # -r dev.in # pytest-cov @@ -45,15 +43,15 @@ pytest==7.4.0 # pytest-sugar pytest-cov==4.1.0 # via -r dev.in -pytest-django==4.5.2 +pytest-django==4.8.0 # via -r dev.in -pytest-factoryboy==2.5.1 +pytest-factoryboy==2.6.0 # via -r dev.in pytest-freezegun==0.4.2 # via -r dev.in -pytest-mock==3.11.1 +pytest-mock==3.12.0 # via -r dev.in -pytest-sugar==0.9.7 +pytest-sugar==1.0.0 # via -r dev.in python-dateutil==2.8.2 # via @@ -69,18 +67,12 @@ sqlparse==0.4.4 # via # django # django-debug-toolbar -termcolor==2.3.0 +termcolor==2.4.0 # via # pytest-sugar # snapshottest -tomli==2.0.1 - # via - # coverage - # pytest -typing-extensions==4.7.1 - # via - # asgiref - # pytest-factoryboy +typing-extensions==4.9.0 + # via pytest-factoryboy wasmer==1.1.0 # via fastdiff wasmer-compiler-cranelift==1.1.0 diff --git a/requirements/production.txt b/requirements/production.txt index 4f1ff713e8bdd33fadf6ca9bece8acbfc4575046..74278924de31427cd843cb614e39cdeb8a461afd 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,11 +1,10 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile production.in # -gunicorn==20.1.0 +gunicorn==21.2.0 # via -r production.in - -# The following packages are considered to be unsafe in a requirements file: -# setuptools +packaging==23.2 + # via gunicorn