Enhance timeshift modify #4

Closed
ajasnz wants to merge 10 commits from dev-timeshift into master
3 changed files with 114 additions and 13 deletions

4
.gitignore vendored
View file

@ -132,3 +132,7 @@ dmypy.json
.pyre/
app/config/calendar.json
/app/cache/
# Development directories
/app/config/
.vscode

View file

@ -1,4 +1,5 @@
# ICS Fusion
Forked from [https://github.com/jdejaegh/ics-fusion](https://github.com/jdejaegh/ics-fusion)
## Introduction
ICS Fusion is a tool to merge multiple ics feed into a single ics calendar. Filters and modifications may be applied on the incoming feeds. The resulting ics can be accessed via an HTTP endpoint.
@ -30,6 +31,11 @@ The JSON configuration file should look like the following.
```json
[
{
"conf": true,
"extends": "str",
"extendFail": "str",
},
{
"url":"str",
"name":"str",
@ -59,15 +65,18 @@ The JSON configuration file should look like the following.
},
"name":{
"addPrefix":"str",
"addSuffix":"str"
"addSuffix":"str",
"redactAs":"str"
},
"description":{
"addPrefix":"str",
"addSuffix":"str"
"addSuffix":"str",
"redactAs":"str"
},
"location":{
"addPrefix":"str",
"addSuffix":"str"
"addSuffix":"str",
"redactAs":"str"
}
}
}
@ -98,11 +107,23 @@ Only the `url` and the `name` field are mandatory.
- `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
- `redactAs`: Replaces the content of the field with the specified string
If multiple calendars are specified in the configuration list, their events will be merged in the resulting ics feed.
The first dataset with {"conf": "true",} specifies options that are globally applied to all calenders in the conf. Omit this set to disable. Options
- `extends`: string specifying the name (excluding .json) of another config file to extend.
- `extendFail`: string speciying the action to take if an extend fails, either "fail" or "ignore". Default is "fail".
## Usage
Once the config file is created, the corresponding HTTP endpoint is accessible. For example, if the file `app/config/my-calendar.json` contains the configuration, the HTTP endpoint will be `http://localhost:8088/my-calendar`.
A config can extend another config file, to do this the extended config should contain begin with`{
"conf": true,
"extends": <name of calendar>,
"extendFail": "fail",
},`
For an extend to work calendars MUST share the same name between the configs
An extending config cannot remove data from a base calendar but can modify fields
## Limitations
Currently, the application only merges events of the ics feeds, the alarms and todos are not supported.

View file

@ -34,15 +34,18 @@ The JSON configuration file used in this module has the following structure
},
"name":{
"addPrefix":"str",
"addSuffix":"str"
"addSuffix":"str",
"redactAs":"str"
},
"description":{
"addPrefix":"str",
"addSuffix":"str"
"addSuffix":"str",
"redactAs":"str"
},
"location":{
"addPrefix":"str",
"addSuffix":"str"
"addSuffix":"str",
"redactAs":"str"
}
}
}
@ -69,6 +72,7 @@ Only the url and the name field are mandatory.
- 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
- redactAs: string to replace the field with
"""
import json
@ -192,9 +196,16 @@ def modify_time(cal: Calendar, modify: dict) -> Calendar:
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)
shift_minutes = (year * 365 * 24 * 60) + (month * 30 * 24 * 60) + (day * 24 * 60) + (hour * 60) + minute
if shift_minutes > 0:
for event in cal.events:
event.end = event.end.shift(minutes=shift_minutes)
event.begin = event.begin.shift(minutes=shift_minutes)
elif shift_minutes < 0:
for event in cal.events:
event.begin = event.begin.shift(minutes=shift_minutes)
event.end = event.end.shift(minutes=shift_minutes)
return cal
@ -250,6 +261,18 @@ def modify_text(cal: Calendar, modify: dict, field_name: str) -> Calendar:
elif field_name == "location":
event.location = event.location + change["addSuffix"] \
if event.location is not None else change["addSuffix"]
if "redactAs" in change:
for event in cal.events:
if field_name == "name":
event.name = change["redactAs"]
elif field_name == "description":
event.description = change["redactAs"]
elif field_name == "location":
event.location = change["redactAs"]
return cal
@ -330,9 +353,31 @@ def process(path: str, from_cache: bool = True) -> Calendar:
file.close()
data = []
for entry in config:
if "conf" in entry:
if entry.get("extends", None) is not None:
try:
o = "app/config/" + sanitize_filename(entry["extends"]) + ".json"
print("Try to open " + o)
file = open(o, "r")
baseConfig = json.loads(file.read())
file.close()
extendingConfig = config
try:
config = merge_json(baseConfig, extendingConfig)
except:
config = extendingConfig
except:
if entry.get("extendFail", "fail") == "fail":
raise FileNotFoundError("The calendar is not cached")
else:
pass
for entry in config:
for entry in config:
if "conf" not in entry:
cal = load_cal(entry)
if "filters" in entry:
@ -343,7 +388,7 @@ def process(path: str, from_cache: bool = True) -> Calendar:
data.append(cal)
return merge(data)
return merge(data)
def get_from_cache(entry: dict) -> Calendar:
@ -406,7 +451,7 @@ def load_cal(entry: dict) -> Calendar:
else:
cal = Calendar(imports=r.content.decode())
cal = horodate(cal, 'Downloaded at')
cal = horodate(cal, 'Event last fetched: ')
return cal
@ -432,3 +477,34 @@ def horodate(cal: Calendar, prefix='') -> Calendar:
if event.description is not None else prefix + ' ' + now
return cal
def merge_json(base, extension):
"""Merges two config files by updating the value of base with the values in extension.
:param base: the base config file
:type base: dict
:param extension: the config file to merge with the base
:type extension: dict
:return: the merged config file
:rtype: dict
"""
new_json = base.copy()
def update_json(base_set, updates):
for key, value in updates.items():
if not key == "conf":
if isinstance(value, dict) and key in base_set and isinstance(base_set[key], dict):
update_json(base_set[key], value)
else:
base_set[key] = value
for base_dataset in new_json:
if "conf" not in base_dataset:
for ext_dataset in extension:
if base_dataset.get("name") == ext_dataset.get("name"):
update_json(base_dataset, ext_dataset)
return new_json