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.
Files changed (23) hide show
  1. {icalcli-1.1 → icalcli-1.1.3}/PKG-INFO +1 -1
  2. {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etebase_crud.py +74 -26
  3. {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etebase_interface.py +11 -7
  4. {icalcli-1.1 → icalcli-1.1.3}/icalcli/icalcli.py +17 -5
  5. {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/PKG-INFO +1 -1
  6. {icalcli-1.1 → icalcli-1.1.3}/setup.py +1 -1
  7. {icalcli-1.1 → icalcli-1.1.3}/LICENSE +0 -0
  8. {icalcli-1.1 → icalcli-1.1.3}/README.md +0 -0
  9. {icalcli-1.1 → icalcli-1.1.3}/icalcli/__init__.py +0 -0
  10. {icalcli-1.1 → icalcli-1.1.3}/icalcli/argparsers.py +0 -0
  11. {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/__init__.py +0 -0
  12. {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etesync_crud.py +0 -0
  13. {icalcli-1.1 → icalcli-1.1.3}/icalcli/etesync_backend/etesync_interface.py +0 -0
  14. {icalcli-1.1 → icalcli-1.1.3}/icalcli/file_backend/__init__.py +0 -0
  15. {icalcli-1.1 → icalcli-1.1.3}/icalcli/file_backend/ics_interface.py +0 -0
  16. {icalcli-1.1 → icalcli-1.1.3}/icalcli/printer.py +0 -0
  17. {icalcli-1.1 → icalcli-1.1.3}/icalcli/utils.py +0 -0
  18. {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/SOURCES.txt +0 -0
  19. {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/dependency_links.txt +0 -0
  20. {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/entry_points.txt +0 -0
  21. {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/requires.txt +0 -0
  22. {icalcli-1.1 → icalcli-1.1.3}/icalcli.egg-info/top_level.txt +0 -0
  23. {icalcli-1.1 → icalcli-1.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: icalcli
3
- Version: 1.1
3
+ Version: 1.1.3
4
4
  Summary: Icalendar Calendar Command Line Interface
5
5
  Home-page: https://github.com/jrvarma/icalcli
6
6
  Maintainer: Jayanth R. Varma
@@ -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
- def __init__(self, user, server_url, password, calendar_uid, silent=True):
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, uid):
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": uid,
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, uid):
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
- uid : uid of event to be updated
87
+ event_uid : uid of event to be updated
76
88
  """
77
- item = self.item_mgr.fetch(uid)
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, uid):
95
+ def retrieve_event(self, event_uid):
82
96
  r"""Retrieve event by uid
83
97
 
84
98
  Parameters
85
99
  ----------
86
- uid : uid of event to be retrieved
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
- return self.item_mgr.fetch(uid).content.decode()
93
-
94
- def all_events(self):
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, uid):
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(uid)
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
- synced = False
145
+ done = False
146
+ chunk = 100
125
147
  for i in range(5):
126
148
  try:
127
- self.items = self.item_mgr.list(FetchOptions().limit(10**6))
128
- synced = True
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 synced:
161
+ if done:
134
162
  silent or print("Syncing completed.")
135
- self.all_events()
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 etes.sync when needed. For example:
30
- # (a) if the server has been updated from another device
31
- # (b) after any CRUD operation other than Retrieve
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, silent=True):
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, calendar_uid, silent)
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 EtebaseCRUD.all_events(self)]
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
- EtebaseCRUD.create_event(self, ics)
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.backend_cache_dirty = True
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
- args.summary = old.Decoded('summary').decode()
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 args.summary:
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', args.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.backend_cache_dirty = True
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: icalcli
3
- Version: 1.1
3
+ Version: 1.1.3
4
4
  Summary: Icalendar Calendar Command Line Interface
5
5
  Home-page: https://github.com/jrvarma/icalcli
6
6
  Maintainer: Jayanth R. Varma
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
  long_description = fh.read()
6
6
 
7
7
  setup(name='icalcli',
8
- version='1.1',
8
+ version='1.1.3',
9
9
  maintainer='Jayanth R. Varma',
10
10
  maintainer_email='jrvarma@gmail.com',
11
11
  description='Icalendar Calendar Command Line Interface',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes