Clean for rust project
This commit is contained in:
parent
7613441836
commit
317f32565b
11 changed files with 105 additions and 741 deletions
194
.gitignore
vendored
194
.gitignore
vendored
|
@ -1,134 +1,88 @@
|
||||||
.idea/
|
# Generated by Cargo
|
||||||
__pycache__/
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
# 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
|
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# These are backup files generated by rustfmt
|
||||||
.ipynb_checkpoints
|
**/*.rs.bk
|
||||||
|
|
||||||
# IPython
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
profile_default/
|
*.pdb
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
.python-version
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
# pipenv
|
# User-specific stuff
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
.idea/**/workspace.xml
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
.idea/**/tasks.xml
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
.idea/**/usage.statistics.xml
|
||||||
# install all needed dependencies.
|
.idea/**/dictionaries
|
||||||
#Pipfile.lock
|
.idea/**/shelf
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
# AWS User-specific
|
||||||
__pypackages__/
|
.idea/**/aws.xml
|
||||||
|
|
||||||
# Celery stuff
|
# Generated files
|
||||||
celerybeat-schedule
|
.idea/**/contentModel.xml
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
# Sensitive or high-churn files
|
||||||
*.sage.py
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
# Environments
|
# Gradle
|
||||||
.env
|
.idea/**/gradle.xml
|
||||||
.venv
|
.idea/**/libraries
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
# Gradle and Maven with auto-import
|
||||||
.spyderproject
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
.spyproject
|
# 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
|
# CMake
|
||||||
.ropeproject
|
cmake-build-*/
|
||||||
|
|
||||||
# mkdocs documentation
|
# Mongo Explorer plugin
|
||||||
/site
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
# mypy
|
# File-based project format
|
||||||
.mypy_cache/
|
*.iws
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
# IntelliJ
|
||||||
.pyre/
|
out/
|
||||||
app/config/calendar.json
|
|
||||||
/app/cache/
|
# 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
|
||||||
|
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
@ -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
|
9
.idea/ics-fusion.iml
generated
Normal file
9
.idea/ics-fusion.iml
generated
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/ics-fusion.iml" filepath="$PROJECT_DIR$/.idea/ics-fusion.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -1,30 +0,0 @@
|
||||||
from flask import Flask, make_response
|
|
||||||
|
|
||||||
from tools.caching import CacheThread
|
|
||||||
from tools.tools import *
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<calendar>')
|
|
||||||
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)
|
|
|
@ -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))
|
|
|
@ -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
|
|
|
@ -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
|
|
Loading…
Add table
Reference in a new issue