From 317f32565b785ccda909eeafb1f04cc6a3144217 Mon Sep 17 00:00:00 2001 From: Jules Dejaeghere Date: Sat, 14 Jan 2023 14:11:38 +0100 Subject: [PATCH] Clean for rust project --- .gitignore | 194 +++++++------------ .idea/.gitignore | 8 + .idea/ics-fusion.iml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + app/__init__.py | 0 app/server.py | 30 --- app/tools/__init__.py | 0 app/tools/caching.py | 151 --------------- app/tools/tools.py | 434 ------------------------------------------ requirements.txt | 6 - 11 files changed, 105 insertions(+), 741 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/ics-fusion.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml delete mode 100644 app/__init__.py delete mode 100644 app/server.py delete mode 100644 app/tools/__init__.py delete mode 100644 app/tools/caching.py delete mode 100644 app/tools/tools.py delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index a054264..d901686 100644 --- a/.gitignore +++ b/.gitignore @@ -1,134 +1,88 @@ -.idea/ -__pycache__/ - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder +# Generated by Cargo +# will have compiled files and executables +debug/ target/ -# Jupyter Notebook -.ipynb_checkpoints +# These are backup files generated by rustfmt +**/*.rs.bk -# IPython -profile_default/ -ipython_config.py +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb -# pyenv -.python-version +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ +# AWS User-specific +.idea/**/aws.xml -# Celery stuff -celerybeat-schedule -celerybeat.pid +# Generated files +.idea/**/contentModel.xml -# SageMath parsed files -*.sage.py +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ +# Gradle +.idea/**/gradle.xml +.idea/**/libraries -# Spyder project settings -.spyderproject -.spyproject +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr -# Rope project settings -.ropeproject +# CMake +cmake-build-*/ -# mkdocs documentation -/site +# Mongo Explorer plugin +.idea/**/mongoSettings.xml -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +# File-based project format +*.iws -# Pyre type checker -.pyre/ -app/config/calendar.json -/app/cache/ +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/ics-fusion.iml b/.idea/ics-fusion.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/ics-fusion.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1c03483 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/server.py b/app/server.py deleted file mode 100644 index ea2665c..0000000 --- a/app/server.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import Flask, make_response - -from tools.caching import CacheThread -from tools.tools import * - -app = Flask(__name__) - - -@app.route('/') -def main(calendar): - conf = calendar + ".json" - - print("Opening " + conf) - - try: - result = str(process(conf)) - response = make_response(result, 200) - response.headers["Content-Disposition"] = "attachment; filename=calendar.ics" - response.headers["Content-Type"] = "text/calendar; charset=utf-8" - - except FileNotFoundError: - response = make_response("Calendar not cached", 425) - - return response - - -thread = CacheThread() -thread.start() - -app.run(host='0.0.0.0', port=8088) diff --git a/app/tools/__init__.py b/app/tools/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/tools/caching.py b/app/tools/caching.py deleted file mode 100644 index f88463a..0000000 --- a/app/tools/caching.py +++ /dev/null @@ -1,151 +0,0 @@ -import json -import os -import sched -import threading -import time -from hashlib import sha256 - -import traceback -import arrow -import requests -from ics import Calendar -from tatsu.exceptions import FailedParse -from tools.tools import horodate, process - - -def cache(entry: dict, scheduler: sched.scheduler = None) -> None: - """Cache an .ics feed in the app/cache directory. - Different entries with the same URL will be cached in the same file. - The cached calendar contains a new line in the description with the current time when cached prefixed by the - 'Cached at' mention - - - - :param entry: representation of the entry to cache. This is the Python representation of the corresponding entry - in the config file - :type entry: dict - - :param scheduler: scheduler used to relaunch the caching task in the future. If not scheduler is specified, - the task will not be relaunched - :type scheduler: sched.scheduler - """ - - try: - if not os.path.isdir('app/cache'): - os.mkdir('app/cache') - - url = entry['url'] - path = "app/cache/" + sha256(url.encode()).hexdigest() + ".ics" - - r = requests.get(entry["url"], allow_redirects=True) - - if "encoding" in entry: - cal = Calendar(imports=r.content.decode(encoding=entry["encoding"])) - else: - cal = Calendar(imports=r.content.decode()) - - cal = horodate(cal, 'Cached at') - open(path, 'w').writelines(cal) - print(arrow.now().format("YYYY-MM-DD HH:mm:ss"), "Cached", entry['name']) - - except FailedParse: - print("Could not parse", entry['name']) - - # Save stack trace when an unknown error occurs - except Exception as e: - with open("error " + arrow.now().format("YYYY-MM-DD HH:mm:ss")+".txt", 'w') as file: - file.write(arrow.now().format("YYYY-MM-DD HH:mm:ss") + "\nCould not cache : " + str(entry)) - file.write(str(e)) - file.write(str(traceback.format_exc())) - finally: - if scheduler is not None: - delay = entry['cache'] if entry['cache'] > 0 else 10 - delay *= 60 - scheduler.enter(delay=delay, priority=1, action=cache, argument=(entry, scheduler)) - - -def precompute(config: str, scheduler: sched.scheduler = None) -> None: - """Precompute a configuration file result to serve it faster when it is requested. This function - should be used with a scheduler to be repeated over time. - - :param config: name of the configuration file to precompute the result for - :type config: str - - scheduler used to relaunch the precomputing task in the future. If not scheduler is specified, - the task will not be relaunched - :type scheduler: sched.scheduler - """ - try: - cal = process(os.path.basename(config), False) - path = "app/cache/" + os.path.basename(config).rstrip('.json') + ".ics" - open(path, 'w').writelines(cal) - print(arrow.now().format("YYYY-MM-DD HH:mm:ss"), "Precomputed", os.path.basename(config).rstrip('.json')) - - except Exception as e: - with open("error " + arrow.now().format("YYYY-MM-DD HH:mm:ss")+".txt", 'w') as file: - file.write(arrow.now().format("YYYY-MM-DD HH:mm:ss") + "\nCould not precompute : " + str(config)) - file.write(str(e)) - file.write(str(traceback.format_exc())) - finally: - if scheduler is not None: - delay = get_min_cache(config) - delay *= 60 - scheduler.enter(delay=delay, priority=1, action=precompute, argument=(config, scheduler)) - - -def get_min_cache(path: str) -> float: - """Get the minimum caching time of all the entries in a config file. - - :param path: path of the config file to use - :type path: str - - :return: float number representing the smallest caching time. - """ - result = float('inf') - - with open(path, 'r') as config_file: - file = json.loads(config_file.read()) - - for entry in file: - if 'cache' in entry and entry['cache'] < result: - result = entry['cache'] - - return result - - -def start_scheduler(scheduler: sched.scheduler) -> None: - """Start the caching of every config file found in the app/config directory - - - :param scheduler: scheduler object to use to schedule the caching - :type scheduler: sched.scheduler - """ - - path = "app/config" - files = [os.path.join(path, f) for f in os.listdir(path) - if os.path.isfile(os.path.join(path, f)) and f.endswith('.json')] - - for file in files: - with open(file, 'r') as config_file: - config = json.loads(config_file.read()) - - for entry in config: - if 'cache' in entry: - scheduler.enter(delay=0, priority=1, action=cache, argument=(entry, scheduler)) - - if get_min_cache(file) < float('inf'): - scheduler.enter(delay=get_min_cache(file)*60, priority=1, action=precompute, argument=(file, scheduler)) - - scheduler.run() - - -class CacheThread(threading.Thread): - """Child class of the threading.Thread class to run the caching process every 10 minutes - """ - - def __init__(self): - threading.Thread.__init__(self) - - def run(self): - print("Starting cache process") - start_scheduler(sched.scheduler(time.time, time.sleep)) diff --git a/app/tools/tools.py b/app/tools/tools.py deleted file mode 100644 index e0aa8e2..0000000 --- a/app/tools/tools.py +++ /dev/null @@ -1,434 +0,0 @@ -"""This module provides methods to combines multiples .ics feeds into a single object. - -The methods allow to filter the events to keep in the final object and to modify the remaining event -according to a configuration file. - -The JSON configuration file used in this module has the following structure - -[ - { - "url":"str", - "name":"str", - "encoding":"str", - "filters":{ - "name":{ - "exclude":"RegEx", - "includeOnly":"RegEx", - "ignoreCase":true - }, - "description":{ - "exclude":"RegEx", - "includeOnly":"RegEx", - "ignoreCase":true - } - }, - "modify":{ - "time":{ - "shift":{ - "year":0, - "month":0, - "day":0, - "hour":0, - "minute":0 - } - }, - "name":{ - "addPrefix":"str", - "addSuffix":"str" - }, - "description":{ - "addPrefix":"str", - "addSuffix":"str" - }, - "location":{ - "addPrefix":"str", - "addSuffix":"str" - } - } - } -] - -Only the url and the name field are mandatory. -- url: specify the url to find the calendar -- name: name to identify the calendar -- encoding: specify the encoding to use - -- filters: structure defining the filters to apply to the calendar -- name: filters to apply to the name field of the events -- description: filters to apply to the name field of the events -- exclude: RegEx to describe the events to exclude - cannot be specified with includeOnly -- includeOnly: RegEx to describe the events to include - cannot be specified with exclude -- ignoreCase: if true the RegEx will ignore the case of the field - -- modify: structure defining the modifications to the events of the calendar -- time: describe the modifications to apply to the timing of the event -- shift: shift the event of a certain amount of time -- year, month, day, hour, minute: amount of time to add to the events -- name: modifications to apply to the name of the events -- description: modifications to apply to the description of the events -- location: modification to apply to the location of the events -- addPrefix: string to add at the beginning of the field -- addSuffix: string to add at the end of the field -""" - -import json -import re -import arrow -import os -from hashlib import sha256 -from typing import List - -import requests -from ics import Calendar -from pathvalidate import sanitize_filename - - -def filtering(cal: Calendar, filters: dict, field_name: str) -> Calendar: - """Filter the event of a calendar according to the filters and the field_name - - - :param cal: the calendar to apply filters to - :type cal: Calendar - - :param filters: the filters to apply to the calendar - :type filters: dict - - :param field_name: the of the field in the filters to consider - :type field_name: str - - - :return: the modified cal argument after filtering out the events - :rtype: Calendar - - - :raises SyntaxError: if both exclude and includeOnly are specified in the filters - """ - - if field_name in filters: - field = filters[field_name] - - if ("exclude" in field) and ("includeOnly" in field): - raise SyntaxError("Cannot specify both exclude and includeOnly") - - if ("exclude" not in field) and ("includeOnly" not in field): - return cal - - new = Calendar() - - ignore_case = True if ("ignoreCase" in field and field["ignoreCase"]) else False - - if "exclude" in field: - p = re.compile(field["exclude"], re.IGNORECASE | re.DOTALL) \ - if ignore_case else re.compile(field["exclude"], re.DOTALL) - - for event in cal.events: - if event.name is None or (field_name == "name" and p.match(event.name) is None): - new.events.add(event) - elif event.description is None or (field_name == "description" and p.match(event.description) is None): - new.events.add(event) - - if "includeOnly" in field: - p = re.compile(field["includeOnly"], re.IGNORECASE | re.DOTALL) \ - if ignore_case else re.compile(field["includeOnly"], re.DOTALL) - - for event in cal.events: - if field_name == "name" and event.name is not None and p.match(event.name) is not None: - new.events.add(event) - elif field_name == "description" and event.description is not None \ - and p.match(event.description) is not None: - new.events.add(event) - - cal = new - return cal - - else: - return cal - - -def apply_filters(cal: Calendar, filters: dict) -> Calendar: - """Apply all the filters to a calendar and returns the resulting calendar - - - :param cal: the calendar to apply filters to - :type cal: Calendar - - :param filters: the filters to apply - :type filters: dict - - - :return: the modified cal parameter to satisfy the filters - :rtype: Calendar - - :raises SyntaxError: if both exclude and includeOnly are specified for the same field in the filters - """ - - cal = filtering(cal, filters, "name") - cal = filtering(cal, filters, "description") - - return cal - - -def modify_time(cal: Calendar, modify: dict) -> Calendar: - """Modify the time of all the events in a calendar as specified in the modify structure - - - :param cal: the calendar where it is needed to modify the time of the events - :type cal: Calendar - - :param modify: the structure defining how to modify the time - :type modify: dict - - - :return: the modified cal parameter - :rtype: Calendar - """ - - if ("time" in modify) and ("shift" in modify["time"]): - shift = modify["time"]["shift"] - - year = 0 if not ("year" in shift) else shift["year"] - month = 0 if not ("month" in shift) else shift["month"] - day = 0 if not ("day" in shift) else shift["day"] - hour = 0 if not ("hour" in shift) else shift["hour"] - minute = 0 if not ("minute" in shift) else shift["minute"] - - for event in cal.events: - event.end = event.end.shift(years=year, months=month, days=day, hours=hour, minutes=minute) - event.begin = event.begin.shift(years=year, months=month, days=day, hours=hour, minutes=minute) - - return cal - - -def modify_text(cal: Calendar, modify: dict, field_name: str) -> Calendar: - """Modify one text field (name, location, description) of all the events in the cal parameter - according to the modify structure and the field_name - - - :param cal: the calendar where it is needed to modify the text field - :type cal: Calendar - - :param modify: the structure defining how to modify the time - :type modify: dict - - :param field_name: the name of the field to modify - :type field_name: str - - - :return: the modified cal parameter - :rtype: Calendar - """ - - if field_name in modify: - change = modify[field_name] - - if "addPrefix" in change: - for event in cal.events: - - if field_name == "name": - event.name = change["addPrefix"] + event.name \ - if event.name is not None else change["addPrefix"] - - elif field_name == "description": - event.description = change["addPrefix"] + event.description \ - if event.description is not None else change["addPrefix"] - - elif field_name == "location": - event.location = change["addPrefix"] + event.location \ - if event.location is not None else change["addPrefix"] - - if "addSuffix" in change: - for event in cal.events: - - if field_name == "name": - event.name = event.name + change["addSuffix"] \ - if event.name is not None else change["addSuffix"] - - elif field_name == "description": - event.description = event.description + change["addSuffix"] \ - if event.description is not None else change["addSuffix"] - - elif field_name == "location": - event.location = event.location + change["addSuffix"] \ - if event.location is not None else change["addSuffix"] - - return cal - - -def apply_modify(cal: Calendar, modify: dict) -> Calendar: - """Apply all the needed modifications to a calendar and returns the resulting calendar - - - :param cal: the calendar to apply modifications to - :type cal: Calendar - - :param modify: the structure containing the modifications to apply - :type modify: dict - - - :return: the modified cal parameter - :rtype: Calendar - """ - - cal = modify_time(cal, modify) - cal = modify_text(cal, modify, "name") - cal = modify_text(cal, modify, "description") - cal = modify_text(cal, modify, "location") - return cal - - -def merge(cals: List[Calendar]) -> Calendar: - """Merge a list of calendars into a single calendar - Only takes the event into account, not the tasks or the alarms - - - :param cals: the list of calendars to merge - :type cals: List[Calendar] - - - :return: the calendar containing the union of the events contained in the cals list - :rtype: Calendar - - - :raises ValueError: if an element of the list is not a Calendar - """ - - result = Calendar() - - for cal in cals: - if not isinstance(cal, Calendar): - raise ValueError("All elements should be Calendar") - result.events = result.events.union(cal.events) - - return result - - -def process(path: str, from_cache: bool = True) -> Calendar: - """Open a config file from the specified path, download the calendars, - apply the filters, modify and merge the calendars as specified in the config file - - - :param from_cache: - :param path: name of the file to open. The file should be in the config/ folder - :type path: str - - - :return: the resulting calendar - :rtype: Calendar - """ - print("app/cache/" + sanitize_filename(path).rstrip(".json") + ".ics") - if from_cache and os.path.isfile("app/cache/" + sanitize_filename(path).rstrip(".json") + ".ics"): - with open("app/cache/" + sanitize_filename(path).rstrip(".json") + ".ics") as file: - data = file.read() - print("Serving precomputed file") - return Calendar(imports=data) - - else: - o = "app/config/" + sanitize_filename(path) - print("Try to open " + o) - file = open(o, "r") - config = json.loads(file.read()) - file.close() - - data = [] - - for entry in config: - - cal = load_cal(entry) - - if "filters" in entry: - cal = apply_filters(cal, entry["filters"]) - - if "modify" in entry: - cal = apply_modify(cal, entry["modify"]) - - data.append(cal) - - return merge(data) - - -def get_from_cache(entry: dict) -> Calendar: - """Retrieve the entry from cache. If the entry is not found, an exception is raised - - - :param entry: representation of the entry to cache. This is the Python representation of the corresponding entry - in the config file - :type entry: dict - - - :return: the corresponding calendar in cache - :rtype: Calendar - - - :raises FileNotfoundError: if the entry has not been cached before - """ - - url = entry['url'] - path = "app/cache/" + sha256(url.encode()).hexdigest() + ".ics" - if not os.path.isfile(path): - print("Not cached") - raise FileNotFoundError("The calendar is not cached") - - with open(path, 'r') as file: - data = file.read() - - return Calendar(imports=data) - - -def load_cal(entry: dict) -> Calendar: - """Load the calendar from the cache or from remote according to the entry. If the calendar is supposed to be in - cached but could not be found in cache, an error is thrown - - - :param entry: representation of the entry to cache. This is the Python representation of the corresponding entry - in the config file - :type entry: dict - - - :return: the calendar corresponding to the entry - :rtype: Calendar - - - :raises FileNotfoundError: if the entry was supposed to be cached but has not been cached before - """ - - if "cache" in entry and entry["cache"]: - print("Getting", entry["name"], "from cache") - try: - return get_from_cache(entry) - except FileNotFoundError: - return Calendar() - - else: - print("Getting", entry["name"], "from remote") - r = requests.get(entry["url"], allow_redirects=True) - if "encoding" in entry: - cal = Calendar(imports=r.content.decode(encoding=entry["encoding"])) - else: - cal = Calendar(imports=r.content.decode()) - - cal = horodate(cal, 'Downloaded at') - return cal - - -def horodate(cal: Calendar, prefix='') -> Calendar: - """Add a new line at the end of the description of every event in the calendar with the current time prefixed by - the prefix parameter and a space - The date is added with the following format: YYYY-MM-DD HH:mm:ss - - - :param cal: calendar to process - :type cal: Calendar - - :param prefix: the prefix to add in front of the date - :type prefix: str - - - :return: the modified calendar - :rtype: Calendar - """ - now = arrow.now().format("YYYY-MM-DD HH:mm:ss") - for event in cal.events: - event.description = event.description + '\n' + prefix + ' ' + now \ - if event.description is not None else prefix + ' ' + now - - return cal diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c3b9d5a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -requests~=2.22.0 -ics~=0.7 -pathvalidate~=2.3.0 -flask~=1.1.1 -arrow~=0.14.7 -tatsu~=4.4.0 \ No newline at end of file