1.1 --- a/imiptools/stores/file.py Fri May 26 23:58:06 2017 +0200
1.2 +++ b/imiptools/stores/file.py Thu Jun 01 23:26:38 2017 +0200
1.3 @@ -26,15 +26,18 @@
1.4 from imiptools.data import Object, make_calendar, parse_object, to_stream
1.5 from imiptools.dates import format_datetime, get_datetime, to_timezone
1.6 from imiptools.filesys import fix_permissions, FileBase
1.7 -from imiptools.freebusy import FreeBusyPeriod, FreeBusyGroupPeriod, \
1.8 - FreeBusyOfferPeriod, \
1.9 - FreeBusyCollection, \
1.10 +
1.11 +from imiptools.freebusy import FreeBusyCollection, \
1.12 FreeBusyGroupCollection, \
1.13 - FreeBusyOffersCollection
1.14 -from imiptools.text import get_table, set_defaults
1.15 + FreeBusyOffersCollection, \
1.16 + period_from_tuple, \
1.17 + period_to_tuple
1.18 +
1.19 +from imiptools.text import FileTable, FileTableDict, FileTableSingle, \
1.20 + have_table
1.21 +
1.22 from os.path import isdir, isfile, join
1.23 from os import listdir, remove, rmdir
1.24 -import codecs
1.25
1.26 # Obtain defaults from the settings.
1.27
1.28 @@ -46,7 +49,7 @@
1.29
1.30 class FileStoreBase(FileBase):
1.31
1.32 - "A file store supporting user-specific locking and tabular data."
1.33 + "A file store supporting user-specific locking."
1.34
1.35 def acquire_lock(self, user, timeout=None):
1.36 FileBase.acquire_lock(self, timeout, user)
1.37 @@ -54,104 +57,6 @@
1.38 def release_lock(self, user):
1.39 FileBase.release_lock(self, user)
1.40
1.41 - # Utility methods.
1.42 -
1.43 - def _set_defaults(self, t, empty_defaults):
1.44 - return set_defaults(t, empty_defaults)
1.45 -
1.46 - def _get_table(self, filename, empty_defaults=None, tab_separated=True):
1.47 -
1.48 - """
1.49 - From the file having the given 'filename', return a list of tuples
1.50 - representing the file's contents.
1.51 -
1.52 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.53 - default value where a column either does not exist or provides an empty
1.54 - value.
1.55 -
1.56 - If 'tab_separated' is specified and is a false value, line parsing using
1.57 - the imiptools.text.parse_line function will be performed instead of
1.58 - splitting each line of the file using tab characters as separators.
1.59 - """
1.60 -
1.61 - return get_table(filename, empty_defaults, tab_separated)
1.62 -
1.63 - def _get_table_atomic(self, user, filename, empty_defaults=None, tab_separated=True):
1.64 -
1.65 - """
1.66 - From the file for the given 'user' having the given 'filename', return
1.67 - a list of tuples representing the file's contents.
1.68 -
1.69 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.70 - default value where a column either does not exist or provides an empty
1.71 - value.
1.72 -
1.73 - If 'tab_separated' is specified and is a false value, line parsing using
1.74 - the imiptools.text.parse_line function will be performed instead of
1.75 - splitting each line of the file using tab characters as separators.
1.76 - """
1.77 -
1.78 - self.acquire_lock(user)
1.79 - try:
1.80 - return self._get_table(filename, empty_defaults, tab_separated)
1.81 - finally:
1.82 - self.release_lock(user)
1.83 -
1.84 - def _set_table(self, filename, items, empty_defaults=None):
1.85 -
1.86 - """
1.87 - Write to the file having the given 'filename' the 'items'.
1.88 -
1.89 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.90 - default value where a column either does not exist or provides an empty
1.91 - value.
1.92 - """
1.93 -
1.94 - f = codecs.open(filename, "wb", encoding="utf-8")
1.95 - try:
1.96 - for item in items:
1.97 - self._set_table_item(f, item, empty_defaults)
1.98 - finally:
1.99 - f.close()
1.100 - fix_permissions(filename)
1.101 -
1.102 - def _set_table_item(self, f, item, empty_defaults=None):
1.103 -
1.104 - "Set in table 'f' the given 'item', using any 'empty_defaults'."
1.105 -
1.106 - if empty_defaults:
1.107 - item = self._set_defaults(list(item), empty_defaults)
1.108 - f.write("\t".join(item) + "\n")
1.109 -
1.110 - def _set_table_atomic(self, user, filename, items, empty_defaults=None):
1.111 -
1.112 - """
1.113 - For the given 'user', write to the file having the given 'filename' the
1.114 - 'items'.
1.115 -
1.116 - The 'empty_defaults' is a list of (index, value) tuples indicating the
1.117 - default value where a column either does not exist or provides an empty
1.118 - value.
1.119 - """
1.120 -
1.121 - self.acquire_lock(user)
1.122 - try:
1.123 - self._set_table(filename, items, empty_defaults)
1.124 - finally:
1.125 - self.release_lock(user)
1.126 -
1.127 - def _set_freebusy(self, user, freebusy, filename):
1.128 -
1.129 - """
1.130 - For the given 'user', convert the 'freebusy' details to a form suitable
1.131 - for writing to 'filename'.
1.132 - """
1.133 -
1.134 - # Obtain tuples from the free/busy objects.
1.135 -
1.136 - self._set_table_atomic(user, filename,
1.137 - map(lambda fb: freebusy.make_tuple(fb.as_tuple(strings_only=True)), list(freebusy)))
1.138 -
1.139 class Store(FileStoreBase, StoreBase):
1.140
1.141 "A file store of tabular free/busy data and objects."
1.142 @@ -438,64 +343,86 @@
1.143 """
1.144
1.145 filename = self.get_object_in_store(user, "freebusy-providers")
1.146 - if not filename or not isfile(filename):
1.147 + if not filename:
1.148 return None
1.149
1.150 # Attempt to read providers, with a declaration of the datetime
1.151 # from which such providers are considered as still being active.
1.152
1.153 - t = self._get_table_atomic(user, filename, [(1, None)])
1.154 - try:
1.155 - dt_string = t[0][0]
1.156 - except IndexError:
1.157 + t = self._get_freebusy_providers_table(filename)
1.158 + header = t.get_header_values()
1.159 + if not header:
1.160 return None
1.161
1.162 - return dt_string, t[1:]
1.163 + return header[0], t
1.164 +
1.165 + def _get_freebusy_providers_table(self, filename):
1.166 +
1.167 + "Return a file-based table for storing providers in 'filename'."
1.168
1.169 - def _set_freebusy_providers(self, user, dt_string, t):
1.170 + return FileTable(filename,
1.171 + in_defaults=[(1, None)],
1.172 + out_defaults=[(1, "")],
1.173 + headers=1)
1.174
1.175 - "Set the given provider timestamp 'dt_string' and table 't'."
1.176 + def _set_freebusy_providers(self, user, dt_string, providers):
1.177 +
1.178 + "Set the given provider timestamp 'dt_string' and 'providers'."
1.179
1.180 filename = self.get_object_in_store(user, "freebusy-providers")
1.181 if not filename:
1.182 return False
1.183
1.184 - t.insert(0, (dt_string,))
1.185 - self._set_table_atomic(user, filename, t, [(1, "")])
1.186 + self.acquire_lock(user)
1.187 + try:
1.188 + if not have_table(providers, filename):
1.189 + pr = self._get_freebusy_providers_table(filename)
1.190 + pr.replaceall(providers)
1.191 + providers = pr
1.192 + providers.set_header_values([dt_string])
1.193 + providers.close()
1.194 + finally:
1.195 + self.release_lock(user)
1.196 return True
1.197
1.198 # Free/busy period access.
1.199
1.200 - def get_freebusy(self, user, name=None, mutable=False, cls=None):
1.201 + def get_freebusy(self, user, name=None, mutable=False):
1.202
1.203 "Get free/busy details for the given 'user'."
1.204
1.205 filename = self.get_object_in_store(user, name or "freebusy")
1.206
1.207 - if not filename or not isfile(filename):
1.208 - periods = []
1.209 - else:
1.210 - cls = cls or FreeBusyPeriod
1.211 - periods = map(lambda t: cls(*t),
1.212 - self._get_table_atomic(user, filename))
1.213 + if not filename:
1.214 + return []
1.215
1.216 - return FreeBusyCollection(periods, mutable)
1.217 + return self._get_freebusy(filename, mutable, FreeBusyCollection)
1.218
1.219 - def get_freebusy_for_other(self, user, other, mutable=False, cls=None, collection=None):
1.220 + def get_freebusy_for_other(self, user, other, mutable=False, collection=None):
1.221
1.222 "For the given 'user', get free/busy details for the 'other' user."
1.223
1.224 filename = self.get_object_in_store(user, "freebusy-other", other)
1.225
1.226 - if not filename or not isfile(filename):
1.227 - periods = []
1.228 - else:
1.229 - cls = cls or FreeBusyPeriod
1.230 - periods = map(lambda t: cls(*t),
1.231 - self._get_table_atomic(user, filename))
1.232 + if not filename:
1.233 + return []
1.234 +
1.235 + return self._get_freebusy(filename, mutable, collection or FreeBusyCollection)
1.236 +
1.237 + def _get_freebusy(self, filename, mutable=False, collection=None):
1.238 +
1.239 + """
1.240 + Return a free/busy collection for 'filename' with the given 'mutable'
1.241 + condition, employing the specified 'collection' class.
1.242 + """
1.243
1.244 collection = collection or FreeBusyCollection
1.245 - return collection(periods, mutable)
1.246 +
1.247 + periods = FileTable(filename, mutable=mutable,
1.248 + in_converter=period_from_tuple(collection.period_class),
1.249 + out_converter=period_to_tuple)
1.250 +
1.251 + return collection(periods, mutable=mutable)
1.252
1.253 def set_freebusy(self, user, freebusy, name=None):
1.254
1.255 @@ -505,10 +432,9 @@
1.256 if not filename:
1.257 return False
1.258
1.259 - self._set_freebusy(user, freebusy, filename)
1.260 - return True
1.261 + return self._set_freebusy(user, freebusy, filename)
1.262
1.263 - def set_freebusy_for_other(self, user, freebusy, other):
1.264 + def set_freebusy_for_other(self, user, freebusy, other, collection=None):
1.265
1.266 "For the given 'user', set 'freebusy' details for the 'other' user."
1.267
1.268 @@ -516,7 +442,24 @@
1.269 if not filename:
1.270 return False
1.271
1.272 - self._set_freebusy(user, freebusy, filename)
1.273 + return self._set_freebusy(user, freebusy, filename, collection)
1.274 +
1.275 + def _set_freebusy(self, user, freebusy, filename, collection=None):
1.276 +
1.277 + "For the given 'user', set 'freebusy' details for the given 'filename'."
1.278 +
1.279 + # Copy to the specified table if different from that given.
1.280 +
1.281 + self.acquire_lock(user)
1.282 + try:
1.283 + if not have_table(freebusy, filename):
1.284 + fbc = self._get_freebusy(filename, True, collection)
1.285 + fbc += freebusy
1.286 + freebusy = fbc
1.287 + freebusy.close()
1.288 + finally:
1.289 + self.release_lock(user)
1.290 +
1.291 return True
1.292
1.293 def get_freebusy_others(self, user):
1.294 @@ -539,7 +482,11 @@
1.295
1.296 "Get free/busy offers for the given 'user'."
1.297
1.298 - offers = []
1.299 + filename = self.get_object_in_store(user, "freebusy-offers")
1.300 +
1.301 + if not filename:
1.302 + return []
1.303 +
1.304 expired = []
1.305 now = to_timezone(datetime.utcnow(), "UTC")
1.306
1.307 @@ -547,39 +494,43 @@
1.308
1.309 self.acquire_lock(user)
1.310 try:
1.311 - l = self.get_freebusy(user, "freebusy-offers", cls=FreeBusyOfferPeriod)
1.312 - for fb in l:
1.313 + offers = self._get_freebusy(filename, True, FreeBusyOffersCollection)
1.314 + for fb in offers:
1.315 if fb.expires and get_datetime(fb.expires) <= now:
1.316 - expired.append(fb)
1.317 - else:
1.318 - offers.append(fb)
1.319 -
1.320 + offers.remove(fb)
1.321 if expired:
1.322 - self.set_freebusy_offers(user, offers)
1.323 + offers.close()
1.324 finally:
1.325 self.release_lock(user)
1.326
1.327 - return FreeBusyOffersCollection(offers, mutable)
1.328 + offers.mutable = mutable
1.329 + return offers
1.330
1.331 # Requests and counter-proposals.
1.332
1.333 - def _get_requests(self, user, queue):
1.334 + def get_requests(self, user, queue="requests"):
1.335
1.336 "Get requests for the given 'user' from the given 'queue'."
1.337
1.338 filename = self.get_object_in_store(user, queue)
1.339 - if not filename or not isfile(filename):
1.340 + if not filename:
1.341 return []
1.342
1.343 - return self._get_table_atomic(user, filename, [(1, None), (2, None)])
1.344 + return FileTable(filename,
1.345 + in_defaults=[(1, None), (2, None)],
1.346 + out_defaults=[(1, ""), (2, "")])
1.347
1.348 - def get_requests(self, user):
1.349 + def set_request(self, user, uid, recurrenceid=None, type=None):
1.350
1.351 - "Get requests for the given 'user'."
1.352 + """
1.353 + For the given 'user', set the queued 'uid' and 'recurrenceid',
1.354 + indicating a request, along with any given 'type'.
1.355 + """
1.356
1.357 - return self._get_requests(user, "requests")
1.358 + requests = self.get_requests(user)
1.359 + return self.set_requests(user, [(uid, recurrenceid, type)])
1.360
1.361 - def _set_requests(self, user, requests, queue):
1.362 + def set_requests(self, user, requests, queue="requests"):
1.363
1.364 """
1.365 For the given 'user', set the list of queued 'requests' in the given
1.366 @@ -590,47 +541,20 @@
1.367 if not filename:
1.368 return False
1.369
1.370 - self._set_table_atomic(user, filename, requests, [(1, ""), (2, "")])
1.371 - return True
1.372 -
1.373 - def set_requests(self, user, requests):
1.374 -
1.375 - "For the given 'user', set the list of queued 'requests'."
1.376 -
1.377 - return self._set_requests(user, requests, "requests")
1.378 -
1.379 - def _set_request(self, user, request, queue):
1.380 -
1.381 - """
1.382 - For the given 'user', set the given 'request' in the given 'queue'.
1.383 - """
1.384 -
1.385 - filename = self.get_object_in_store(user, queue)
1.386 - if not filename:
1.387 - return False
1.388 + # Copy to the specified table if different from that given.
1.389
1.390 self.acquire_lock(user)
1.391 try:
1.392 - f = codecs.open(filename, "ab", encoding="utf-8")
1.393 - try:
1.394 - self._set_table_item(f, request, [(1, ""), (2, "")])
1.395 - finally:
1.396 - f.close()
1.397 - fix_permissions(filename)
1.398 + if not have_table(requests, filename):
1.399 + req = self.get_requests(user, queue)
1.400 + req.replaceall(requests)
1.401 + requests = req
1.402 + requests.close()
1.403 finally:
1.404 self.release_lock(user)
1.405
1.406 return True
1.407
1.408 - def set_request(self, user, uid, recurrenceid=None, type=None):
1.409 -
1.410 - """
1.411 - For the given 'user', set the queued 'uid' and 'recurrenceid',
1.412 - indicating a request, along with any given 'type'.
1.413 - """
1.414 -
1.415 - return self._set_request(user, (uid, recurrenceid, type), "requests")
1.416 -
1.417 def get_counters(self, user, uid, recurrenceid=None):
1.418
1.419 """
1.420 @@ -807,10 +731,10 @@
1.421 "Return a list of delegates for 'quota'."
1.422
1.423 filename = self.get_object_in_store(quota, "delegates")
1.424 - if not filename or not isfile(filename):
1.425 + if not filename:
1.426 return []
1.427
1.428 - return [value for (value,) in self._get_table_atomic(quota, filename)]
1.429 + return FileTableSingle(filename)
1.430
1.431 def set_delegates(self, quota, delegates):
1.432
1.433 @@ -820,7 +744,16 @@
1.434 if not filename:
1.435 return False
1.436
1.437 - self._set_table_atomic(quota, filename, [(value,) for value in delegates])
1.438 + self.acquire_lock(quota)
1.439 + try:
1.440 + if not have_table(delegates, filename):
1.441 + de = self.get_delegates(quota)
1.442 + de.replaceall(delegates)
1.443 + delegates = de
1.444 + delegates.close()
1.445 + finally:
1.446 + self.release_lock(quota)
1.447 +
1.448 return True
1.449
1.450 # Groups of users sharing quotas.
1.451 @@ -830,10 +763,10 @@
1.452 "Return the identity mappings for the given 'quota' as a dictionary."
1.453
1.454 filename = self.get_object_in_store(quota, "groups")
1.455 - if not filename or not isfile(filename):
1.456 + if not filename:
1.457 return {}
1.458
1.459 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
1.460 + return FileTableDict(filename, tab_separated=False)
1.461
1.462 def set_groups(self, quota, groups):
1.463
1.464 @@ -843,7 +776,16 @@
1.465 if not filename:
1.466 return False
1.467
1.468 - self._set_table_atomic(quota, filename, groups.items())
1.469 + self.acquire_lock(quota)
1.470 + try:
1.471 + if not have_table(groups, filename):
1.472 + gr = self.get_groups(quota)
1.473 + gr.updateall(groups)
1.474 + groups = gr
1.475 + groups.close()
1.476 + finally:
1.477 + self.release_lock(quota)
1.478 +
1.479 return True
1.480
1.481 def get_limits(self, quota):
1.482 @@ -854,10 +796,10 @@
1.483 """
1.484
1.485 filename = self.get_object_in_store(quota, "limits")
1.486 - if not filename or not isfile(filename):
1.487 + if not filename:
1.488 return {}
1.489
1.490 - return dict(self._get_table_atomic(quota, filename, tab_separated=False))
1.491 + return FileTableDict(filename, tab_separated=False)
1.492
1.493 def set_limits(self, quota, limits):
1.494
1.495 @@ -870,7 +812,16 @@
1.496 if not filename:
1.497 return False
1.498
1.499 - self._set_table_atomic(quota, filename, limits.items())
1.500 + self.acquire_lock(quota)
1.501 + try:
1.502 + if not have_table(limits, filename):
1.503 + li = self.get_limits(quota)
1.504 + li.updateall(limits)
1.505 + limits = li
1.506 + limits.close()
1.507 + finally:
1.508 + self.release_lock(quota)
1.509 +
1.510 return True
1.511
1.512 # Journal entry methods.
1.513 @@ -896,6 +847,9 @@
1.514 # Compatibility methods.
1.515
1.516 def get_freebusy_for_other(self, user, other, mutable=False):
1.517 - return Store.get_freebusy_for_other(self, user, other, mutable, cls=FreeBusyGroupPeriod, collection=FreeBusyGroupCollection)
1.518 + return Store.get_freebusy_for_other(self, user, other, mutable, collection=FreeBusyGroupCollection)
1.519 +
1.520 + def set_freebusy_for_other(self, user, entries, other):
1.521 + Store.set_freebusy_for_other(self, user, entries, other, collection=FreeBusyGroupCollection)
1.522
1.523 # vim: tabstop=4 expandtab shiftwidth=4