icalcli 1.1__tar.gz → 1.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {icalcli-1.1 → icalcli-1.1.3}/PKG-INFO +1 -1
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etebase_crud.py +74 -26
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etebase_interface.py +11 -7
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/icalcli.py +17 -5
- {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/PKG-INFO +1 -1
- {icalcli-1.1 → icalcli-1.1.3}/setup.py +1 -1
- {icalcli-1.1 → icalcli-1.1.3}/LICENSE +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/README.md +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/__init__.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/argparsers.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/__init__.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etesync_crud.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etesync_interface.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/file_backend/__init__.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/file_backend/ics_interface.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/printer.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli/utils.py +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/SOURCES.txt +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/dependency_links.txt +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/entry_points.txt +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/requires.txt +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/top_level.txt +0 -0
- {icalcli-1.1 → icalcli-1.1.3}/setup.cfg +0 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
# this is part of the etesync 2.0 backend
|
|
4
4
|
# for etesync 1.0 backend see etesync_crud.py
|
|
5
5
|
|
|
6
|
-
from etebase import Client, Account, FetchOptions
|
|
6
|
+
from etebase import Client, Account, FetchOptions, Base64Url
|
|
7
7
|
from time import sleep, time
|
|
8
|
+
import json
|
|
8
9
|
|
|
9
10
|
# The EtesyncCRUD class exposes methods for each of the CRUD operations
|
|
10
11
|
# (Create, Retrieve, Update and Delete) and for sync with the server.
|
|
@@ -31,7 +32,9 @@ from time import sleep, time
|
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class EtebaseCRUD:
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
def __init__(self, user, server_url, password, calendar_uid,
|
|
37
|
+
cache_file=None, silent=True):
|
|
35
38
|
"""Initialize
|
|
36
39
|
|
|
37
40
|
Parameters
|
|
@@ -40,77 +43,95 @@ class EtebaseCRUD:
|
|
|
40
43
|
password : etebase password
|
|
41
44
|
server_url : url of etebase server
|
|
42
45
|
calendar_uid : uid of calendar
|
|
46
|
+
cache_file: path to the cache file or None
|
|
43
47
|
"""
|
|
44
48
|
client = Client(user, server_url)
|
|
45
49
|
etebase = Account.login(client, user, password)
|
|
46
50
|
col_mgr = etebase.get_collection_manager()
|
|
47
51
|
collection = col_mgr.fetch(calendar_uid)
|
|
48
52
|
self.item_mgr = col_mgr.get_item_manager(collection)
|
|
53
|
+
self.items = {}
|
|
54
|
+
self.stoken = None
|
|
55
|
+
self.raw_events = []
|
|
56
|
+
self.cache_file = cache_file
|
|
57
|
+
self.load_cache()
|
|
49
58
|
self.sync(silent)
|
|
59
|
+
print("Reading all events")
|
|
60
|
+
self.get_all_events()
|
|
50
61
|
|
|
51
|
-
def create_event(self, event,
|
|
62
|
+
def create_event(self, event, event_uid):
|
|
52
63
|
"""Create event
|
|
53
64
|
|
|
54
65
|
Parameters
|
|
55
66
|
----------
|
|
56
67
|
event : iCalendar file as bytes
|
|
57
68
|
(calendar containing one event to be added)
|
|
69
|
+
event_uid : uid of event to be updated
|
|
58
70
|
"""
|
|
59
71
|
item = self.item_mgr.create(
|
|
60
72
|
{
|
|
61
|
-
"name":
|
|
73
|
+
"name": event_uid,
|
|
62
74
|
"mtime": int(round(time() * 1000))
|
|
63
75
|
},
|
|
64
76
|
event
|
|
65
77
|
)
|
|
66
78
|
self.item_mgr.batch([item])
|
|
67
79
|
|
|
68
|
-
def update_event(self, event,
|
|
80
|
+
def update_event(self, event, event_uid):
|
|
69
81
|
"""Edit event
|
|
70
82
|
|
|
71
83
|
Parameters
|
|
72
84
|
----------
|
|
73
85
|
event : iCalendar file as bytes
|
|
74
86
|
(calendar containing one event to be updated)
|
|
75
|
-
|
|
87
|
+
event_uid : uid of event to be updated
|
|
76
88
|
"""
|
|
77
|
-
item = self.item_mgr.fetch(
|
|
89
|
+
item = self.item_mgr.fetch(self.event_uid_to_item_uid[event_uid])
|
|
90
|
+
assert item.meta['name'] == event_uid
|
|
78
91
|
item.content = event
|
|
92
|
+
# item.meta["mtime"] = int(round(time() * 1000))
|
|
79
93
|
self.item_mgr.batch([item])
|
|
80
94
|
|
|
81
|
-
def retrieve_event(self,
|
|
95
|
+
def retrieve_event(self, event_uid):
|
|
82
96
|
r"""Retrieve event by uid
|
|
83
97
|
|
|
84
98
|
Parameters
|
|
85
99
|
----------
|
|
86
|
-
|
|
100
|
+
event_uid : uid of event to be retrieved
|
|
87
101
|
|
|
88
102
|
Returns
|
|
89
103
|
-------
|
|
90
104
|
iCalendar file (as a string)
|
|
91
105
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"""Retrieve all events in calendar
|
|
96
|
-
|
|
97
|
-
Returns
|
|
98
|
-
-------
|
|
99
|
-
List of iCalendar files (as strings)
|
|
100
|
-
"""
|
|
101
|
-
return [e.content.decode() for e in self.items.data if not e.deleted]
|
|
106
|
+
item = self.item_mgr.fetch(self.event_uid_to_item_uid[event_uid])
|
|
107
|
+
assert item.meta['name'] == event_uid
|
|
108
|
+
return item.content.decode()
|
|
102
109
|
|
|
103
|
-
def delete_event(self,
|
|
110
|
+
def delete_event(self, event_uid):
|
|
104
111
|
"""Delete event and sync calendar
|
|
105
112
|
|
|
106
113
|
Parameters
|
|
107
114
|
----------
|
|
108
115
|
uid : uid of event to be deleted
|
|
109
116
|
"""
|
|
110
|
-
item = self.item_mgr.fetch(
|
|
117
|
+
item = self.item_mgr.fetch(self.event_uid_to_item_uid[event_uid])
|
|
118
|
+
assert item.meta['name'] == event_uid
|
|
119
|
+
item.meta["mtime"] = int(round(time() * 1000))
|
|
111
120
|
item.delete()
|
|
112
121
|
self.item_mgr.batch([item])
|
|
113
122
|
|
|
123
|
+
def get_all_events(self):
|
|
124
|
+
"""Retrieve all events in calendar
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
List of iCalendar files (as strings)
|
|
129
|
+
"""
|
|
130
|
+
self.event_uid_to_item_uid = {
|
|
131
|
+
e.meta['name']: e.uid for e in self.items.values()}
|
|
132
|
+
self.raw_events = [e.content.decode()
|
|
133
|
+
for e in self.items.values() if not e.deleted]
|
|
134
|
+
|
|
114
135
|
def sync(self, silent=False):
|
|
115
136
|
"""Initialize
|
|
116
137
|
|
|
@@ -121,17 +142,44 @@ class EtebaseCRUD:
|
|
|
121
142
|
silent or print("Syncing with server. Please wait")
|
|
122
143
|
msg = "etebase fetch attempt {:} failed. Will retry after {:} seconds"
|
|
123
144
|
delay = 5
|
|
124
|
-
|
|
145
|
+
done = False
|
|
146
|
+
chunk = 100
|
|
125
147
|
for i in range(5):
|
|
126
148
|
try:
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
while not done:
|
|
150
|
+
items = self.item_mgr.list(
|
|
151
|
+
FetchOptions().stoken(self.stoken).limit(chunk))
|
|
152
|
+
self.items.update(
|
|
153
|
+
{item.uid: item for item in items.data})
|
|
154
|
+
self.stoken = items.stoken
|
|
155
|
+
done = items.done
|
|
156
|
+
# silent or print(".", end='')
|
|
129
157
|
break
|
|
130
158
|
except Exception:
|
|
131
159
|
silent or print(msg.format(i+1, delay))
|
|
132
160
|
sleep(delay)
|
|
133
|
-
if
|
|
161
|
+
if done:
|
|
134
162
|
silent or print("Syncing completed.")
|
|
135
|
-
self.
|
|
163
|
+
self.save_cache()
|
|
136
164
|
else:
|
|
137
165
|
print("Syncing with server failed after 5 attempts")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
def load_cache(self):
|
|
169
|
+
if self.cache_file:
|
|
170
|
+
d = json.load(open(self.cache_file))
|
|
171
|
+
if 'stoken' in d:
|
|
172
|
+
self.stoken = d['stoken']
|
|
173
|
+
if 'blobs' in d:
|
|
174
|
+
for cache_blob in d['blobs']:
|
|
175
|
+
item = self.item_mgr.cache_load(
|
|
176
|
+
Base64Url.from_base64(cache_blob))
|
|
177
|
+
self.items[item.uid] = item
|
|
178
|
+
|
|
179
|
+
def save_cache(self):
|
|
180
|
+
if self.cache_file:
|
|
181
|
+
cache = dict(
|
|
182
|
+
stoken=self.stoken,
|
|
183
|
+
blobs=[Base64Url.to_base64(self.item_mgr.cache_save(item))
|
|
184
|
+
for item in self.items.values()])
|
|
185
|
+
json.dump(cache, open(self.cache_file, 'w'))
|
|
@@ -26,15 +26,16 @@ from icalendar import Calendar
|
|
|
26
26
|
# etes.create_event, etes.retrieve_event,
|
|
27
27
|
# etes.update_event and etes.delete_event
|
|
28
28
|
|
|
29
|
-
# The calling program must explicitly call
|
|
30
|
-
#
|
|
31
|
-
# (
|
|
29
|
+
# The calling program must explicitly call sync() when the server
|
|
30
|
+
# has been updated from another device
|
|
31
|
+
# sync() is not needed after any CRUD operation
|
|
32
32
|
|
|
33
33
|
# No exception handling is done. That is left to the calling program.
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class EtebaseInterface (EtebaseCRUD):
|
|
37
|
-
def __init__(self, user, server_url, password, calendar_uid,
|
|
37
|
+
def __init__(self, user, server_url, password, calendar_uid,
|
|
38
|
+
cache_file=None, silent=True):
|
|
38
39
|
r"""Initialize EtebaseInterface
|
|
39
40
|
|
|
40
41
|
Parameters
|
|
@@ -45,12 +46,14 @@ class EtebaseInterface (EtebaseCRUD):
|
|
|
45
46
|
calendar_uid : uid of calendar
|
|
46
47
|
"""
|
|
47
48
|
super(EtebaseInterface, self).__init__(
|
|
48
|
-
user, server_url, password,
|
|
49
|
+
user=user, server_url=server_url, password=password,
|
|
50
|
+
calendar_uid=calendar_uid, cache_file=cache_file, silent=silent)
|
|
51
|
+
print("Parsing all events")
|
|
49
52
|
self.all_events()
|
|
50
53
|
|
|
51
54
|
def all_events(self):
|
|
52
55
|
self.events = [Calendar.from_ical(ev).walk('VEVENT')[0]
|
|
53
|
-
for ev in
|
|
56
|
+
for ev in self.raw_events]
|
|
54
57
|
|
|
55
58
|
def create_event(self, event, vtimezone=None):
|
|
56
59
|
r"""Create event
|
|
@@ -60,7 +63,8 @@ class EtebaseInterface (EtebaseCRUD):
|
|
|
60
63
|
event : event to be added (iCalendar object)
|
|
61
64
|
"""
|
|
62
65
|
ics = self.event_to_ics(event, vtimezone)
|
|
63
|
-
|
|
66
|
+
uid = event.decoded('uid').decode()
|
|
67
|
+
EtebaseCRUD.create_event(self, ics, uid)
|
|
64
68
|
|
|
65
69
|
def update_event(self, event, vtimezone=None):
|
|
66
70
|
r"""Update event
|
|
@@ -1317,7 +1317,8 @@ class IcalendarInterface:
|
|
|
1317
1317
|
continue
|
|
1318
1318
|
self.backend_interface.delete_event(self.uid(event))
|
|
1319
1319
|
self.printer.msg("Event deleted\n")
|
|
1320
|
-
self.
|
|
1320
|
+
if self.backend_interface.sync_after_edit:
|
|
1321
|
+
self.backend_cache_dirty = True
|
|
1321
1322
|
deleted += 1
|
|
1322
1323
|
self.printer.msg(f'{deleted} events deleted\n')
|
|
1323
1324
|
|
|
@@ -1454,6 +1455,8 @@ class IcalendarInterface:
|
|
|
1454
1455
|
return self.raw_ics(original)
|
|
1455
1456
|
default_event_duration = timedelta(minutes=30)
|
|
1456
1457
|
old = None
|
|
1458
|
+
if args.summary:
|
|
1459
|
+
summary = args.summary
|
|
1457
1460
|
if original:
|
|
1458
1461
|
# simulate old = original.deepcopy()
|
|
1459
1462
|
old = Calendar.from_ical(original.to_ical().decode())
|
|
@@ -1463,7 +1466,7 @@ class IcalendarInterface:
|
|
|
1463
1466
|
('duration' in old and old.Decoded('duration')) or
|
|
1464
1467
|
(old.Decoded('dtend') - old.Decoded('dtstart')))
|
|
1465
1468
|
if not args.summary:
|
|
1466
|
-
|
|
1469
|
+
summary = old.Decoded('summary').decode()
|
|
1467
1470
|
if (
|
|
1468
1471
|
not args.time and not (args.start and 'T' in args.start)
|
|
1469
1472
|
and self.isallday(old)
|
|
@@ -1477,7 +1480,7 @@ class IcalendarInterface:
|
|
|
1477
1480
|
gethostname())
|
|
1478
1481
|
# old_start = self.now
|
|
1479
1482
|
# old_duration = None
|
|
1480
|
-
if not
|
|
1483
|
+
if not summary:
|
|
1481
1484
|
raise Exception('Summary must be specified')
|
|
1482
1485
|
day = args.day or self.now.day
|
|
1483
1486
|
month = args.month or self.now.month + (
|
|
@@ -1563,7 +1566,7 @@ class IcalendarInterface:
|
|
|
1563
1566
|
add_or_change(event, field, parsed_value)
|
|
1564
1567
|
|
|
1565
1568
|
add_or_change(event, 'last-modified', dtstamp)
|
|
1566
|
-
add_or_change(event, 'summary',
|
|
1569
|
+
add_or_change(event, 'summary', summary)
|
|
1567
1570
|
add_or_change(event, 'dtstart', self.calendar_timezone(start))
|
|
1568
1571
|
add_or_change(event, 'dtend', self.calendar_timezone(end))
|
|
1569
1572
|
if args.free:
|
|
@@ -1606,7 +1609,8 @@ class IcalendarInterface:
|
|
|
1606
1609
|
self.printer.msg("Event %s\n" % ("updated" if old
|
|
1607
1610
|
else "added"))
|
|
1608
1611
|
self.iterate_events(None, [event], print_count=False)
|
|
1609
|
-
self.
|
|
1612
|
+
if self.backend_interface.sync_after_edit:
|
|
1613
|
+
self.backend_cache_dirty = True
|
|
1610
1614
|
return True
|
|
1611
1615
|
|
|
1612
1616
|
def raw_ics(self, original=None):
|
|
@@ -1721,6 +1725,14 @@ def repl(ecal=None):
|
|
|
1721
1725
|
add_parser=get_add_parser(),
|
|
1722
1726
|
backend_interface=config.backend_interface,
|
|
1723
1727
|
printer=printer, **vars(FLAGS))
|
|
1728
|
+
if not hasattr(ecal.backend_interface, "sync_after_edit"):
|
|
1729
|
+
ecal.backend_interface.sync_after_edit = True
|
|
1730
|
+
# The file and etesync 1.0 backends operate on the cache and
|
|
1731
|
+
# they sync to server only when sync() is called
|
|
1732
|
+
# Some backends may work directly on server
|
|
1733
|
+
# and may require sync() only if server updated from another device
|
|
1734
|
+
# Such backends can set the attribute sync_after_edit = False
|
|
1735
|
+
# If attribute is missing, we assume it is True
|
|
1724
1736
|
if hasattr(config, 'timezones') and 'tz' in config.timezones:
|
|
1725
1737
|
IcalendarInterface.calendar_tz = config.timezones['tz']
|
|
1726
1738
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|