Skip to content
Snippets Groups Projects
Commit 622e533f authored by Alexa Valentová's avatar Alexa Valentová
Browse files

fix calendars (by cloning library to local repository)

parent 98aba54e
No related branches found
No related tags found
2 merge requests!939Release,!937Fix calendars
Pipeline #17008 failed
"""
iCalEvents search all events occurring in a given time frame in an iCal file.
"""
__all__ = ["icaldownload", "icalparser", "icalevents"]
"""
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
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
"""
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)
......@@ -13,7 +13,6 @@ pirates<=0.7
whitenoise==5.3.0
opencv-python
requests
icalevents
ics
arrow
sentry-sdk
......
#
# 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
#
# 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
......
#
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment